【问题标题】:Why is Cython slower than vectorized NumPy?为什么 Cython 比矢量化 NumPy 慢?
【发布时间】:2014-08-10 02:06:51
【问题描述】:

考虑以下 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 索引显然存在一些开销。

【问题讨论】:

  • 10 次循环:您真的只运行了 10 次性能测试以获得平均值吗?如果是这样,那么正常方差可能会大于您尝试测量的值。改为尝试 100000 次。
  • 这是 Python 2.x 吗?如果是这样,range 可能会造成一些差异
  • @AaronDigulla :我用 100 次运行的时间更新了问题
  • @MrE:我的印象是 Cython 自动将 range 的使用转换为 C 循环。我错了吗?
  • 根据您的硬件和 numpy 版本,一些基本的数学运算可能使用 SSE2 指令,因此使用 double 运行速度比普通版快两倍,或者使用 float 快 4 倍C/Cython 实现。

标签: python arrays performance numpy cython


【解决方案1】:

在我的机器上差异不是很大,但我几乎可以通过像这样更改 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 编译 C 输出时,我使用标志 -O3-march=native。 这似乎表明时序的差异来自于使用不同的编译器优化。

我使用 64 位版本的 MinGW 和 NumPy 1.8.1。 您的结果可能会因您的软件包版本、硬件、平台和编译器而异。

如果您使用的是 IPython notebook 的 Cython 魔法,您可以通过将 %%cython 替换为 %%cython -f -c=-O3 -c=-march=native 来强制使用其他编译器标志进行更新

如果您为 cython 模块使用标准 setup.py,则可以在创建传递给 distutils.setup 的扩展对象时指定 extra_compile_args 参数。

注意:我在指定 NumPy 数组的类型时删除了 ndim=1 标志,因为它不是必需的。 无论如何,该值默认为 1。

【讨论】:

  • 我正在使用 setup.py 文件,因为我不知道 IPython 的魔法,顺便说一句,这很不错! IIRC distutils 在编译扩展时默认为 -O2,也许这就是这里发生的事情。我会在星期一调查它!
【解决方案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 编译。有没有办法将您的答案用于二维?
  • @ThomasAhle docs.cython.org/en/latest/src/userguide/…,我认为double[:, ::1](或double[::1, :])应该没问题。
  • 这使我的代码改进了 2 倍。你能把我介绍给我可以了解发生了什么的地方吗?
【解决方案3】:

另一种选择是使用原始指针(以及避免重复@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])

【讨论】:

  • 好主意,我会用这个函数的时间更新问题!
  • 查看我的更新。显然,原始指针似乎与矢量化 NumPy 版本相当。我将对此进行进一步调查,如果没有更好的选择,我会接受您的回答。
  • 其实我没有,所以我接受你的回答,谢谢提醒!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-09
  • 1970-01-01
  • 1970-01-01
  • 2017-12-17
  • 2020-04-14
相关资源
最近更新 更多