为什么这个简单附加函数的 Cython 实现比 Numba 慢?
Why is this Cython implementation of this simple appending function slower than Numba?
我有一个函数的 2 个版本,可以将一行附加到二维数组;一个在 Cython 中,另一个在 Numba 中。
Cython 版本的性能比 Numba 版本慢很多。我想优化 Cython 版本,使其性能至少与 Numba 版本一样好。
我正在用这个 timer.py
模块对代码进行计时:
导入时间
class Timer(object):
def __init__(self, name='', output=print):
self._name = name
self._output = output
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, a, b, c):
self.end = time.time()
self.time_taken = self.end - self.start
self._output('%s Took %0.2fs seconds' % (self._name, self.time_taken))
我的 append_2d_cython.pyx
模块是:
#!python
#cython: boundscheck=False
#cython: wraparound=False
import numpy as np
cimport numpy as cnp
cnp.import_array() # needed to initialize numpy-API
cpdef empty_2d(int d1, int d2):
cdef:
cnp.npy_intp[2] dimdim
dimdim[0] = d1
dimdim[1] = d2
return cnp.PyArray_SimpleNew(2, dimdim, cnp.NPY_INT32)
cpdef append_2d(int[:, :] arr, int[:] value):
cdef int[:, :] result
result = empty_2d(arr.shape[0]+1, arr.shape[1])
result[:arr.shape[0], :] = arr
result[arr.shape[0], :] = value
return result
我的 append_2d_numba.py
模块是:
import numba as nb
import numpy as np
@nb.jit(nopython=True)
def append_2d(arr, value):
result = np.empty((arr.shape[0]+1, arr.shape[1]), dtype=arr.dtype)
result[:-1] = arr
result[-1] = value
return result
我正在将 append_2d
的 Numba 和 Cython 版本与此脚本进行比较:
import pyximport
import numpy as np
pyximport.install(setup_args={'include_dirs': np.get_include()})
from timer import Timer
from append_2d_cython import append_2d as append_2d_cython
from append_2d_numba import append_2d as append_2d_numba
arr_2d = np.random.randint(0, 100, size=(5, 4), dtype=np.int32)
arr_1d = np.array([0, 1, 2, 3], np.int32)
num_tests = 100000
with Timer('append_2d_cython'):
for _ in range(num_tests):
r_cython = append_2d_cython(arr_2d, arr_1d)
# # JIT Compile it
append_2d_numba(arr_2d, arr_1d)
with Timer('append_2d_numba'):
for _ in range(num_tests):
r_numba = append_2d_numba(arr_2d, arr_1d)
打印:
make many with cython Took 0.36s seconds
make many with numba Took 0.12s seconds
因此,对于这段代码,numba 比 Cython 快 3 倍。我想将 Cython 代码重构为至少与 Numba 代码一样快。我该怎么做?
这项调查将表明,Cython 的大开销是 Cython 性能不佳的原因。此外,将提出一个(有点古怪的)替代方案来避免其中的大部分 - 因此 numba 解决方案将被因素 4 击败。
让我们先在我的机器上建立基线(我调用你的函数 cy_append_2d
和 nb_append_2d
并使用 %timeit
魔法来测量 运行 次) :
arr_2d = np.arange(5*4, dtype=np.int32).reshape((5,4))
arr_1d = np.array([0, 1, 2, 3], np.int32)
%timeit cy_append_2d(arr_2d, arr_1d)
# 8.27 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_append_2d(arr_2d, arr_1d)
# 2.84 µs ± 169 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Numba 版本大约快三倍 - 与您观察到的时间相似。
但是,我们必须知道,我们衡量的不是复制数据所需的时间,而是开销。它不像 numba 在做一些花哨的事情——它只是碰巧有更少的开销(但仍然很多——创建一个 numpy 数组和复制 24 个整数几乎需要 3 微秒!)
如果我们增加复制的数据量,我们会发现 cython 和 numba 的性能非常相似 - 没有花哨的编译器可以大大改善复制:
N=5000
arr_2d_large = np.arange(5*N, dtype=np.int32).reshape((5,N))
arr_1d_large = np.arange(N, dtype=np.int32)
%timeit cy_append_2d(arr_2d_large, arr_1d_large)
# 35.7 µs ± 597 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit nb_append_2d(arr_2d_large, arr_1d_large)
# 44.8 µs ± 1.36 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
此处 Cython 稍快一些,但对于不同的机器和不同的大小,这可能会有所不同,出于我们的目的,我们可以认为它们几乎同样快。
正如@DavidW 所指出的,从 numpy 数组中的 cython-ndarray 创建 cython-arrays 会带来相当大的开销。考虑这个虚拟函数:
%%cython
cpdef dummy(int[:, :] arr, int[:] value):
pass
%timeit dummy(arr_2d, arr_1d)
# 3.24 µs ± 47.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
这意味着在函数中的第一个操作开始之前,原来的 8µs 中有 3 个已经用完了 - 在这里您可以看到创建内存视图的成本。
通常情况下,人们不会关心这种开销 - 因为如果您为如此小的数据块调用 numpy 功能,无论如何性能都不会很好。
然而,如果你真的喜欢这种微优化,你可以直接使用 Numpy 的 C-API,而无需使用 Cythons ndarray
-helper。我们不能指望结果会像复制 24 个整数一样快 - 因为创建一个新的 buffer/numpy-array 只是代价高昂,但是我们击败 8µs 的机会非常高!
这是一个原型,展示了可能的可能性:
%%cython
from libc.string cimport memcpy
# don't use Cythons wrapper, because it uses ndarray
# define only the needed stuff
cdef extern from "numpy/arrayobject.h":
ctypedef int npy_intp # it isn't actually int, but cython doesn't care anyway
int _import_array() except -1
char *PyArray_BYTES(object arr)
npy_intp PyArray_DIM(object arr, int n)
object PyArray_SimpleNew(int nd, npy_intp* dims, int typenum)
cdef enum NPY_TYPES:
NPY_INT32
# initialize Numpy's C-API when imported.
_import_array()
def cy_fast_append_2d(upper, lower):
# create resulting array:
cdef npy_intp dims[2]
dims[0] = PyArray_DIM(upper, 0)+1
dims[1] = PyArray_DIM(upper, 1)
cdef object res = PyArray_SimpleNew(2, &dims[0], NPY_INT32)
# copy upper array, assume C-order/memory layout
cdef char *dest = PyArray_BYTES(res)
cdef char *source = PyArray_BYTES(upper)
cdef int size = (dims[0]-1)*dims[1]*4 # int32=4 bytes
memcpy(dest, source, size)
# copy lower array, assume C-order/memory layout
dest += size
source = PyArray_BYTES(lower)
size = dims[1]*4
memcpy(dest, source, size)
return res
现在的时间是:
%timeit cy_fast_append_2d(arr_2d, arr_1d)
753 ns ± 3.13 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
这意味着 Cython 以 4 倍的优势击败 Numba。
但是,有很多安全损失 - 例如,它仅适用于 C 顺序数组而不适用于 Fortran 顺序数组。但我的目标不是提供防水解决方案,而是研究直接使用 Numpy 的 C-API 可能变得多快 - 是否应该采用这种 hacky 方式由您决定。
我有一个函数的 2 个版本,可以将一行附加到二维数组;一个在 Cython 中,另一个在 Numba 中。
Cython 版本的性能比 Numba 版本慢很多。我想优化 Cython 版本,使其性能至少与 Numba 版本一样好。
我正在用这个 timer.py
模块对代码进行计时:
导入时间
class Timer(object):
def __init__(self, name='', output=print):
self._name = name
self._output = output
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, a, b, c):
self.end = time.time()
self.time_taken = self.end - self.start
self._output('%s Took %0.2fs seconds' % (self._name, self.time_taken))
我的 append_2d_cython.pyx
模块是:
#!python
#cython: boundscheck=False
#cython: wraparound=False
import numpy as np
cimport numpy as cnp
cnp.import_array() # needed to initialize numpy-API
cpdef empty_2d(int d1, int d2):
cdef:
cnp.npy_intp[2] dimdim
dimdim[0] = d1
dimdim[1] = d2
return cnp.PyArray_SimpleNew(2, dimdim, cnp.NPY_INT32)
cpdef append_2d(int[:, :] arr, int[:] value):
cdef int[:, :] result
result = empty_2d(arr.shape[0]+1, arr.shape[1])
result[:arr.shape[0], :] = arr
result[arr.shape[0], :] = value
return result
我的 append_2d_numba.py
模块是:
import numba as nb
import numpy as np
@nb.jit(nopython=True)
def append_2d(arr, value):
result = np.empty((arr.shape[0]+1, arr.shape[1]), dtype=arr.dtype)
result[:-1] = arr
result[-1] = value
return result
我正在将 append_2d
的 Numba 和 Cython 版本与此脚本进行比较:
import pyximport
import numpy as np
pyximport.install(setup_args={'include_dirs': np.get_include()})
from timer import Timer
from append_2d_cython import append_2d as append_2d_cython
from append_2d_numba import append_2d as append_2d_numba
arr_2d = np.random.randint(0, 100, size=(5, 4), dtype=np.int32)
arr_1d = np.array([0, 1, 2, 3], np.int32)
num_tests = 100000
with Timer('append_2d_cython'):
for _ in range(num_tests):
r_cython = append_2d_cython(arr_2d, arr_1d)
# # JIT Compile it
append_2d_numba(arr_2d, arr_1d)
with Timer('append_2d_numba'):
for _ in range(num_tests):
r_numba = append_2d_numba(arr_2d, arr_1d)
打印:
make many with cython Took 0.36s seconds
make many with numba Took 0.12s seconds
因此,对于这段代码,numba 比 Cython 快 3 倍。我想将 Cython 代码重构为至少与 Numba 代码一样快。我该怎么做?
这项调查将表明,Cython 的大开销是 Cython 性能不佳的原因。此外,将提出一个(有点古怪的)替代方案来避免其中的大部分 - 因此 numba 解决方案将被因素 4 击败。
让我们先在我的机器上建立基线(我调用你的函数 cy_append_2d
和 nb_append_2d
并使用 %timeit
魔法来测量 运行 次) :
arr_2d = np.arange(5*4, dtype=np.int32).reshape((5,4))
arr_1d = np.array([0, 1, 2, 3], np.int32)
%timeit cy_append_2d(arr_2d, arr_1d)
# 8.27 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_append_2d(arr_2d, arr_1d)
# 2.84 µs ± 169 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Numba 版本大约快三倍 - 与您观察到的时间相似。
但是,我们必须知道,我们衡量的不是复制数据所需的时间,而是开销。它不像 numba 在做一些花哨的事情——它只是碰巧有更少的开销(但仍然很多——创建一个 numpy 数组和复制 24 个整数几乎需要 3 微秒!)
如果我们增加复制的数据量,我们会发现 cython 和 numba 的性能非常相似 - 没有花哨的编译器可以大大改善复制:
N=5000
arr_2d_large = np.arange(5*N, dtype=np.int32).reshape((5,N))
arr_1d_large = np.arange(N, dtype=np.int32)
%timeit cy_append_2d(arr_2d_large, arr_1d_large)
# 35.7 µs ± 597 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit nb_append_2d(arr_2d_large, arr_1d_large)
# 44.8 µs ± 1.36 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
此处 Cython 稍快一些,但对于不同的机器和不同的大小,这可能会有所不同,出于我们的目的,我们可以认为它们几乎同样快。
正如@DavidW 所指出的,从 numpy 数组中的 cython-ndarray 创建 cython-arrays 会带来相当大的开销。考虑这个虚拟函数:
%%cython
cpdef dummy(int[:, :] arr, int[:] value):
pass
%timeit dummy(arr_2d, arr_1d)
# 3.24 µs ± 47.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
这意味着在函数中的第一个操作开始之前,原来的 8µs 中有 3 个已经用完了 - 在这里您可以看到创建内存视图的成本。
通常情况下,人们不会关心这种开销 - 因为如果您为如此小的数据块调用 numpy 功能,无论如何性能都不会很好。
然而,如果你真的喜欢这种微优化,你可以直接使用 Numpy 的 C-API,而无需使用 Cythons ndarray
-helper。我们不能指望结果会像复制 24 个整数一样快 - 因为创建一个新的 buffer/numpy-array 只是代价高昂,但是我们击败 8µs 的机会非常高!
这是一个原型,展示了可能的可能性:
%%cython
from libc.string cimport memcpy
# don't use Cythons wrapper, because it uses ndarray
# define only the needed stuff
cdef extern from "numpy/arrayobject.h":
ctypedef int npy_intp # it isn't actually int, but cython doesn't care anyway
int _import_array() except -1
char *PyArray_BYTES(object arr)
npy_intp PyArray_DIM(object arr, int n)
object PyArray_SimpleNew(int nd, npy_intp* dims, int typenum)
cdef enum NPY_TYPES:
NPY_INT32
# initialize Numpy's C-API when imported.
_import_array()
def cy_fast_append_2d(upper, lower):
# create resulting array:
cdef npy_intp dims[2]
dims[0] = PyArray_DIM(upper, 0)+1
dims[1] = PyArray_DIM(upper, 1)
cdef object res = PyArray_SimpleNew(2, &dims[0], NPY_INT32)
# copy upper array, assume C-order/memory layout
cdef char *dest = PyArray_BYTES(res)
cdef char *source = PyArray_BYTES(upper)
cdef int size = (dims[0]-1)*dims[1]*4 # int32=4 bytes
memcpy(dest, source, size)
# copy lower array, assume C-order/memory layout
dest += size
source = PyArray_BYTES(lower)
size = dims[1]*4
memcpy(dest, source, size)
return res
现在的时间是:
%timeit cy_fast_append_2d(arr_2d, arr_1d)
753 ns ± 3.13 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
这意味着 Cython 以 4 倍的优势击败 Numba。
但是,有很多安全损失 - 例如,它仅适用于 C 顺序数组而不适用于 Fortran 顺序数组。但我的目标不是提供防水解决方案,而是研究直接使用 Numpy 的 C-API 可能变得多快 - 是否应该采用这种 hacky 方式由您决定。