在NumPy数组上进行迭代时,为什么Cython比Numba慢得多?

时间:2018-11-06 11:15:50

标签: python numpy cython numba

在NumPy数组上进行迭代时,Numba似乎比Cython快得多。
我可能会缺少哪些Cython优化?

这是一个简单的例子:

纯Python代码:

import numpy as np

def f(arr):
  res=np.zeros(len(arr))

  for i in range(len(arr)):
     res[i]=(arr[i])**2

  return res

arr=np.random.rand(10000)
%timeit f(arr)

输出:每个循环4.81 ms±72.2 µs(平均±标准偏差,共运行7次,每个循环100个循环)


Cython代码(在Jupyter内):

%load_ext cython
%%cython

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport pow

#@cython.boundscheck(False)
#@cython.wraparound(False)

cpdef f(double[:] arr):
   cdef np.ndarray[dtype=np.double_t, ndim=1] res
   res=np.zeros(len(arr),dtype=np.double)
   cdef double[:] res_view=res
   cdef int i

   for i in range(len(arr)):
      res_view[i]=pow(arr[i],2)

   return res

arr=np.random.rand(10000)
%timeit f(arr)

输出:每个循环445 µs±5.49 µs(平均±标准偏差,共运行7次,每个循环1000次)


Numba代码:

import numpy as np
import numba as nb

@nb.jit(nb.float64[:](nb.float64[:]))
def   f(arr):
   res=np.zeros(len(arr))

   for i in range(len(arr)):
       res[i]=(arr[i])**2

   return res

arr=np.random.rand(10000)
%timeit f(arr)

输出:每循环9.59 µs±98.8 ns(平均±标准偏差,共运行7次,每个循环100000次)


在此示例中,Numba比Cython快50倍。
作为赛顿的初学者,我想我缺少一些东西。

当然,在这种简单的情况下,使用NumPy square向量化函数会更合适:

%timeit np.square(arr)

输出:每个循环5.75 µs±78.9 ns(平均值±标准偏差,共运行7次,每个循环100000次)

1 个答案:

答案 0 :(得分:7)

正如@Antonio所指出的那样,使用pow进行简单的乘法不是很明智,并且会导致相当大的开销:

因此,将pow(arr[i], 2)替换为arr[i]*arr[i]可以大大提高速度:

cython-pow-version        356 µs
numba-version              11 µs
cython-mult-version        14 µs

剩余的差异可能是由于编译器和优化级别之间的差异(在我的案例中为llvm vs MSVC)。您可能想使用clang来匹配numba的性能(例如,请参见此SO-answer

为了使编译器更容易进行优化,应将输入声明为连续数组,即double[::1] arr(请参阅this question,为什么它对矢量化很重要),请使用@cython.boundscheck(False) (使用选项-a可以看到较少的黄色),还可以添加编译器标志(即-O3-march=native或类似标记,具体取决于您的编译器以启用矢量化,请注意构建-默认情况下使用的标志会禁止某些优化,例如-fwrapv)。最后,您可能想用C语言编写Working-horse循环,使用标记/编译器的正确组合进行编译,然后使用Cython对其进行包装。

顺便说一句,通过将函数的参数键入为nb.float64[:](nb.float64[:])会降低numba的性能-不再允许假定输入数组是连续的,从而排除了向量化的可能性。让numba检测类型(或将其定义为连续的类型,即nb.float64[::1](nb.float64[::1]),您将获得更好的性能:

@nb.jit(nopython=True)
def nb_vec_f(arr):
   res=np.zeros(len(arr))

   for i in range(len(arr)):
       res[i]=(arr[i])**2

   return res

可带来以下改进:

%timeit f(arr)  # numba version
# 11.4 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_vec_f(arr)
# 7.03 µs ± 48.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

正如@ max9111指出的那样,我们不必使用零初始化结果数组,但是可以使用np.empty(...)而不是np.zeros(...)-此版本甚至胜过了numpy的{{1} }

我的机器上不同方法的性能如下:

np.square()