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

bee*_*eep 13 python performance numpy

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

带轴的np.sum:

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

每回路186μs±4.21μs(平均值±标准偏差,7次运行,每次1000次循环)

没有轴的np.sum:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1.sum()
%timeit test(p1)
Run Code Online (Sandbox Code Playgroud)

每回路17.9μs±236 ns(平均值±标准偏差,7次运行,每次10000次循环)

+:

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

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

乘法:

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

每个环路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 
Run Code Online (Sandbox Code Playgroud)

所以我想在某种程度上会有一些开销.

ead*_*ead 8

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

  • 必须考虑舍入误差,因此使用成对求和来减少它.
  • 平铺对于更大的数组非常重要,因为它可以充分利用可用的缓存
  • 为了能够使用现代CPU的SIMD指令/无序执行能力,应该并行计算多行

我已经在这里这里更详细地讨论了上面的主题.

但是,如果只需要添加两个元素,那么所有这些都不是必需的,并且不比天真的总和更好 - 您可以获得相同的结果,但开销更少,速度更快.

对于仅1000个元素,调用numpy功能的开销可能高于实际执行这1000次添加(或者就此而言的乘法,因为在现代CPU上,流水线加法/乘法具有相同的成本) - 您可以看到,对于10 ^ 4运行时间只有2倍左右,这是开销在10 ^ 3中发挥更大作用的明确信号!在这个答案中,更详细地研究了开销和缓存未命中的影响.

让我们看一下profiler-result,看看上面的理论是否成立(我使用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
   ...
Run Code Online (Sandbox Code Playgroud)

使用的开销reduce_loop,pairwise_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
Run Code Online (Sandbox Code Playgroud)

正如所料:Python开销起着重要作用,使用简单DOUBLE_add.


呼叫时开销更少 a.sum()

  • 一次, reduce_loop不是每行都要调用,而是只调用一次,这意味着开销要小得多.
  • 没有创建新的结果数组,不再需要向内存写入1000个双打.

所以可以预期,这a.sum()是更快的(尽管事实上,2000年而不是1000必须加成 - 但正如我们已经看到它主要是关于开销和实际工作 - 增加不负责的大部分运行时间).


通过运行获取数据:

perf record python run.py
perf report
Run Code Online (Sandbox Code Playgroud)

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

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