使用 NumExpr 提升 NumPy 代码的运行时间:分析

kmario23

由于 NumPy 不使用多核,我正在学习使用 NumExpr 加速 NumPy 代码,因为它对多线程有很好的支持。以下是我正在使用的示例:

# input array to work with
x = np.linspace(-1, 1, 1e7)

# a cubic polynomial expr
cubic_poly = 0.25*x**3 + 0.75*x**2 + 1.5*x - 2

%timeit -n 10 cubic_poly = 0.25*x**3 + 0.75*x**2 + 1.5*x - 2
# 657 ms ± 5.04 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

现在,我们可以使用 NumExpr 做同样的事情:

cubic_poly_str = "0.25*x**3 + 0.75*x**2 + 1.5*x - 2"
# set number of threads to 1 for fair comparison
ne.set_num_threads(1)

%timeit -n 10 ne.evaluate(cubic_poly_str)
# 60.5 ms ± 908 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

正如我们从定时看,NumExpr10倍以上,更快,甚至当我们使用相同数量的线程13759 NumPy的(即1)


现在,让我们增加计算并使用所有可用线程并观察:

# use all available threads/cores
ne.set_num_threads(ne.detect_number_of_threads())

%timeit -n 10 ne.evaluate(cubic_poly_str)
# 16.1 ms ± 82.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# sanity check
np.allclose(cubic_poly, ne.evaluate(cubic_poly_str))

不出所料且令人信服的是,这比仅使用单线程快 5 倍

为什么即使使用相同数量的线程(即 1) NumExpr 也快 10 倍?

您认为加速仅/主要来自并行化的假设是错误的。正如@Brenlla 已经指出的那样, numexpr 加速的最大份额通常来自更好地利用缓存。然而,还有一些其他原因。

首先, numpy 和 numexpr 不计算相同的表达式:

  • numpy 计算x**3andx**2作为pow(x,3)and pow(x,2)
  • numexpr 随意将其评估为x**3=x*x*xand x**2=x*x

pow 比一两次乘法更复杂,因此慢得多,比较:

ne.set_num_threads(1)
%timeit ne.evaluate("0.25*x**3 + 0.75*x**2 + 1.5*x - 2")
# 60.7 ms ± 1.2 ms, base line on my machine

%timeit 0.25*x**3 + 0.75*x**2 + 1.5*x - 2
# 766 ms ± 4.02 ms
%timeit 0.25*x*x*x + 0.75*x*x + 1.5*x - 2 
# 130 ms ± 692 µs 

现在, numexpr 的速度只有两倍。我的猜测是,pow-version 受 CPU 限制,而乘法版本受更多内存限制。

当数据很大时,Numexpr 最有效 - 大于 L3 缓存(例如我机器上的 15Mb),这在您的示例中给出,x大约为 76Mb:

  • numexp 按块计算 - 即所有操作都针对一个块进行评估,并且每个块(至少)适合 L3 缓存,从而最大限度地提高缓存的利用率。只有在完成一个块后,才会评估另一个块。
  • numpy 对整个数据进行一个又一个的评估,因此数据在可以重用之前从缓存中被逐出。

例如,我们可以查看缓存未命中valgrind(请参阅本文附录中的脚本):

>>> valgrind --tool=cachegrind python np_version.py
...
...
==5676== D   refs:      1,144,572,370  (754,717,376 rd   + 389,854,994 wr)
==5676== D1  misses:      220,844,716  (181,436,970 rd   +  39,407,746 wr)
==5676== LLd misses:      217,056,340  (178,062,890 rd   +  38,993,450 wr)
==5676== D1  miss rate:          19.3% (       24.0%     +        10.1%  )
==5676== LLd miss rate:          19.0% (       23.6%     +        10.0%  )
....

对我们来说有趣的部分是LLd-misses(即 L3 未命中,有关输出解释的信息,请参见此处) - 大约 25% 的读访问是未命中。

对 numexpr 的相同分析显示:

>>> valgrind --tool=cachegrind python ne_version.py 
...
==5145== D   refs:      2,612,495,487  (1,737,673,018 rd   + 874,822,469 wr)
==5145== D1  misses:      110,971,378  (   86,949,951 rd   +  24,021,427 wr)
==5145== LLd misses:       29,574,847  (   15,579,163 rd   +  13,995,684 wr)
==5145== D1  miss rate:           4.2% (          5.0%     +         2.7%  )
==5145== LLd miss rate:           1.1% (          0.9%     +         1.6%  )
...

只有 5% 的读取未命中!

然而,numpy 也有一些优点:在引擎盖下 numpy 使用 mkl 例程(至少在我的机器上),而 numexpr 没有。因此 numpy 最终使用打包的 SSE 操作(movups+ mulpd+ addpd),而 numexpr 最终使用标量版本(movsd+ mulsd)。

这解释了 numpy 版本的 25% 未命中率:一次读取是 128 位 ( movups),这意味着在 4 次读取后处理缓存行 (64 字节) 并产生未命中。它可以在配置文件中看到(例如perf在 Linux 上):

 32,93 │       movups 0x10(%r15,%rcx,8),%xmm4                                                                               
  1,33 │       movups 0x20(%r15,%rcx,8),%xmm5                                                                               
  1,71 │       movups 0x30(%r15,%rcx,8),%xmm6                                                                               
  0,76 │       movups 0x40(%r15,%rcx,8),%xmm7                                                                               
 24,68 │       movups 0x50(%r15,%rcx,8),%xmm8                                                                               
  1,21 │       movups 0x60(%r15,%rcx,8),%xmm9                                                                               
  2,54 │       movups 0x70(%r15,%rcx,8),%xmm10 

每四分之一movups需要更多时间,因为它等待内存访问。


Numpy 适合较小的数组大小,适合 L1 缓存(但最大的份额是开销而不是计算本身,这在 numpy 中更快 - 但这并没有起到很大的作用):

x = np.linspace(-1, 1, 10**3)
%timeit ne.evaluate("0.25*x*x*x + 0.75*x*x + 1.5*x - 2")
# 20.1 µs ± 306 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%timeit 0.25*x*x*x + 0.75*x*x + 1.5*x - 2
# 13.1 µs ± 125 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

附带说明:将函数评估为 会更快((0.25*x + 0.75)*x + 1.5)*x - 2

两者都是因为较少的 CPU 使用率:

# small x - CPU bound
x = np.linspace(-1, 1, 10**3)
%timeit ((0.25*x + 0.75)*x + 1.5)*x - 2
#  9.02 µs ± 204 ns 

和更少的内存访问:

# large x - memory bound
x = np.linspace(-1, 1, 10**7)
%timeit ((0.25*x + 0.75)*x + 1.5)*x - 2
#  73.8 ms ± 3.71 ms

房源:

np_version.py

import numpy as np

x = np.linspace(-1, 1, 10**7)
for _ in range(10):
    cubic_poly = 0.25*x*x*x + 0.75*x*x + 1.5*x - 2

ne_version.py

import numpy as np
import numexpr as ne

x = np.linspace(-1, 1, 10**7)
ne.set_num_threads(1)
for _ in range(10):
    ne.evaluate("0.25*x**3 + 0.75*x**2 + 1.5*x - 2")

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

使用numpy或scipy优化sympy代码的运行时

来自分类Dev

使用GLCM减少纹理分析的运行时间[Python]

来自分类Dev

使用GLCM减少纹理分析的运行时间[Python]

来自分类Dev

使用numexpr创建可调用对象

来自分类Dev

使用Numpy进行电源运行时警告

来自分类Dev

性能比较Fortran,Numpy,Cython和Numexpr

来自分类Dev

如何使用/不使用PowerShell的提升特权来运行exe

来自分类Dev

使用Big-O渐近分析计算总运行时间

来自分类Dev

使用CLRS代码和Robert Sedgewick代码的插入排序运行时间的差异

来自分类Dev

在测量代码的运行时间时,我将如何使用执行代码来求解矩阵?

来自分类Dev

使用 UAC 提升权限

来自分类Dev

如何在python中使用numpy.linalg.matrix_power将矩阵提升为较大的幂?

来自分类Dev

使用提升的权限从批处理文件运行 Powershell 脚本?

来自分类Dev

在运行时提升Java应用程序

来自分类Dev

使用QueryPerformanceCounter()向后运行时间

来自分类Dev

LowLevelKeyboardProc 在作为非提升运行时被提升的应用程序调用

来自分类Dev

使用 SUID 人为提升权限

来自分类Dev

使用numpy和gdal的Python C扩展在运行时提供未定义的符号

来自分类Dev

使用np.python的numpy运行时警告,其中np.errstate和警告'错误'

来自分类Dev

在numpy中使用lagrange插值时的运行时警告

来自分类Dev

使用CreateProcessAsUser函数从提升的流程创建非提升的流程

来自分类Dev

链表和ArrayList之间的运行时间?代码分析

来自分类Dev

我使用cudaEvent来衡量代码的运行时间。但是我发现了一些难题

来自分类Dev

如何使用ActionFilterAttribute记录运行时间?

来自分类Dev

使用list :: size()后,运行时间显着增加

来自分类Dev

使用潜在方法查找摊销的运行时间?

来自分类Dev

使用Qt测量过程的实际运行时间

来自分类Dev

使用电池获得系统正常运行时间?

来自分类Dev

如何使用ActionFilterAttribute记录运行时间?

Related 相关文章

  1. 1

    使用numpy或scipy优化sympy代码的运行时

  2. 2

    使用GLCM减少纹理分析的运行时间[Python]

  3. 3

    使用GLCM减少纹理分析的运行时间[Python]

  4. 4

    使用numexpr创建可调用对象

  5. 5

    使用Numpy进行电源运行时警告

  6. 6

    性能比较Fortran,Numpy,Cython和Numexpr

  7. 7

    如何使用/不使用PowerShell的提升特权来运行exe

  8. 8

    使用Big-O渐近分析计算总运行时间

  9. 9

    使用CLRS代码和Robert Sedgewick代码的插入排序运行时间的差异

  10. 10

    在测量代码的运行时间时,我将如何使用执行代码来求解矩阵?

  11. 11

    使用 UAC 提升权限

  12. 12

    如何在python中使用numpy.linalg.matrix_power将矩阵提升为较大的幂?

  13. 13

    使用提升的权限从批处理文件运行 Powershell 脚本?

  14. 14

    在运行时提升Java应用程序

  15. 15

    使用QueryPerformanceCounter()向后运行时间

  16. 16

    LowLevelKeyboardProc 在作为非提升运行时被提升的应用程序调用

  17. 17

    使用 SUID 人为提升权限

  18. 18

    使用numpy和gdal的Python C扩展在运行时提供未定义的符号

  19. 19

    使用np.python的numpy运行时警告,其中np.errstate和警告'错误'

  20. 20

    在numpy中使用lagrange插值时的运行时警告

  21. 21

    使用CreateProcessAsUser函数从提升的流程创建非提升的流程

  22. 22

    链表和ArrayList之间的运行时间?代码分析

  23. 23

    我使用cudaEvent来衡量代码的运行时间。但是我发现了一些难题

  24. 24

    如何使用ActionFilterAttribute记录运行时间?

  25. 25

    使用list :: size()后,运行时间显着增加

  26. 26

    使用潜在方法查找摊销的运行时间?

  27. 27

    使用Qt测量过程的实际运行时间

  28. 28

    使用电池获得系统正常运行时间?

  29. 29

    如何使用ActionFilterAttribute记录运行时间?

热门标签

归档