Python 为什么Numpy的表现要比cython的好3倍

Python 为什么Numpy的表现要比cython的好3倍,python,numpy,cython,Python,Numpy,Cython,我刚刚开始使用cython进行实验,作为第一个练习,我创建了一个函数的以下(重新)实现,该函数计算数组中每个元素的sin。这就是我的罪恶 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(ar

我刚刚开始使用cython进行实验,作为第一个练习,我创建了一个函数的以下(重新)实现,该函数计算数组中每个元素的sin。这就是我的罪恶

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)
努比以3倍的优势获胜。为什么呢?我认为对于输入数组的类型和维数做出非常明确的假设的函数在这里更具竞争力。当然,我也明白numpy是不可思议的聪明,但我很可能在这里做了一些愚蠢的事情


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

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

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


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

以下是一些变体,以及我使用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
对于踢腿,我采用了一种麻木的紧张方式:

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或return中的数据结构初始化有一些开销。在这种情况下,Numba的情况稍差一些

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

结果是Cython将Python编译为C并使用C库实现数学函数,比Numpy解决方案快45%。为什么?可能是因为Numpy有很多检查和概括,我没有添加到Cython版本中。我最近做了很多Cython与C的测试,如果你能使用可以翻译成C的代码,那么差别就不大了。赛顿真的很快

代码是:

%%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+尺寸阵列上,它的速度提高了四倍

你的Cython代码中不是还有boundschecking之类的东西吗?在这个例子中,他们会在某个时候关闭它。是的,我现在已经包括了它。只有很小的改进。仍然几乎是因子3…
numpy.zero
是分配内存的一种可怕的方式。至少使用
numpy.empty
@tschm
numpy
的正弦函数可能是矢量化的,其中对整个数组只进行一个函数调用,而在Cython例程中,每个数组元素都需要一个函数调用…谢谢,这非常有用。我现在只比numpy慢20%。cdef np.float64_t[::1]\u h=h背后的想法是什么?这是一个cython类型的memoryview,
[::1]
告诉cython内存是c连续的(在创建
h
时可以保证,但不一定用于输入
arr
)。该变体使用了一种技巧,允许您将内存创建为一个数据数组,然后通过类型化的memoryview对其进行操作,然后将数据作为一个数据数组返回(而不是在最后的memoryview上调用
np.asarray
,这会更慢)。我还要补充一点,我没有严格测试所有优化对计时的影响,但我肯定会检查使用
-O2
-O3
编译是否有效果,当然,正如@Bhante所提到的,向cython提供有关
h
的信息可能是关键。我支持
-a
标志的建议来查看注释的源代码。是的,我的实验证实了关于h的信息对于cython是绝对重要的,正如Bhante提到的,并且在你的实验@JoshAdel中所做的那样