为啥在迭代 NumPy 数组时 Cython 比 Numba 慢得多?

Posted

技术标签:

【中文标题】为啥在迭代 NumPy 数组时 Cython 比 Numba 慢得多?【英文标题】:Why is Cython so much slower than Numba when iterating over NumPy arrays?为什么在迭代 NumPy 数组时 Cython 比 Numba 慢得多? 【发布时间】:2019-04-09 18:43:36 【问题描述】:

在遍历 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 倍。 作为一个 Cython 初学者,我想我错过了一些东西。

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

%timeit np.square(arr)

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

【问题讨论】:

你为什么不在 cython 代码中也做 arr[i]**2 ?我认为一个可能的原因是pow(arr[i],2) 会将2 视为浮点数并使计算更加复杂 谢谢,但我也尝试过使用 arr[i]**2 而不是 pow(arr[i],2) ,两种解决方案的性能几乎相同。一般来说,即使在没有数学转换的情况下对 numpy 数组进行简单迭代,numba 编译函数的运行速度也比 cython 快。 【参考方案1】:

正如@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 与 MSVC)。您可能希望使用 clang 来匹配 numba 性能(例如,请参见 SO-answer)

为了使编译器更容易优化,你应该将输入声明为连续数组,即double[::1] arr(参见this question为什么它对向量化很重要),使用@cython.boundscheck(False)(使用选项-a看到没有更少的黄色)并添加编译器标志(即-O3-march=native 或类似的,具体取决于您的编译器以启用矢量化,注意默认使用的构建标志,这可能会抑制一些优化,例如-fwrapv)。最后,您可能想用 C 编写工作马循环,使用正确的标志/编译器组合进行编译,然后使用 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 的np.square()

不同方法在我的机器上的表现是:

numba+vectorization+empty     3µs
np.square                     4µs
numba+vectorization           7µs
numba missed vectorization   11µs
cython+mult                  14µs
cython+pow                  356µs

【讨论】:

非常感谢您的洞察力!通过您的优化,我的 cython 函数的运行速度几乎与 numba 一样快。我 与问题不完全相关,但少了一件小事。在开始时分配数组的不必要归零占用了总运行时间的 30% 以上,并且至少在 Numba 中没有被编译器优化掉。 @ead 这只是一个出于好奇的问题。但是不久前,我在 cython 中遇到了与 pow 类似的问题。如果您不对 Numba 中的指数进行硬编码并且存在 SVML,它会在 256 位向量上调用 SVML 的 pow 函数,大约需要 150µs。 Cython 中是否有不使用 icc 的简单替代方案? @max9111,我必须承认我从未尝试过。我可能宁愿用 C 编写代码并将功能包装在 Cython 中,也不愿尝试直接从 Cython 访问“intristics”

以上是关于为啥在迭代 NumPy 数组时 Cython 比 Numba 慢得多?的主要内容,如果未能解决你的问题,请参考以下文章

Cython:从参考获得时,Numpy 数组缺少两个第一个元素

将 3D numpy 数组从 cython 传递到 C++

为啥 Numpy 和 Pandas 数组比源数据消耗更多内存? [关闭]

将带有字符串的结构化 numpy 数组传递给 cython 函数

Cython 中 numpy 数组掩码的性能

如何正确地将 numpy 数组传递给 Cython 函数?