为什么在遍历 NumPy 数组时 Cython 比 Numba 慢很多?
Why is Cython so much slower than Numba when iterating over NumPy arrays?
在 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 次循环)
正如@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 性能(例如,参见 )
为了使编译器更容易优化,您应该将输入声明为连续数组,即double[::1] arr
(参见 why it is important for vectorization), use @cython.boundscheck(False)
(use option -a
to see that there is less yellow) and also add compiler flags (i.e. -O3
, -march=native
or similar depending on your compiler to enable the vectorization, watch out for build-flags used by default which can inhibit some optimization, for example )。最后,您可能想用 C 语言编写 working-horse-loop,使用 flags/compiler 的正确组合进行编译,然后使用 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
在 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 次循环)
正如@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 性能(例如,参见
为了使编译器更容易优化,您应该将输入声明为连续数组,即double[::1] arr
(参见@cython.boundscheck(False)
(use option -a
to see that there is less yellow) and also add compiler flags (i.e. -O3
, -march=native
or similar depending on your compiler to enable the vectorization, watch out for build-flags used by default which can inhibit some optimization, for example
顺便说一句,将函数的参数键入 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