使用 Cython 实现 Numba 的性能
Achieving Numba's performance with Cython
通常我在使用 Cython 时能够达到 Numba 的性能。然而,在这个例子中我没有这样做——Numba 比我的 Cython 版本快大约 4 倍。
这里是 Cython 版本:
%%cython -c=-march=native -c=-O3
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[dtype=double] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
这是 Numba 版本:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
经过测试,Cython 版本与 numpy 的 where
相当,但明显不如 Numba:
#Python3.6 + Cython 0.28.3 + gcc-7.2
import numpy
np.random.seed(0)
n = 10000000
data = np.random.random(n)
assert (cy_where(data)==nb_where(data)).all()
assert (np.where(data>0.5,2*data, data)==nb_where(data)).all()
%timeit cy_where(data) # 179ms
%timeit nb_where(data) # 49ms (!!)
%timeit np.where(data>0.5,2*data, data) # 278 ms
Numba性能的原因是什么,使用Cython时如何匹配?
正如@max9111 所建议的那样,通过使用连续的内存视图来消除步幅,这并没有提高多少性能:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where_cont(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[dtype=double] output = np.empty(n, dtype=np.float64)
cdef double[::1] view = output # view as continuous!
for i in range(n):
if df[i]>0.5:
view[i] = 2.0*df[i]
else:
view[i] = df[i]
return output
%timeit cy_where_cont(data) # 165 ms
这似乎完全是由 LLVM 能够进行的优化驱动的。如果我用 clang 编译 cython 示例,则两个示例之间的性能是相同的。就其价值而言,windows 上的 MSVC 显示出与 numba 类似的性能差异。
$ CC=clang ipython
<... setup code>
In [7]: %timeit cy_where(data) # 179ms
...: %timeit nb_where(data) # 49ms (!!)
30.8 ms ± 309 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
30.2 ms ± 498 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
有趣的是,使用 clang 作为后端,使用 pythran 编译原始 Numpy 代码,产生与 Numba 版本相同的性能。
import numpy as np
#pythran export work(float64[])
def work(df):
return np.where(data>0.5,2*data, data)
编译为
CXX=clang++ CC=clang pythran pythran_work.py -O3 -march=native
和基准 session:
import numpy as np
np.random.seed(0)
n = 10000000
data = np.random.random(n)
import numba_work, pythran_work
%timeit numba_work.work(data)
12.7 ms ± 20 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit pythran_work.work(data)
12.7 ms ± 32.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
通常我在使用 Cython 时能够达到 Numba 的性能。然而,在这个例子中我没有这样做——Numba 比我的 Cython 版本快大约 4 倍。
这里是 Cython 版本:
%%cython -c=-march=native -c=-O3
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[dtype=double] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
这是 Numba 版本:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
经过测试,Cython 版本与 numpy 的 where
相当,但明显不如 Numba:
#Python3.6 + Cython 0.28.3 + gcc-7.2
import numpy
np.random.seed(0)
n = 10000000
data = np.random.random(n)
assert (cy_where(data)==nb_where(data)).all()
assert (np.where(data>0.5,2*data, data)==nb_where(data)).all()
%timeit cy_where(data) # 179ms
%timeit nb_where(data) # 49ms (!!)
%timeit np.where(data>0.5,2*data, data) # 278 ms
Numba性能的原因是什么,使用Cython时如何匹配?
正如@max9111 所建议的那样,通过使用连续的内存视图来消除步幅,这并没有提高多少性能:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where_cont(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[dtype=double] output = np.empty(n, dtype=np.float64)
cdef double[::1] view = output # view as continuous!
for i in range(n):
if df[i]>0.5:
view[i] = 2.0*df[i]
else:
view[i] = df[i]
return output
%timeit cy_where_cont(data) # 165 ms
这似乎完全是由 LLVM 能够进行的优化驱动的。如果我用 clang 编译 cython 示例,则两个示例之间的性能是相同的。就其价值而言,windows 上的 MSVC 显示出与 numba 类似的性能差异。
$ CC=clang ipython
<... setup code>
In [7]: %timeit cy_where(data) # 179ms
...: %timeit nb_where(data) # 49ms (!!)
30.8 ms ± 309 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
30.2 ms ± 498 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
有趣的是,使用 clang 作为后端,使用 pythran 编译原始 Numpy 代码,产生与 Numba 版本相同的性能。
import numpy as np
#pythran export work(float64[])
def work(df):
return np.where(data>0.5,2*data, data)
编译为
CXX=clang++ CC=clang pythran pythran_work.py -O3 -march=native
和基准 session:
import numpy as np
np.random.seed(0)
n = 10000000
data = np.random.random(n)
import numba_work, pythran_work
%timeit numba_work.work(data)
12.7 ms ± 20 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit pythran_work.work(data)
12.7 ms ± 32.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)