周围有numexpr、numba和cython,这个答案的目标是考虑到这些可能性。
但首先让我们明确一点:无论您如何将 Python 函数映射到 numpy 数组,它仍然是一个 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-approach 比纯 python 版本快 10 到 100 倍。更大数组大小的性能下降可能是因为数据不再适合缓存。
还值得一提的是,vectorize 也使用了大量内存,因此内存使用通常是瓶颈(参见相关的SO-question)。另请注意,numpy 在np.vectorize 上的文档指出它“主要是为了方便,而不是为了性能”。
应该使用其他工具,当需要性能时,除了从头开始编写 C 扩展外,还有以下可能性:
人们经常听到,numpy 的性能是最好的,因为它在引擎盖下是纯 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-approach 快 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) 与本地编译器 (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)'
)