为什么Numpy的表现超过了这个cython例程3倍

时间:2014-10-07 08:44:55

标签: python numpy cython

我刚刚开始尝试使用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包装器,但这是以后的另一个问题......

3 个答案:

答案 0 :(得分:7)

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

在这种情况下,问题似乎是h未正确输入。如果你只是输入一个数组ndarray,你就告诉Cython它是一个数组,但是你没有给cython足够的信息告诉它如何有效地索引它,你< em>必须提供类型和形状信息。您已在函数声明中正确完成此操作。

我想,一旦修复后,性能将具有可比性,但如果它没有注释会告诉你错误。如果cython仍然较慢,那么numpy可能使用比标准c更快的sin函数(你可以获得更快的sin近似值,如果感兴趣的话可以使用谷歌搜索它。)

答案 1 :(得分:6)

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

%%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的情况稍差。

答案 2 :(得分:0)

我以为我会使用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

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