为什么numpy的einsum比numpy的内置函数更快?

让我们从三个dtype=np.double数组开始。 计时是在英特尔CPU上使用nccu 1.7.1与icc编译并链接到英特尔mkl 。 用gcc不带mkl编译的numpy 1.6.1的AMD cpu也被用来validation时序。 请注意,时序规模几乎与系统规模成线性关系,并不是因为numpy函数中的小开销, if这些差异将以微秒而不是毫秒显示:

 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) 

首先让我们看一下np.sum函数:

 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 

鲍尔斯:

 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 

外部产品:

 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 

以上所有都是np.einsum两倍。 这些应该是苹果比较苹果,因为一切都是特别的dtype=np.double 。 我希望在这样的操作中加快速度:

 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 

对于np.innernp.outernp.kronnp.sum np.kron ,Einsum似乎至less快了两倍,而不pipeaxesselect如何。 主要的例外是np.dot因为它从BLAS库调用DGEMM。 那么为什么np.einsum比其他numpy函数更快呢?

DGEMM案件的完整性:

 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 

领先的理论来自@sebergs评论np.einsum可以使用SSE2 ,但numpy的ufuncs不会直到numpy 1.8(请参阅更改日志 )。 我相信这是正确的答案,但一直未能证实。 一些有限的certificate可以通过改变input数组的dtype和观察速度差异来find,并且不是每个人都遵守同样的时间趋势。

首先,关于这个在numpy名单上的讨论已经有很多了。 例如,见: http: discussion.10968.n7.nabble.com/odd-performance-of-sum-td3332.html

一些归结为这个事实,即einsum是新的,大概是试图更好地cachingalignment和其他内存访问问题,而许多旧的numpyfunction集中在一个轻松便携的实施,而不是一个严重优化。 不过,我只是在猜测。


然而,你所做的一些不是一个“苹果对苹果”的比较。

除了@Jamie已经说过的之外, sum使用了一个更适合数组的累加器

例如, sum对于检查input的types和使用适当的累加器更加小心。 例如,请考虑以下几点:

 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, 255], dtype=uint8) 

请注意, sum是正确的:

 In [3]: x.sum() Out[3]: 25500 

虽然“ einsum会给出错误的结果:

 In [4]: np.einsum('i->', x) Out[4]: 156 

但是如果我们使用一个更less的dtype ,我们仍然会得到你所期望的结果:

 In [5]: y = 255 * np.ones(100) In [6]: np.einsum('i->', y) Out[6]: 25500.0 

现在numpy 1.8发布了,根据文档所有ufuncs都应该使用SSE2,我想仔细检查Seberg对SSE2的评论是否有效。

为了执行testing,创build了一个新的python 2.7安装 – numpy 1.7和1.8在运行Ubuntu的AMD opteron内核上使用标准选项使用icc编译。

这是1.8升级之前和之后的testing运行:

 import numpy as np import timeit arr_1D=np.arange(5000,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) print 'Summation test:' print timeit.timeit('np.sum(arr_3D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print timeit.timeit('np.einsum("ijk->", arr_3D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print '----------------------\n' print 'Power test:' print timeit.timeit('arr_3D*arr_3D*arr_3D', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print timeit.timeit('np.einsum("ijk,ijk,ijk->ijk", arr_3D, arr_3D, arr_3D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print '----------------------\n' print 'Outer test:' print timeit.timeit('np.outer(arr_1D, arr_1D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print timeit.timeit('np.einsum("i,k->ik", arr_1D, arr_1D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print '----------------------\n' print 'Einsum test:' print timeit.timeit('np.sum(arr_2D*arr_3D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print timeit.timeit('np.einsum("ij,oij->", arr_2D, arr_3D)', 'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D', number=5)/5 print '----------------------\n' 

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 

我认为这是相当确定的,上证所在时差方面起着很大的作用,应该指出的是,重复这些testing的时机只有0.003秒左右。 剩下的差别应该在这个问题的其他答案中加以说明。

我认为这些时间解释了发生了什么事情:

 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.sum时基本上有一个几乎恒定的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正在跳过一些检查来提取特定于types的函数来执行乘法和加法操作,并且仅针对标准Ctypes直接使用*+


多维的情况并没有什么不同:

 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 

所以一个大部分是不变的开销,一旦他们接近它,不是一个更快的运行。