为什么Cython比向量化的NumPy慢?

12

考虑以下Cython代码:

cimport cython
cimport numpy as np
import numpy as np

@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]

@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]

def test_numpyvec(a, b):
    a += b

def gendata(nb=40000000):
    a = np.random.random(nb)
    b = np.random.random(nb)
    return a, b

在解释器中运行它几次(以便缓存预热)会产生以下结果:


In [14]: %timeit -n 100 test_memoryview(a, b)
100 loops, best of 3: 148 ms per loop

In [15]: %timeit -n 100 test_numpy(a, b)
100 loops, best of 3: 159 ms per loop

In [16]: %timeit -n 100 test_numpyvec(a, b)
100 loops, best of 3: 124 ms per loop

# See answer below :
In [17]: %timeit -n 100 test_raw_pointers(a, b)
100 loops, best of 3: 129 ms per loop

我尝试使用不同的数据集大小,结果发现向量化的NumPy函数比编译后的Cython代码更快,而我本来期望Cython在性能方面与向量化的NumPy相当。

我是否忘记了在Cython代码中进行优化?NumPy是否使用了某些东西(如BLAS),以使这样的简单操作运行得更快?我能否改进此代码的性能?

更新:原始指针版本似乎与NumPy相当。因此,使用内存视图或NumPy索引存在一些开销。


2
10个循环:你真的只运行了10次性能测试来获取平均值吗?如果是这样,那么正常的方差可能比你尝试测量的要大。建议改为尝试100000次。 - Aaron Digulla
@AaronDigulla:我已经更新了问题,包括100次运行的时间。 - F.X.
2
@MrE:我原本以为Cython会自动将range的使用转换成C循环,难道我错了吗? - F.X.
@F.X. 谢谢 - 我不知道! - YXD
4
根据您的硬件和numpy版本,一些基本数学操作可能会使用SSE2指令,因此使用double时运行速度可能比纯C/Cython实现快两倍,使用float时可能快四倍。请注意,具体的加速效果取决于您的硬件和numpy版本。 - Jaime
显示剩余6条评论
3个回答

10

另一个选项是使用原始指针(以及全局指令来避免重复的@cython...):

#cython: wraparound=False
#cython: boundscheck=False
#cython: nonecheck=False

#...

cdef ctest_raw_pointers(int n, double *a, double *b):
    cdef int i
    for i in range(n):
        a[i] += b[i]

def test_raw_pointers(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
    ctest_raw_pointers(a.shape[0], &a[0], &b[0])

好主意,我会用这个函数的时间更新问题! - F.X.
1
看到我的更新。显然,原始指针似乎与矢量化的NumPy版本相当。我将进一步调查这个问题,如果没有更好的选择,我会接受你的答案。 - F.X.
1
我其实没有想到,所以我会接受你的答案,谢谢提醒! - F.X.

3
在我的电脑上,差异不是很大,但我可以通过修改numpy和内存视图函数来几乎消除它,如下所示。
@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
    cdef int i, n=a.shape[0]
    for i in range(n):
        a[i] += b[i]

@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double] a, np.ndarray[double] b):
    cdef int i, n=a.shape[0]
    for i in range(n):
        a[i] += b[i]

然后,当我从Cython编译输出时,我使用标志-O3-march=native。这似乎表明时间差异来自于使用不同的编译器优化。
我使用64位版本的MinGW和NumPy 1.8.1。您的结果可能会因软件包版本、硬件、平台和编译器而有所不同。
如果您正在使用IPython笔记本的Cython魔术功能,您可以通过将%%cython替换为%%cython -f -c=-O3 -c=-march=native来强制更新附加编译器标志。
如果您在为Cython模块使用标准的setup.py,则可以在创建传递给distutils.setup的Extension对象时指定extra_compile_args参数。
注意:我在指定NumPy数组类型时删除了ndim=1标志,因为它是不必要的。该值默认为1。

我正在使用setup.py文件,因为我不知道IPython魔法,它非常好用!如果我没记错,distutils在编译扩展时默认使用-O2,也许这就是发生的事情。我会在周一调查一下! - F.X.

2

稍微提高速度的一种方法是指定步幅:

def test_memoryview_inorder(double[::1] a, double[::1] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]

我有一个二维数组,尝试指定 double[::1, ::1] b,但被告知“无法指定既是 C 连续又是 Fortran 连续的数组。” 只写 double[:, ::1] b 可以编译。是否有办法在两个维度上使用你的答案? - Thomas Ahle
@ThomasAhle https://docs.cython.org/en/latest/src/userguide/memoryviews.html#view-general-layouts,我认为 double[:, ::1](或 double[::1, :])应该没问题。 - Veedrac
这使我的代码性能提高了2倍。你能给我推荐一些学习相关知识的地方吗? - Thomas Ahle

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