我有2个版本的函数,可将行附加到2d数组中;一个在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代码一样快。我该怎么办?
答案 0 :(得分:3)
此调查将显示,大量的Cython开销是Cython性能不佳的原因。此外,将提出一种(有点hacky)替代方案来避免大多数情况-因此,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µs!)
如果我们增加复制数据的数量,我们将看到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数组会带来相当大的开销。考虑一下这个虚拟函数:
%%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个整数一样快-因为创建一个新的缓冲区/ numpy数组成本很高,但是我们击败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击败Numba的原因是4。
但是,这会损失很多安全性-例如,它仅适用于C阶数组,不适用于Fortran阶数组。但是,我的目标不是提供防水解决方案,而是研究直接使用Numpy的C-API可能会变得有多快-您的决定是否应该采用这种骇人听闻的方式。