为什么numpy的einsum比numpy的内置函数更快?
为什么numpy的einsum比numpy的内置函数更快?
让我们从三个dtype=np.double
的数组开始。定时是在使用numpy 1.7.1编译的、链接到英特尔的mkl的icc编译器上的英特尔CPU上进行的。还使用了一个使用gcc编译的numpy 1.6.1的AMD CPU来验证定时。请注意,定时与系统大小几乎呈线性比例,不是由于numpy函数中的微小开销造成的,这些差异将以微秒而不是毫秒显示出来:\n
arr_1D=np.arange(500,dtype=np.double) large_arr_1D=np.arange(100000,dtype=np.double) arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500) arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)
\n首先让我们看一下np.sum
函数:\n
np.all(np.sum(arr_3D)==np.einsum('ijk->',arr_3D)) True %timeit np.sum(arr_3D) 10 loops, best of 3: 142 ms per loop %timeit np.einsum('ijk->', arr_3D) 10 loops, best of 3: 70.2 ms per loop
\n幂运算:\n
np.allclose(arr_3D*arr_3D*arr_3D,np.einsum('ijk,ijk,ijk->ijk',arr_3D,arr_3D,arr_3D)) True %timeit arr_3D*arr_3D*arr_3D 1 loops, best of 3: 1.32 s per loop %timeit np.einsum('ijk,ijk,ijk->ijk', arr_3D, arr_3D, arr_3D) 1 loops, best of 3: 694 ms per loop
\n外积:\n
np.all(np.outer(arr_1D,arr_1D)==np.einsum('i,k->ik',arr_1D,arr_1D)) True %timeit np.outer(arr_1D, arr_1D) 1000 loops, best of 3: 411 us per loop %timeit np.einsum('i,k->ik', arr_1D, arr_1D) 1000 loops, best of 3: 245 us per loop
\n以上所有操作都使用np.einsum
速度是两倍快的。这些应该是苹果和苹果的比较,因为所有的东西都是特定的dtype=np.double
。我预计这样的操作将加速:\n
np.allclose(np.sum(arr_2D*arr_3D),np.einsum('ij,oij->',arr_2D,arr_3D)) True %timeit np.sum(arr_2D*arr_3D) 1 loops, best of 3: 813 ms per loop %timeit np.einsum('ij,oij->', arr_2D, arr_3D) 10 loops, best of 3: 85.1 ms per loop
\n与np.inner
、np.outer
、np.kron
和np.sum
相比,np.einsum
至少快两倍,无论axes
选择如何。主要异常是np.dot
,因为它调用了BLAS库中的DGEMM。那么为什么np.einsum
比其他等效的numpy函数更快呢?\n为了完整起见,DGEMM的情况如下:\n
np.allclose(np.dot(arr_2D,arr_2D),np.einsum('ij,jk',arr_2D,arr_2D)) True %timeit np.einsum('ij,jk',arr_2D,arr_2D) 10 loops, best of 3: 56.1 ms per loop %timeit np.dot(arr_2D,arr_2D) 100 loops, best of 3: 5.17 ms per loop
\n
\n主要理论来自@sebergs的评论,即np.einsum
可以利用SSE2,但直到numpy 1.8才能使用numpy的ufuncs(请参阅更改日志)。我相信这是正确的答案,但尚未能够确认。通过更改输入数组的dtype并观察速度差异,以及并不是每个人都观察到相同的定时趋势,可以找到一些有限的证据。
为什么numpy的einsum函数比numpy的内置函数运行得更快?
通过观察下面的计时结果,我们可以解释其中的原因:
a = np.arange(1000, dtype=np.double) %timeit np.einsum('i->', a) 100000 loops, best of 3: 3.32 us per loop %timeit np.sum(a) 100000 loops, best of 3: 6.84 us per loop a = np.arange(10000, dtype=np.double) %timeit np.einsum('i->', a) 100000 loops, best of 3: 12.6 us per loop %timeit np.sum(a) 100000 loops, best of 3: 16.5 us per loop a = np.arange(100000, dtype=np.double) %timeit np.einsum('i->', a) 10000 loops, best of 3: 103 us per loop %timeit np.sum(a) 10000 loops, best of 3: 109 us per loop
可以看出,调用`np.sum`相比于`np.einsum`会有大约3微秒的额外开销,所以它们的运行速度基本相同,只是`np.sum`需要更长的启动时间。为什么会出现这种情况呢?我认为可能是以下原因:
a = np.arange(1000, dtype=object) %timeit np.einsum('i->', a) Traceback (most recent call last): ... TypeError: invalid data type for einsum %timeit np.sum(a) 10000 loops, best of 3: 20.3 us per loop
我不确定具体发生了什么,但似乎`np.einsum`跳过了一些检查,以便提取特定类型的函数来执行乘法和加法运算,而仅适用于标准的C类型。
多维情况下的情况也是类似的:
n = 10; a = np.arange(n**3, dtype=np.double).reshape(n, n, n) %timeit np.einsum('ijk->', a) 100000 loops, best of 3: 3.79 us per loop %timeit np.sum(a) 100000 loops, best of 3: 7.33 us per loop n = 100; a = np.arange(n**3, dtype=np.double).reshape(n, n, n) %timeit np.einsum('ijk->', a) 1000 loops, best of 3: 1.2 ms per loop %timeit np.sum(a) 1000 loops, best of 3: 1.23 ms per loop
可以看出,它们的开销基本相同,一旦开始执行计算,运行速度就会接近。
此外,文档中提到`einsum`函数不会自动进行广播,而是依赖于用户来表达广播操作的规则。因此,`einsum`可以跳过许多检查(类型检查、广播等)。
,numpy的einsum函数比numpy的内置函数运行得更快的原因可能是因为einsum函数跳过了一些检查来提高性能,并且使用了特定类型的函数来执行乘法和加法运算。同时,einsum函数也没有自动进行广播,而是依赖于用户来指定广播规则。这些优化使得einsum函数在处理大规模数据时更加高效。
为了验证Seberg关于SSE2的评论是否有效,我创建了一个新的python 2.7安装程序,并使用标准选项在运行Ubuntu的AMD Opteron核心上使用icc编译了numpy 1.7和1.8。以下是在1.8升级之前和之后运行的测试结果:
Numpy 1.7.1:
Summation test:
0.172988510132
0.0934836149216
----------------------
Power test:
1.93524689674
0.839519000053
----------------------
Outer test:
0.130380821228
0.121401786804
----------------------
Einsum test:
0.979052495956
0.126066613197
Numpy 1.8:
Summation test:
0.116551589966
0.0920487880707
----------------------
Power test:
1.23683619499
0.815982818604
----------------------
Outer test:
0.131808176041
0.127472200394
----------------------
Einsum test:
0.781750011444
0.129271841049
从上述结果可以看出,SSE在计时差异中起到了重要作用,值得注意的是,重复这些测试的计时只相差约0.003秒。其余差异应该可以在这个问题的其他答案中得到解释。
这是一个很好的后续工作!这是我需要更频繁地使用einsum的另一个原因。顺便说一句,我认为你应该将自己的答案标记为正确答案。
为什么numpy的einsum比numpy内置函数要快?
首先,关于这个问题,在numpy列表中已经有很多讨论。例如,参考以下链接:http://numpy-discussion.10968.n7.nabble.com/poor-performance-of-sum-with-sub-machine-word-integer-types-td41.html 和 http://numpy-discussion.10968.n7.nabble.com/odd-performance-of-sum-td3332.html。
其中一些问题归结为einsum是新的,而且可能在缓存对齐和其他内存访问问题上更加注意,而许多旧的numpy函数则注重易于移植的实现而不是高度优化的实现。尽管这只是我的猜测。
然而,你所做的比较并不完全是"一比一"的比较。
除了我已经说过的之外,sum函数对于数组使用了更合适的累加器。
例如,sum函数对输入的类型进行更仔细的检查,并使用适当的累加器。例如,考虑以下示例:
In [1]: x = 255 * np.ones(100, dtype=np.uint8) In [2]: x Out[2]: array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], dtype=uint8)
注意,sum的结果是正确的:
In [3]: x.sum() Out[3]: 25500
而einsum将给出错误的结果:
In [4]: np.einsum('i->', x) Out[4]: 156
但是,如果我们使用一个不那么受限制的dtype,我们仍然会得到你期望的结果:
In [5]: y = 255 * np.ones(100) In [6]: np.einsum('i->', y) Out[6]: 25500.0
你有关于sum函数如何选择累加器的好链接吗?有趣的是,当你的x数组扩展到1E8个元素时,np.einsum('i->',x,dtype=np.uint64)只比sum函数快大约10%(15毫秒)。
- sum的文档中有一些细节。你可以使用sum的dtype关键字参数来指定它。如果没有指定,并且数组具有比"默认平台整数"(通常是int64,即使在32位平台上也是如此)更低精度的整数dtype,则默认为默认整数。参见:docs.scipy.org/doc/numpy/reference/generated/numpy.sum.html
另外,sum是通过np.add.reduce实现的,所以如果你对细节感兴趣,可以看一下这里的reduction ufunc的源代码:github.com/numpy/numpy/blob/master/numpy/core/src/umath/…
如果我理解正确,这些都是"一比一"的比较,因为所有东西都明确限制为dtype=np.double?
我认为是的。毕竟,这就是你一开始所做的。因此,我提出的观点可能并不那么相关。