【问题标题】:Why is Numpy outperforming this cython routine by a factor 3为什么 Numpy 的性能比这个 cython 程序高出 3 倍
【发布时间】:2014-10-07 08:44:55
【问题描述】:

我刚刚开始尝试使用 cython,作为第一个练习,我创建了以下(重新)实现的函数,用于计算数组中每个元素的 sin。所以这是我的 sin.pyx

from numpy cimport ndarray, float64_t
import numpy as np

cdef extern from "math.h":
    double sin(double x)

def sin_array(ndarray[float64_t, ndim=1] arr):
    cdef int n = len(arr)
    cdef ndarray h = np.zeros(n, dtype=np.float64)
    for i in range(n):
        h[i] = sin(arr[i])
    return h

我还为此创建了以下 setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

import numpy

ext = Extension("sin", sources=["sin.pyx"])

setup(ext_modules=[ext],
      cmdclass={"build_ext": build_ext},
      include_dirs=[numpy.get_include()])

所以这会创建我的 *.so 文件。我将它导入 python 并创建 1000 个随机数,例如

import sin
import numpy as np

x = np.random.randn(1000)

%timeit sin.sin_array(x)
%timeit np.sin(x)

Numpy 以 3 倍的优势获胜。这是为什么呢?我认为对输入数组的类型和维度做出非常明确的假设的函数在这里可能更具竞争力。当然,我也知道 numpy 非常聪明,但我很可能在这里做了一些愚蠢的事情......

请注意,本练习的重点不是重写更快的 sin 函数,而是为我们的一些内部工具创建一些 cython 包装器,但这是稍后的另一个问题......

【问题讨论】:

  • 您的 Cython 代码中还没有边界检查等吗?在documentation example 中,他们会在某个时候将其关闭。
  • 是的,我现在已经包含了。只有非常小的改进。仍然几乎是 3 倍...
  • numpy.zeros 是一种糟糕的内存分配方式。至少使用numpy.empty
  • @tschm NumPy 的正弦函数可能是矢量化的,其中整个数组只调用一个函数,而在 Cython 例程中,每个数组元素都需要一个函数调用......

标签: python numpy cython


【解决方案1】:

Cython 的注释功能,cython -a filename.pyx 是你的朋友。它会生成一个 html 文件,您可以在浏览器中加载该文件,并突出显示没有得到很好优化的代码行。可以点击一行查看生成的c代码。

在这种情况下,问题似乎是h 没有正确输入。如果您只是将数组键入为ndarray,您是在告诉 Cython 它是一个数组,但您没有为 cython 提供足够的信息来告诉它如何有效地对其进行索引,您必须给出类型和形状信息。您已在函数声明中正确执行此操作。

我想一旦这个问题得到解决,性能将是可比的,但如果不是,annotate 会告诉你哪里出了问题。如果 cython 仍然较慢,那么 numpy 可能使用比标准 c 更快的 sin 函数(您可以获得更快的 sin 近似值,如果有兴趣可以尝试谷歌搜索)。

【讨论】:

  • 谢谢,这很有用。我现在只比 numpy 慢 20%。
【解决方案2】:

以下是使用 ipython 中的 cython 魔法在我的机器上的几个变体和性能(可能会有所不同):

%%cython --compile-args=-O3 -a

import numpy as np
cimport numpy as np
import cython

from libc.math cimport sin

def sin_array(np.ndarray[np.float64_t, ndim=1] arr):
    cdef int n = len(arr)
    cdef np.ndarray h = np.zeros(n, dtype=np.float64)
    for i in range(n):
        h[i] = sin(arr[i])
    return h

@cython.boundscheck(False)
@cython.wraparound(False)
def sin_array1(np.ndarray[np.float64_t, ndim=1] arr):
    cdef int n = arr.shape[0]
    cdef unsigned int i
    cdef np.ndarray[np.float64_t, ndim=1] h = np.empty_like(arr)
    for i in range(n):
        h[i] = sin(arr[i])
    return h


@cython.boundscheck(False)
@cython.wraparound(False)
def sin_array2(np.float64_t[:] arr):
    cdef int n = arr.shape[0]
    cdef unsigned int i
    cdef np.ndarray[np.float64_t, ndim=1] h = np.empty(n, np.float64)
    cdef np.float64_t[::1] _h = h
    for i in range(n):
        _h[i] = sin(arr[i])
    return h

为了踢球,我使用了 Numba jitted 方法:

import numpy as np
import numba as nb

@nb.jit
def sin_numba(x):
    n = x.shape[0]
    h = np.empty(n, np.float64)
    for k in range(n):
        h[k] = np.sin(x[k])

    return h

还有时间:

In [25]:

x = np.random.randn(1000)

%timeit np.sin(x)
%timeit sin_array(x)
%timeit sin_array1(x)
%timeit sin_array2(x)
%timeit sin_numba(x)
10000 loops, best of 3: 27 µs per loop
10000 loops, best of 3: 80.3 µs per loop
10000 loops, best of 3: 28.7 µs per loop
10000 loops, best of 3: 32.8 µs per loop
10000 loops, best of 3: 31.4 µs per loop

numpy 内置仍然是最快的(但只是一点点),考虑到不指定任何类型信息的简单性,numba 性能相当不错。

更新

看看各种数组大小总是好的。以下是包含 10000 个元素的数组的时序:

In [26]:

x = np.random.randn(10000)

%timeit np.sin(x)
%timeit sin_array(x)
%timeit sin_array1(x)
%timeit sin_array2(x)
%timeit sin_numba(x)
1000 loops, best of 3: 267 µs per loop
1000 loops, best of 3: 783 µs per loop
1000 loops, best of 3: 267 µs per loop
1000 loops, best of 3: 268 µs per loop
1 loops, best of 3: 287 µs per loop

在这里,您可以看到原始方法的优化版本和 np.sin 调用之间几乎相同的时序,这表明在 cython 中初始化数据结构或返回时存在一些开销。在这些条件下,Numba 的表现略差。

【讨论】:

  • cdef np.float64_t[::1] _h = h 背后的想法是什么?
  • 这是一个 cython 类型的内存视图,[::1] 告诉 cython 内存是 c 连续的(在您创建 h 时保证,但不一定用于输入 arr)。该变体使用一种技巧,允许您将内存创建为 ndarray,然后通过类型化的 memoryview 对其进行操作,然后将数据作为 ndarray 返回(而不是最后在 memoryview 上调用np.asarray,这更慢)。
  • 我还要补充一点,我没有严格测试所有优化对时序的影响,但我肯定会检查使用-O2-O3 编译是否有效果,当然正如@Bhante 所提到的,提供有关h 的cython 信息可能是关键。我赞同-a 标志的建议,以查看带注释的源代码。
  • 是的,我的实验证实关于 h 的信息对于 cython 绝对至关重要,正如 Bhante 提到的并且在您的实验中所做的@JoshAdel
【解决方案3】:

我想我会使用 Python 3.6.1 和 Cython 0.25.2 来更新它。正如@blake-walsh 所建议的那样,我正确输入了所有变量,并使用 -a 选项检查代码是否已翻译为 C,而无需额外测试。我还使用了较新的类型化 memoryview 方法将数组传递给函数。

结果是 Cython 将 Python 编译为 C 并将 C 库用于数学函数比 Numpy 解决方案快 45%。为什么?可能是因为 Numpy 有许多我没有添加到 Cython 版本的检查和概括。我最近做了一些 Cython vs C 测试,如果你可以使用可以翻译成 C 的代码,那么差异并不显着。 Cython 真的很快。

代码是:

%%cython -c=-O3 -c=-march=native
import cython
cimport cython
from libc.math cimport sin

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.cdivision(True)
cpdef double [:] cy_sin(double [:] arr):
    cdef unsigned int i, n = arr.shape[0]
    for i in range(n):
        arr[i] = sin(arr[i])
    return arr

import numpy as np
x = np.random.randn(1000)
%timeit np.sin(x)
%timeit cy_sin(x)

结果是:

15.6 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
10.7 µs ± 58.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

编辑: 我通过将代码更改为:

%%cython --compile-args=-fopenmp --link-args=-fopenmp --force -c=-O3 -c=-march=native
import cython
cimport cython
cimport openmp
from cython.parallel import parallel, prange
from libc.math cimport sin

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.cdivision(True)
cpdef double [:] cy_sin(double [:] arr):
    cdef int i, n = arr.shape[0]
    for i in prange(n, nogil=True):
#     for i in range(n):
        arr[i] = sin(arr[i])
    return arr

在这个小型阵列上,它大约将速度提高了一倍(i5-3470 3.2GHz x4 处理器)以在5.75 µs 中完成。在更大的 1M+ 大小的阵列上,它的速度翻了两番。

【讨论】:

    猜你喜欢
    • 2012-10-14
    • 2012-04-05
    • 2018-11-12
    • 2020-04-16
    • 1970-01-01
    • 1970-01-01
    • 2014-08-10
    • 1970-01-01
    • 2018-01-16
    相关资源
    最近更新 更多