为什么numpy sum比+运算符慢10倍?

时间:2018-10-31 22:59:00

标签: python performance numpy

我非常紧张地注意到np.sum比手写的总和慢10倍。

轴总和:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1.sum(axis=1)
%timeit test(p1)
  

每个循环186 µs±4.21 µs(平均±标准偏差,共运行7次,每个循环1000次)

不带轴的np.sum:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1.sum()
%timeit test(p1)
  

每个循环17.9 µs±236 ns(平均±标准偏差,共运行7次,每个循环10000次)

+:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1[:,0] + p1[:,1]
%timeit test(p1)
  

每个循环15.8 µs±328 ns(平均±标准偏差,共运行7次,每个循环100000次)

乘法:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1[:,0]*p1[:,1]
%timeit test(p1)
  

每个循环15.7 µs±701 ns(平均±标准偏差,共运行7次,每个循环10000次)

我没有看到任何原因。知道为什么吗?我的numpy版本是1.15.3

编辑: 10000000:

np.sum (with axis): 202 ms (5 x)
np.sum (without axis): 12 ms
+ : 46 ms (1 x)
* : 44.3 ms 

所以我想一定程度上有一些开销...

2 个答案:

答案 0 :(得分:7)

主要区别是计算a.sum(axis=1)时开销较大。计算减少量(在这种情况下为sum)不是一件小事:

  • 一个人必须考虑到舍入误差,因此使用pairwise summation来减少误差。
  • 平铺对于更大的阵列很重要,因为它可以充分利用可用的缓存
  • 为了能够使用现代CPU的SIMD指令/乱序执行功能,人们应该并行计算多行

我已经更详细地讨论了上面的主题,例如herehere

但是,如果仅添加两个元素,则不需要所有这些操作,也不会比单纯的求和好-您获得相同的结果,但开销却少得多,而且速度更快。

对于仅1000个元素,调用numpy功能的开销可能比实际执行这1000次加法(或乘法)要高,因为在现代CPU上,流水线式的加法/乘法具有相同的成本)-如您所见,对于10 ^ 3,运行时间仅高出约2倍,这无疑表明开销在10 ^ 3中起着更大的作用!在this answer中,将详细研究开销和缓存未命中的影响。

让我们看一下探查器结果,看看上面的理论是否成立(我使用perf):

对于a.sum(axis=1)

  17,39%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] reduce_loop
  11,41%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] pairwise_sum_DOUBLE
   9,78%  python   multiarray.cpython-36m-x86_64-linux-gnu.so  [.] npyiter_buffered_reduce_iternext_ite
   9,24%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] DOUBLE_add
   4,35%  python   python3.6                                   [.] _PyEval_EvalFrameDefault
   2,17%  python   multiarray.cpython-36m-x86_64-linux-gnu.so  [.] _aligned_strided_to_contig_size8_src
   2,17%  python   python3.6                                   [.] lookdict_unicode_nodummy
   ...

使用reduce_looppairwise_sum_DOUBLE的开销占主导。

对于a[:,0]+a[:,1])

   7,24%  python   python3.6                                   [.] _PyEval_EvalF
   5,26%  python   python3.6                                   [.] PyObject_Mall
   3,95%  python   python3.6                                   [.] visit_decref
   3,95%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] DOUBLE_add
   2,63%  python   python3.6                                   [.] PyDict_SetDef
   2,63%  python   python3.6                                   [.] _PyTuple_Mayb
   2,63%  python   python3.6                                   [.] collect
   2,63%  python   python3.6                                   [.] fast_function
   2,63%  python   python3.6                                   [.] visit_reachab
   1,97%  python   python3.6                                   [.] _PyObject_Gen

如预期的那样:Python开销起很大作用,使用了简单的DOUBLE_add


调用a.sum()

时开销较小
  • 对于一次,reduce_loop并不是每行都被调用,而只会被调用一次,这意味着开销要少得多。
  • 不会创建新的结果数组,不再需要向内存写入1000个double。

因此可以预期a.sum()会更快(尽管事实是必须添加2000而不是1000,但正如我们所看到的主要是开销和实际工作一样,并不是运行时间的很大一部分。


通过运行获取数据:

perf record python run.py
perf report

#run.py
import numpy as np
a=np.random.rand(1000,2)

for _ in range(10000):
  a.sum(axis=1)
  #a[:,0]+a[:,1]

答案 1 :(得分:0)

对于带有轴vs不带轴的.sum(),该轴必须生成与输入一样长的浮点数组,每行都有一个元素。这意味着它必须沿轴= 1调用reduce() 10,000次。如果没有axis参数,它将把每个元素的总和计算为单个浮点数,这只是通过数组的平面表示来减少的一个调用。

我不确定为什么手动添加功能会更快,而且我不喜欢深入研究源代码,但我想我有一个很好的猜测。我认为,开销来自于必须为每一行在轴= 1上执行reduce,因此要进行10,000个单独的调用以进行reduce。在手动添加功能中,在定义“ +”功能的参数时,仅执行一次轴拆分,然后可以将拆分列的每个元素并行添加在一起。