numpy ufuncs速度vs循环速度

god*_*ygo 13 python performance for-loop numpy numpy-ufunc

我已经阅读了很多"避免与numpy循环".所以,我试过了.我正在使用此代码(简化版).一些辅助数据:

 In[1]: import numpy as np
        resolution = 1000                             # this parameter varies
        tim = np.linspace(-np.pi, np.pi, resolution) 
        prec = np.arange(1, resolution + 1)
        prec = 2 * prec - 1
        values = np.zeros_like(tim)
Run Code Online (Sandbox Code Playgroud)

我的第一个实现是for循环:

 In[2]: for i, ti in enumerate(tim):
           values[i] = np.sum(np.sin(prec * ti))
Run Code Online (Sandbox Code Playgroud)

然后,我摆脱了显式for循环,并实现了这一点:

 In[3]: values = np.sum(np.sin(tim[:, np.newaxis] * prec), axis=1)
Run Code Online (Sandbox Code Playgroud)

对于小型阵列来说,这个解决方案更快,但是当我扩大规模时,我有这样的时间依赖: 在此输入图像描述

我缺少什么或是正常行为?如果不是,在哪里挖?

编辑:根据评论,这里有一些额外的信息.用IPython中的测量的时间%timeit%%timeit,在新内核进行每一次运行.我的笔记本电脑是acer aspire v7-482pg(i7,8GB).我正在使用:

  • python 3.5.2
  • numpy 1.11.2 + mkl
  • Windows 10

MSe*_*ert 9

这是正常和预期的行为.它太简单了,无法应用"避免循环与numpy"语句.如果你正在处理内部循环,它(几乎)总是如此.但是在外环(例如你的情况下)的情况下,有更多例外.特别是如果替代方案是使用广播,因为这会通过使用更多的内存来加速您的操作.

只是为了"避免for numpy循环"语句添加一些背景知识:

NumPy数组存储为具有类型的连续数组.Python int与C不同int!因此,无论何时迭代数组中的每个项目,您都需要从数组中插入项目,将其转换为Python int然后执行任何您想要的操作,最后您可能需要再次将其转换为ac整数(称为装箱)并取消装箱值).例如,您希望sum使用Python在数组中的项目:

import numpy as np
arr = np.arange(1000)
%%timeit
acc = 0
for item in arr:
    acc += item
# 1000 loops, best of 3: 478 µs per loop
Run Code Online (Sandbox Code Playgroud)

你最好使用numpy:

%timeit np.sum(arr)
# 10000 loops, best of 3: 24.2 µs per loop
Run Code Online (Sandbox Code Playgroud)

即使你将循环推入Python C代码,你仍然远离numpy性能:

%timeit sum(arr)
# 1000 loops, best of 3: 387 µs per loop
Run Code Online (Sandbox Code Playgroud)

此规则可能有例外,但这些将非常稀疏.至少只要有一些等效的numpy功能.因此,如果您要迭代单个元素,那么您应该使用numpy.


有时一个普通的python循环就足够了.它没有广泛宣传,但与Python功能相比,numpy函数的开销很大.例如,考虑一个3元素数组:

arr = np.arange(3)
%timeit np.sum(arr)
%timeit sum(arr)
Run Code Online (Sandbox Code Playgroud)

哪一个会更快?

解决方案:Python函数比numpy解决方案表现更好:

# 10000 loops, best of 3: 21.9 µs per loop  <- numpy
# 100000 loops, best of 3: 6.27 µs per loop <- python
Run Code Online (Sandbox Code Playgroud)

但是这与你的例子有什么关系呢?事实并非如此,因为你总是在数组上使用numpy-functions(不是单个元素,甚至不是很少的元素)所以你的内部循环已经使用了优化的函数.这就是为什么两者表现大致相同(+/-因子10,元素很少,因子2,大约500元素).但这不是真正的循环开销,而是函数调用开销!

循环解决方案

使用line-profiler和a resolution = 100:

def fun_func(tim, prec, values):
    for i, ti in enumerate(tim):
        values[i] = np.sum(np.sin(prec * ti))
%lprun -f fun_func fun_func(tim, prec, values)
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2       101          752      7.4      5.7      for i, ti in enumerate(tim):
     3       100        12449    124.5     94.3          values[i] = np.sum(np.sin(prec * ti))
Run Code Online (Sandbox Code Playgroud)

95%是在循环中花费的,我甚至将循环体分成几个部分来验证这一点:

def fun_func(tim, prec, values):
    for i, ti in enumerate(tim):
        x = prec * ti
        x = np.sin(x)
        x = np.sum(x)
        values[i] = x
%lprun -f fun_func fun_func(tim, prec, values)
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2       101          609      6.0      3.5      for i, ti in enumerate(tim):
     3       100         4521     45.2     26.3          x = prec * ti
     4       100         4646     46.5     27.0          x = np.sin(x)
     5       100         6731     67.3     39.1          x = np.sum(x)
     6       100          714      7.1      4.1          values[i] = x
Run Code Online (Sandbox Code Playgroud)

时间消费者np.multiply,np.sin,np.sum在这里,你可以很容易地通过每个呼叫与他们的开销比较它们的时候检查:

arr = np.ones(1, float)
%timeit np.sum(arr)
# 10000 loops, best of 3: 22.6 µs per loop
Run Code Online (Sandbox Code Playgroud)

因此,一旦与计算运行时相比,计算函数调用开销很小,您就会有类似的运行时.即使有100个项目,你也非常接近开销时间.诀窍是知道他们在哪个方面实现收支平衡.有1000个项目,呼叫开销仍然很重要:

%lprun -f fun_func fun_func(tim, prec, values)
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2      1001         5864      5.9      2.4      for i, ti in enumerate(tim):
     3      1000        42817     42.8     17.2          x = prec * ti
     4      1000       119327    119.3     48.0          x = np.sin(x)
     5      1000        73313     73.3     29.5          x = np.sum(x)
     6      1000         7287      7.3      2.9          values[i] = x
Run Code Online (Sandbox Code Playgroud)

但是与resolution = 5000运行时相比,开销很低:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2      5001        29412      5.9      0.9      for i, ti in enumerate(tim):
     3      5000       388827     77.8     11.6          x = prec * ti
     4      5000      2442460    488.5     73.2          x = np.sin(x)
     5      5000       441337     88.3     13.2          x = np.sum(x)
     6      5000        36187      7.2      1.1          values[i] = x
Run Code Online (Sandbox Code Playgroud)

当你在每次np.sin通话中花费500us时,你不再关心20us的开销了.

需要注意的是:line_profiler每行可能包含一些额外的开销,也可能是每个函数调用,因此函数调用开销变得可以忽略的点可能更低!

您的广播解决方案

我首先分析了第一个解决方案,让我们对第二个解决方案做同样的事情:

def fun_func(tim, prec, values):
    x = tim[:, np.newaxis]
    x = x * prec
    x = np.sin(x)
    x = np.sum(x, axis=1)
    return x
Run Code Online (Sandbox Code Playgroud)

再次使用line_profiler resolution=100:

%lprun -f fun_func fun_func(tim, prec, values)
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2         1           27     27.0      0.5      x = tim[:, np.newaxis]
     3         1          638    638.0     12.9      x = x * prec
     4         1         3963   3963.0     79.9      x = np.sin(x)
     5         1          326    326.0      6.6      x = np.sum(x, axis=1)
     6         1            4      4.0      0.1      return x
Run Code Online (Sandbox Code Playgroud)

这已经显着超过了开销时间,因此与循环相比,我们最终加快了10倍.

我也进行了分析resolution=1000:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2         1           28     28.0      0.0      x = tim[:, np.newaxis]
     3         1        17716  17716.0     14.6      x = x * prec
     4         1        91174  91174.0     75.3      x = np.sin(x)
     5         1        12140  12140.0     10.0      x = np.sum(x, axis=1)
     6         1           10     10.0      0.0      return x
Run Code Online (Sandbox Code Playgroud)

并与precision=5000:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def fun_func(tim, prec, values):
     2         1           34     34.0      0.0      x = tim[:, np.newaxis]
     3         1       333685 333685.0     11.1      x = x * prec
     4         1      2391812 2391812.0    79.6      x = np.sin(x)
     5         1       280832 280832.0      9.3      x = np.sum(x, axis=1)
     6         1           14     14.0      0.0      return x
Run Code Online (Sandbox Code Playgroud)

1000大小仍然更快,但正如我们已经看到的那样,呼叫开销在循环解决方案中仍然是不可忽略的.但是因为resolution = 5000每个步骤花费的时间几乎相同(有些慢一点,有些更快但总体上非常相似)

另一个影响是,当你进行乘法时实际的广播变得很重要.即使有非常聪明的numpy解决方案,这仍然包括一些额外的计算.因为resolution=10000你看到广播乘法开始占用与循环解决方案相关的更多"%时间":

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def broadcast_solution(tim, prec, values):
     2         1           37     37.0      0.0      x = tim[:, np.newaxis]
     3         1      1783345 1783345.0    13.9      x = x * prec
     4         1      9879333 9879333.0    77.1      x = np.sin(x)
     5         1      1153789 1153789.0     9.0      x = np.sum(x, axis=1)
     6         1           11     11.0      0.0      return x


Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     8                                           def loop_solution(tim, prec, values):
     9     10001        62502      6.2      0.5      for i, ti in enumerate(tim):
    10     10000      1287698    128.8     10.5          x = prec * ti
    11     10000      9758633    975.9     79.7          x = np.sin(x)
    12     10000      1058995    105.9      8.6          x = np.sum(x)
    13     10000        75760      7.6      0.6          values[i] = x
Run Code Online (Sandbox Code Playgroud)

但除了实际花费的时间之外还有另一件事:内存消耗.循环解决方案需要O(n)内存,因为您始终处理n元素.然而,广播解决​​方案需要O(n*n)记忆.你可能需要等待一段时间,如果你使用resolution=20000你的循环,但它仍然只需要,8bytes/element * 20000 element ~= 160kB但你需要的广播~3GB.这忽略了常数因素(如临时数组,即中间数组)!假设你走得更远,你的内存会快速耗尽!


是时候再次总结一下这些要点:

  • 如果你对一个numpy数组中的单个项目执行python循环,那么你做错了.
  • 如果循环遍历numpy-array的子​​数组,请确保每个循环中的函数调用开销与函数中花费的时间相比是可忽略的.
  • 如果你广播numpy数组,请确保你的内存不足.

但关于优化的最重要的一点仍然是:

  • 如果代码太慢,只能优化代码!如果它太慢,那么只有在分析代码后才进行优化.

  • 不要盲目地信任简化的语句,再也不要在没有分析的情况下优化.


最后一个想法:

如果在不存在已有的解决方案,则可以使用,轻松实现需要循环或广播的此类函数.

例如,将循环解决方案的内存效率与广播解决方案的速度结合在一起的numba函数resolutions如下所示:

from numba import njit

import math

@njit
def numba_solution(tim, prec, values):
    size = tim.size
    for i in range(size):
        ti = tim[i]
        x = 0
        for j in range(size):
            x += math.sin(prec[j] * ti)
        values[i] = x
Run Code Online (Sandbox Code Playgroud)

正如评论中指出的那样,numexpr也可以非常快速地评估广播的计算,而不需要O(n*n)记忆:

>>> import numexpr
>>> tim_2d = tim[:, np.newaxis]
>>> numexpr.evaluate('sum(sin(tim_2d * prec), axis=1)')
Run Code Online (Sandbox Code Playgroud)