周围有numexpr、numba和cython,这个答案的目标是考虑这些可能性。
但首先让我们声明一个显而易见的事实:无论你如何将一个Python函数映射到numpy-array上,它始终是一个Python函数,这意味着对于每一个求值:
numpy-array元素必须转换为python对象(例如Float)。
所有计算都是用python对象完成的,这意味着有解释器、动态分派和不可变对象的开销。
因此,由于上面提到的开销,实际使用哪种机制来遍历数组并没有发挥很大的作用——它比使用numpy的内置功能慢得多。
让我们来看看下面的例子:
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np。Vectorize被选为纯python函数类方法的代表。使用perfplot(参见答案附录中的代码),我们得到以下运行时间:
我们可以看到,numpy-方法比纯python版本快10 -100倍。较大数组的性能下降可能是因为数据不再适合缓存。
值得一提的是,向量化也使用大量内存,因此内存使用通常是瓶颈(参见相关so -问题)。还要注意,numpy关于np的文档。Vectorize声明它“主要是为了方便,而不是为了性能”。
如果需要性能,除了从头开始编写c扩展,还可以使用其他工具:
人们经常听说,它的性能非常好,因为它的底层是纯C语言。然而,还有很大的改进空间!
向量化numpy版本使用了大量额外的内存和内存访问。Numexp-library尝试平铺numpy-arrays,从而获得更好的缓存利用率:
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
导致以下比较:
我不能在上面的图中解释所有的事情:我们可以看到numexpr-library在开始时的开销更大,但因为它更好地利用缓存,对于更大的数组,它的速度大约快10倍!
另一种方法是对函数进行jit编译,从而得到一个真正的纯c UFunc。这是numba的方法:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
它比原来的numpy方法快10倍:
然而,令人尴尬的是,这个任务是可并行的,因此我们也可以使用prange来并行计算循环:
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
正如预期的那样,并行函数对于较小的输入较慢,但对于较大的输入较快(几乎是2倍):
numba专门用于优化numpy-arrays的操作,而Cython是一个更通用的工具。提取与numba相同的性能更加复杂——通常是llvm (numba) vs本地编译器(gcc/MSVC):
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
Cython会导致一些较慢的函数:
结论
显然,只测试一个函数并不能证明任何东西。还应该记住,对于所选的函数(例如,内存带宽对于大于10^5个元素的大小是瓶颈),因此我们在这个区域中对numba、numexpr和cython具有相同的性能。
最后,最终的答案取决于函数类型、硬件、python分布和其他因素。例如,Anaconda-distribution使用Intel的VML来处理numpy的函数,因此对于先验函数,如exp, sin, cos和类似的函数,它的性能很容易超过numba(除非它使用SVML,参见这个SO-post) -参见下面的SO-post。
然而,从这次调查和到目前为止我的经验来看,我想说,只要不涉及先验函数,numba似乎是最简单、性能最好的工具。
使用perfplot-package绘制运行时间:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)