Python 以numpy数组为参数的Cython内联函数
考虑这样的代码:Python 以numpy数组为参数的Cython内联函数,python,performance,numpy,inline,cython,Python,Performance,Numpy,Inline,Cython,考虑这样的代码: import numpy as np cimport numpy as np cdef inline inc(np.ndarray[np.int32_t] arr, int i): arr[i]+= 1 def test1(np.ndarray[np.int32_t] arr): cdef int i for i in xrange(len(arr)): inc(arr, i) def test2(np.ndarray[np.int
import numpy as np
cimport numpy as np
cdef inline inc(np.ndarray[np.int32_t] arr, int i):
arr[i]+= 1
def test1(np.ndarray[np.int32_t] arr):
cdef int i
for i in xrange(len(arr)):
inc(arr, i)
def test2(np.ndarray[np.int32_t] arr):
cdef int i
for i in xrange(len(arr)):
arr[i] += 1
我使用ipython测量test1和test2的速度:
In [7]: timeit ttt.test1(arr)
100 loops, best of 3: 6.13 ms per loop
In [8]: timeit ttt.test2(arr)
100000 loops, best of 3: 9.79 us per loop
有没有办法优化test1?为什么cython不按要求内联这个函数
更新:
实际上我需要的是这样的多维代码:
# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np
cdef inline inc(np.ndarray[np.int32_t, ndim=2] arr, int i, int j):
arr[i, j] += 1
def test1(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc(arr, i, j)
def test2(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
arr[i,j] += 1
# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np
cdef inline inc(np.ndarray[np.float32_t, ndim=2] arr, int i, int j):
arr[i, j]+= 1
def test1(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc(arr, i, j)
def test2(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
arr[i,j] += 1
cdef class FastPassingFloat2DArray(object):
cdef float* data
cdef int stride0, stride1
def __init__(self, np.ndarray[np.float32_t, ndim=2] arr):
self.data = <float*>arr.data
self.stride0 = arr.strides[0]/arr.dtype.itemsize
self.stride1 = arr.strides[1]/arr.dtype.itemsize
def __getitem__(self, tuple tp):
cdef int i, j
cdef float *pr, r
i, j = tp
pr = (self.data + self.stride0*i + self.stride1*j)
r = pr[0]
return r
def __setitem__(self, tuple tp, float value):
cdef int i, j
cdef float *pr, r
i, j = tp
pr = (self.data + self.stride0*i + self.stride1*j)
pr[0] = value
cdef inline inc2(FastPassingFloat2DArray arr, int i, int j):
arr[i, j]+= 1
def test3(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
cdef FastPassingFloat2DArray tmparr = FastPassingFloat2DArray(arr)
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc2(tmparr, i,j)
时间安排:
In [7]: timeit ttt.test1(arr)
1 loops, best of 3: 647 ms per loop
In [8]: timeit ttt.test2(arr)
100 loops, best of 3: 2.07 ms per loop
显式内联提供了300倍的加速。而且我的实际函数相当大,因此内联使代码的可维护性变得更差
更新2:
# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np
cdef inline inc(np.ndarray[np.int32_t, ndim=2] arr, int i, int j):
arr[i, j] += 1
def test1(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc(arr, i, j)
def test2(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
arr[i,j] += 1
# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np
cdef inline inc(np.ndarray[np.float32_t, ndim=2] arr, int i, int j):
arr[i, j]+= 1
def test1(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc(arr, i, j)
def test2(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
arr[i,j] += 1
cdef class FastPassingFloat2DArray(object):
cdef float* data
cdef int stride0, stride1
def __init__(self, np.ndarray[np.float32_t, ndim=2] arr):
self.data = <float*>arr.data
self.stride0 = arr.strides[0]/arr.dtype.itemsize
self.stride1 = arr.strides[1]/arr.dtype.itemsize
def __getitem__(self, tuple tp):
cdef int i, j
cdef float *pr, r
i, j = tp
pr = (self.data + self.stride0*i + self.stride1*j)
r = pr[0]
return r
def __setitem__(self, tuple tp, float value):
cdef int i, j
cdef float *pr, r
i, j = tp
pr = (self.data + self.stride0*i + self.stride1*j)
pr[0] = value
cdef inline inc2(FastPassingFloat2DArray arr, int i, int j):
arr[i, j]+= 1
def test3(np.ndarray[np.float32_t, ndim=2] arr):
cdef int i,j
cdef FastPassingFloat2DArray tmparr = FastPassingFloat2DArray(arr)
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc2(tmparr, i,j)
您正在将数组作为类型为
numpy.ndarray
的Python对象传递给inc()
。由于引用计数等问题,传递Python对象的代价很高,而且似乎会阻止内联。如果以C方式传递数组,即作为指针,test1()
比我的机器上的test2()
更快:
cimport numpy as np
cdef inline inc(int* arr, int i):
arr[i] += 1
def test1(np.ndarray[np.int32_t] arr):
cdef int i
for i in xrange(len(arr)):
inc(<int*>arr.data, i)
cimport numpy作为np
cdef内联公司(int*arr,int i):
arr[i]+=1
def测试1(np.ndarray[np.int32_t]arr):
cdef int i
对于x范围内的i(len(arr)):
公司(arr.data,i)
问题在于分配numpy数组(或者,等效地,将其作为函数参数传入)不仅仅是一个简单的分配,而是一个“缓冲区提取”,它填充结构并将跨距和指针信息提取到快速索引所需的局部变量中。如果迭代的元素数量适中,那么O(1)开销很容易在循环中分摊,但对于小函数来说,情况肯定不是这样
改善这一点是许多人的愿望,但这不是一个微不足道的变化。例如,请参见上的讨论,自该问题发布以来已经过去了三年多,同时也取得了很大的进展。关于此代码(问题的更新2): 我得到以下时间安排:
arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test1(arr)
%timeit test2(arr)
1 loops, best of 3: 354 ms per loop
1000 loops, best of 3: 1.02 ms per loop
因此,即使在超过3年之后,这个问题还是可以重现的。Cython现在有了,因为它是在Cython 0.16中引入的,所以在发布问题时不可用。为此:
# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np
cdef inline inc(int[:, ::1] tmv, int i, int j):
tmv[i, j]+= 1
def test3(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
cdef int[:, ::1] tmv = arr
for i in xrange(tmv.shape[0]):
for j in xrange(tmv.shape[1]):
inc(tmv, i, j)
def test4(np.ndarray[np.int32_t, ndim=2] arr):
cdef int i,j
cdef int[:, ::1] tmv = arr
for i in xrange(tmv.shape[0]):
for j in xrange(tmv.shape[1]):
tmv[i,j] += 1
有了这个,我得到了:
arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test3(arr)
%timeit test4(arr)
1000 loops, best of 3: 977 µs per loop
1000 loops, best of 3: 838 µs per loop
我们快到了,而且已经比老式的方式快了!现在,可以声明inc()
函数了,让我们这样声明吧!但糟糕的是:
Error compiling Cython file:
[...]
cdef inline inc(int[:, ::1] tmv, int i, int j) nogil:
^
[...]
Function with Python return type cannot be declared nogil
啊,我完全没有注意到void
返回类型丢失了!再次但现在使用void
:
cdef inline void inc(int[:, ::1] tmv, int i, int j) nogil:
tmv[i, j]+= 1
最后我得到:
%timeit test3(arr)
%timeit test4(arr)
1000 loops, best of 3: 843 µs per loop
1000 loops, best of 3: 853 µs per loop
和手动内联一样快
现在,为了好玩,我尝试了以下代码:
import numpy as np
from numba import autojit, jit
@autojit
def inc(arr, i, j):
arr[i, j] += 1
@autojit
def test5(arr):
for i in xrange(arr.shape[0]):
for j in xrange(arr.shape[1]):
inc(arr, i, j)
我得到:
arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test5(arr)
100 loops, best of 3: 4.03 ms per loop
尽管它比Cython慢4.7倍,很可能是因为JIT编译器未能内联inc()
,但我认为它非常棒我所需要做的就是添加@autojit
,而不必用笨拙的类型声明来搞乱代码;88倍的加速比几乎为零
我用麻木试过其他东西,比如
@jit('void(i4[:],i4,i4)')
def inc(arr, i, j):
arr[i, j] += 1
或nopython=True
,但未能进一步改进
,我们只需要提交更多请求,使其具有更高的优先级。;) 好的,那么2d和3d数组呢?@Maxim:您自己的代码只适用于一维数组,所以我只为这种情况提供了一个更快的版本。(请注意,
ndim=1
是隐式的,如果您没有为ndarray
提供显式的ndim
参数,那么当我将ndim=2
添加到您的代码和带有50x50数组的时间test1()
和test2()
时,在机器上,它们之间几乎没有任何性能差异。请参阅问题中的更新。在这里,我在ndim=2上也得到了巨大的性能差异(这是意料之中的,因为如果inc不是内联的,它会在每次调用时获取并释放numpy缓冲区)。在nD情况下,只传递指针是不够的,因为您还需要知道每个维度的大小,传递它们会使函数看起来很糟糕,并使每个数组访问变得复杂……在我的机器上,两个维度的性能差异大约为5%(而不是30000%)。您使用的Python和Cython版本是什么?哪个C编译器?Windows、Python 2.6、Cython 0.14、Gcc 4.5.1。你能发布你的2d代码吗?现在我看到了区别:我刚刚在你的代码的第一个版本中添加了ndim=2
(因为我认为这是你真正想要的)。如果inc(),它接受数组和一些索引,并使用它们进行一些计算。是否有可能创建一些cython对象,它将保存指向数据的指针并跨越信息,并提供类似nD数组的[]接口,而不会造成巨大的性能损失(理想情况下,它将内联到类似(data+istrides[0]+jstrides[1]+…)的代码)?是的,这是我的问题。我现在就接受你的回答。