如何使Cython比Python(不使用Numpy)更快,以将两个数组相加?

3

我希望使用Cython来对两个数组进行逐元素相加,以减少时间消耗,但不想使用Numpy数组。我发现最快的基本Python方法是使用列表推导式,如下所示:

def add_arrays(a,b):
    return [m + n for m,n in zip(a,b)]

我的Cython方法有点复杂,代码如下:

from array import array
from libc.stdlib cimport malloc
from cython cimport boundscheck,wraparound

@boundscheck(False)
@wraparound(False)
cpdef add_arrays_Cython(int[:] Aarr, int[:] Barr):
    cdef size_t i, I
    I = Aarr.shape[0]
    cdef int *Carr = <int *> malloc(640000 * sizeof(int))
    for i in range(I):
        Carr[i] = Aarr[i]+Barr[i]
    result_as_array  = array('i',[e for e in Carr[:640000]])
    return result_as_array

请注意,我使用@boundscheck(False)@wraparound(False)来使其运行更快。此外,我关注一个非常大的数组(大小为640000),如果我仅使用cdef int Carr[640000],它会崩溃,所以我使用了malloc()解决了这个问题。最后,我将数据结构作为Python整数类型的数组返回。
为了对代码进行分析,我运行了以下命令:
a = array.array('i', range(640000)) #create integer array
b = a[:] #array to add

T=time.clock()
for i in range(20): add_arrays(a,b) #Python list comprehension approach
print(time.clock() - T)

>6.33 seconds

T=time.clock()
for i in range(20): add_arrays_Cython(a,b) #Cython approach
print(time.clock() - T)

> 4.54秒

很明显,基于Cython的方法可以提高约30%的速度。我预计速度提升会更接近一个数量级,甚至更多(就像对Numpy一样)。

我该如何进一步加速Cython代码?我的代码中是否存在明显的瓶颈?我是Cython的初学者,可能有误解。


确保描述清楚何时使用Python的listarray.arraynumpy在很大程度上取代了内置的array包。我不知道cython实现得有多好。为了最大化速度,请考虑使用array的缓冲区接口以及cythontyped memoryview - hpaulj
1个回答

2
最大的瓶颈在于将结果指针转换回数组。
以下是优化版本:
from cython cimport boundscheck,wraparound
from cython cimport view

@boundscheck(False)
@wraparound(False)
cpdef add_arrays_Cython(int[:] Aarr, int[:] Barr):
    cdef size_t i, I
    I = Aarr.shape[0]
    result_as_array = view.array(shape=(I,), itemsize=sizeof(int), format='i')
    cdef int[:] Carr = result_as_array
    for i in range(I):
        Carr[i] = Aarr[i]+Barr[i]
    return result_as_array

需要注意的几点:不要使用malloc分配临时缓冲区,然后将结果复制到数组中,而是创建cython.view.array并将其强制转换为int[:]。这样可以获得指针访问的原始速度,并避免不必要的复制。我还直接返回Cython对象,而不是先将其转换为Python对象。总体上,与您原始的Cython实现相比,这使我提速了70倍。
将view对象转换为列表有点棘手:如果仅将return语句更改为return list(result_as_array),则代码的速度会变慢约10倍,比您的初始实现还要慢。但是,如果添加一个额外的包装层,例如:return list(memoryview(result_as_array)),该函数比您的版本快约5倍。因此,主要的开销是从快速本机对象转换为通用Python对象,如果需要快速代码,则应始终避免这种情况。
为了进行比较,我使用numpy运行了代码。numpy版本的性能与我的Cython版本完全相同。这意味着C编译器能够自动对我的代码内部的逐对求和循环进行矢量化处理。
附注:在malloc()分配的指针上调用free(),否则会泄漏内存。

如果需要返回一个列表,可以使用C-API构建一个列表,这比创建一个中间数组更快。 - ead
1
@Stefan 谢谢你的有益和教育性回答。确实,我实现了大约5倍的加速!我希望能够获得更多,但我想这是一个教训,我应该使用视图对象进行工作。 - CodeWanderer
@ead 感谢您的建议。我尝试了 int[::1],但速度并没有改变太多。我还尝试将其转换为 array.array,使用 array.array('i',memoryview(result_as_array)),但这导致了显著的减速。 - CodeWanderer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接