为啥在这里 numba 比 numpy 快?
Posted
技术标签:
【中文标题】为啥在这里 numba 比 numpy 快?【英文标题】:Why is numba faster than numpy here?为什么在这里 numba 比 numpy 快? 【发布时间】:2014-11-15 01:16:57 【问题描述】:我不明白为什么 numba 在这里击败了 numpy(超过 3 倍)。我在这里进行基准测试时是否犯了一些基本错误?似乎是 numpy 的完美情况,不是吗?请注意,作为检查,我还运行了一个结合 numba 和 numpy 的变体(未显示),正如预期的那样,它与在没有 numba 的情况下运行 numpy 相同。
(顺便说一句,这是Fastest way to numerically process 2d-array: dataframe vs series vs array vs numba 的后续问题)
import numpy as np
from numba import jit
nobs = 10000
def proc_numpy(x,y,z):
x = x*2 - ( y * 55 ) # these 4 lines represent use cases
y = x + y*2 # where the processing time is mostly
z = x + y + 99 # a function of, say, 50 to 200 lines
z = z * ( z - .88 ) # of fairly simple numerical operations
return z
@jit
def proc_numba(xx,yy,zz):
for j in range(nobs): # as pointed out by Llopis, this for loop
x, y = xx[j], yy[j] # is not needed here. it is here by
# accident because in the original benchmarks
x = x*2 - ( y * 55 ) # I was doing data creation inside the function
y = x + y*2 # instead of passing it in as an array
z = x + y + 99 # in any case, this redundant code seems to
z = z * ( z - .88 ) # have something to do with the code running
# faster. without the redundant code, the
zz[j] = z # numba and numpy functions are exactly the same.
return zz
x = np.random.randn(nobs)
y = np.random.randn(nobs)
z = np.zeros(nobs)
res_numpy = proc_numpy(x,y,z)
z = np.zeros(nobs)
res_numba = proc_numba(x,y,z)
结果:
In [356]: np.all( res_numpy == res_numba )
Out[356]: True
In [357]: %timeit proc_numpy(x,y,z)
10000 loops, best of 3: 105 µs per loop
In [358]: %timeit proc_numba(x,y,z)
10000 loops, best of 3: 28.6 µs per loop
我在 2012 macbook air (13.3) 标准 anaconda 发行版上运行此程序。如果相关,我可以提供有关我的设置的更多详细信息。
【问题讨论】:
我不明白为什么在 proc_numba 中执行 for 循环,而在 proc_numpy 中却没有 @JohnE 你也应该使用 Numexpr(你必须把它写成一个类似字符串的表达式),但应该更接近 numba perf - 它避免了临时性 @Llopis 实际上,这只是我最初编写基准的方式的残余。但问题仍然存在,如何(相当愚蠢地)编写它,就像我用额外的步骤所做的那样,实际上最终会导致超过 3 倍的加速?除非我真的从根本上错过了什么(很可能)。 @JohnE 您可以通过执行以下操作来优化 numpy 代码:np.add(x,y, out=z) 以避免临时代码(这样做并不好,但应该提高性能)跨度> @Jeff 好的,我之前没有明确使用过 numexpr,但我会尝试弄清楚并稍后添加。很高兴了解 np.add(),但从实际的角度来看,我不确定为什么我不会在这里使用 numba,如果它可以让我写得更简单。 【参考方案1】:我将在此处添加更多内容以回应 Jeff、Jaime、Veedrac,而不是进一步混淆原始问题:
def proc_numpy2(x,y,z):
np.subtract( np.multiply(x,2), np.multiply(y,55),out=x)
np.add( x, np.multiply(y,2),out=y)
np.add(x,np.add(y,99),out=z)
np.multiply(z,np.subtract(z,.88),out=z)
return z
def proc_numpy3(x,y,z):
x *= 2
x -= y*55
y *= 2
y += x
z = x + y
z += 99
z *= (z-.88)
return z
我的机器今天的运行速度似乎比昨天快一点,所以这里它们与 proc_numpy 相比(proc_numba 的时间与以前相同)
In [611]: %timeit proc_numpy(x,y,z)
10000 loops, best of 3: 103 µs per loop
In [612]: %timeit proc_numpy2(x,y,z)
10000 loops, best of 3: 92.5 µs per loop
In [613]: %timeit proc_numpy3(x,y,z)
10000 loops, best of 3: 85.1 µs per loop
请注意,在我编写 proc_numpy2/3 时,我开始看到一些副作用,因此我制作了 x、y、z 的副本并传递了副本,而不是重新使用 x、y、z。此外,不同的函数有时在精度上有细微的差别,所以其中一些没有通过相等性测试,但如果你对它们进行比较,它们真的很接近。我认为这是由于创建或(不创建)临时变量。例如:
In [458]: (res_numpy2 - res_numba)[:12]
Out[458]:
array([ -7.27595761e-12, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, -7.27595761e-12, 0.00000000e+00])
此外,它非常小(大约 10 µs),但使用浮点字面量(55. 而不是 55)也会为 numpy 节省一点时间,但对 numba 没有帮助。
【讨论】:
您必须使用 out 参数(第 3 个)才能使其生效 不要使用函数,x = x*2 - ( y * 55 )
应该写成x *= 2; x -= y*55
,与其他行类似。这样可以避免大多数临时性的视觉噪音更少。
@Veedrac 好的,在上面添加了。差别不大,但比我预期的要大。【参考方案2】:
我认为这个问题突出了(某种程度上)从高级语言调用预编译函数的局限性。假设您在 C++ 中编写如下内容:
for (int i = 0; i != N; ++i) a[i] = b[i] + c[i] + 2 * d[i];
编译器在编译时看到所有这些,整个表达式。它可以在这里做很多非常智能的事情,包括优化临时变量(和循环展开)。
然而,在 python 中,考虑正在发生的事情:当你使用 numpy 时,每个 ''+'' 都在 np 数组类型上使用运算符重载(它们只是围绕连续内存块的薄包装,即低级意义上的数组),并调用一个 fortran(或 C++)函数,它可以超快速地进行添加。但它只是做了一个添加,并吐出一个临时的。
我们可以看到,在某种程度上,虽然 numpy 非常棒、方便且非常快,但它正在减慢速度,因为虽然它似乎正在调用一种快速编译的语言来进行艰苦的工作,但编译器并没有得到要查看整个程序,它只是提供了一些孤立的小部分。这对编译器来说是非常不利的,尤其是现代编译器,它们非常智能,当代码编写得很好时,每个周期可以退出多条指令。
另一方面,Numba 使用了 jit。因此,在运行时,它可以找出不需要的临时对象,并将它们优化掉。基本上,Numba 有机会将程序作为一个整体编译,numpy 只能调用本身已经预编译的小原子块。
【讨论】:
我认为关键是 Python 倾向于将一些任务委托给更快的语言,但 Julia 将所有内容编译在一起,因此快速部分和慢速部分之间没有区别,因此用户不会像 OP 一样感到困惑在这里。 numba jit 编译器无法智能地找出如何避免临时变量或使用任何类型的全程序优化。不同之处在于,在循环中,通过将所有内容编码为标量操作,明确指示编译器不要制作任何临时文件。在 Julia 中也是如此,如果以“普通”矢量化形式编写它,则会获得临时性,从而获得类似 numpy 的速度。明确避免写成循环或使用点广播临时文件。如果编译器真的是聪明的,一个人可以得到循环展开并在此之上进行simd。 在一般情况下,几乎没有语言可以优化掉中间数组分配,除非它可以证明每个子表达式都是纯的,这是非常困难的。此外,在某些情况下,中间数组确实会加快速度,例如,如果出于某种原因您要对一个子结果进行排序。 @NirFriedman 这已经够远了。我只想指出,“分配”是指“临时分配”(堆分配,因为它们是缓慢的原因),而且,正如您可以自己检查的那样,numba 加速是由于重新-将计算编写为显式循环,而不是由于编译器的聪明才智。也就是说,你答案的最后一段是错误的。 @NirFriedman - 你的上帝螺栓代码不正确;您在add
函数中缺少 return output;
。它甚至警告过你!这样,您可以看到add3
分配了两个向量,即使它内联add
。除此之外,我希望您能体会到临时数组与临时标量的巨大成本差异;学究式的头发分裂没有帮助。【参考方案3】:
Numba 通常比 Numpy 甚至 Cython 更快(至少在 Linux 上)。
这是一个情节(从Numba vs. Cython: Take 2窃取):
在此基准测试中,已计算成对距离,因此这可能取决于算法。
请注意,这在其他平台上可能会有所不同,请参阅 Winpython(来自 WinPython Cython tutorial):
【讨论】:
【参考方案4】:当你要求 numpy 做时:
x = x*2 - ( y * 55 )
它在内部被翻译成类似的东西:
tmp1 = y * 55
tmp2 = x * 2
tmp3 = tmp2 - tmp1
x = tmp3
这些临时变量中的每一个都是必须分配、操作然后释放的数组。另一方面,Numba 一次处理一个项目,而不必处理这些开销。
【讨论】:
嗯...所以基本上我的 for 循环具有关闭 numpy 的意想不到的好处,从而避免了临时数组? 完全正确...如果不是 JIT 编译器,Python 循环和函数调用的开销通常比额外的数组分配慢几个数量级。但是如果你直接用 C 写东西,你永远不会做 numpy 在内部做的事情! 感谢 Jaime 和其他所有人提供的见解。你的答案和 Nir 的答案很相似,我认为 Nir 可以比你更多地使用代表点,所以我会给他支票。 ;-)以上是关于为啥在这里 numba 比 numpy 快?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 np.hypot 和 np.subtract.outer 与香草广播相比非常快?使用 Numba 并行加速 numpy 进行距离矩阵计算