2014-10-07 22 views
2

我刚刚开始尝试使用cython,并且作为第一个练习,我创建了一个计算数组中每个元素的sin函数的下列(重新)实现。因此,这里是我的sin.pyx为什么Numpy的性能超过了这个例程的3

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封皮为我们的一些内部工具,但这是以后的另一个问题...

+1

在您的Cython代码中是否还存在边界检查?在[文档示例](http://docs.cython.org/src/userguide/numpy_tutorial.html#numpy-tutorial)中,他们在某个时候关闭了它。 – Evert 2014-10-07 08:52:49

+0

是的,我现在已经包括了。只有很小的改进。仍然几乎因素3 ... – tschm 2014-10-07 09:03:45

+1

http://stackoverflow.com/questions/14466950/why-is-my-python-numpy-example-faster-than-pure-c-implementation – tschm 2014-10-07 09:17:16

回答

6

这里有几个变种,并在我的机器上的表现(这可能会发生变化)使用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实时编译的方法抛出:

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的票价稍差。

+0

cdef背后的想法np.float64_t [:: 1] _h = h? – tschm 2014-10-07 13:56:29

+2

这是一个cython类型的memoryview,'[:: 1]'告诉cython内存是c-contiguous(当你创建'h'时保证,但不一定是输入'arr')。该变体使用了一种技巧,可以让你创建内存为ndarray,然后通过类型化的内存视图对内存进行操作,然后以ndarray的形式返回数据(而不是在内存视图的末尾调用'np.asarray',比较慢)。 – JoshAdel 2014-10-07 15:12:42

+1

我还会补充一点,我没有严格测试所有优化对时序的影响,但我肯定会检查用'-O2'或'-O3'编译是否有效果, @Bhante给出有关'h'的cython信息可能是关键。我第二次推荐'-a'标志来查看注释的源代码。 – JoshAdel 2014-10-07 15:16:22

7

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

在这种情况下,问题似乎是h未正确键入。如果你只是输入一个数组为ndarray,你告诉Cython它是一个数组,但是你没有给cython足够的信息来告诉它如何有效地索引它,你必须必须给出类型和形状信息。你已经在函数声明中正确地做了这个。

我想,一旦这个问题得到解决,表现将是可比较的,但如果它不是注释会告诉你什么是错的。如果cython仍然比较慢,那么numpy可能使用比标准c更快的sin函数(你可以得到更快的sin近似值,如果感兴趣的话可以尝试用google搜索)。

+0

谢谢,这是非常有用的。我现在只比numpy慢20%。 – tschm 2014-10-07 11:27:45

0

我想我会更新这个使用Python 3.6.1和Cython 0.25.2。正如@ blake-walsh 所示,我正确输入了所有变量,并使用-a选项检查代码是否已转换为C而没有额外的测试。我还使用了更新类型的memoryview方法将数组传递给函数。

结果是,将Python编译为C并将C库用于数学函数的Cython比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 +大小的阵列上,速度提高了四倍。

相关问题