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]+…)的代码)?是的,这是我的问题。我现在就接受你的回答。