numpy比numba和cython快,如何改善numba代码

时间:2019-07-07 08:59:46

标签: python performance numpy cython numba

我这里有一个简单的示例,可以帮助我了解使用numba和cython的情况。我是numba和cython的新手。我已尽力将所有技巧结合在一起,以使numba更快,并在某种程度上与cython相同,但我的numpy代码几乎比numba快2倍(对于float64),如果使用float32,则快2倍以上。不确定我在这里缺少什么。

我当时在想问题可能不再是编码,而是更多关于编译器的问题,而我对此不太熟悉。

我走过很多关于numpy,numba和cython的stackoverflow帖子,没有找到直接的答案。

numpy版本:

def py_expsum(x):
    return np.sum( np.exp(x) )

numba版本:

@numba.jit( nopython=True)    
def nb_expsum(x):
    nx, ny = x.shape
    val = 0.0
    for ix in range(nx):
        for iy in range(ny):
            val += np.exp(x[ix, iy])
    return val

Cython版本:

import numpy as np
import cython
from libc.math cimport exp

@cython.boundscheck(False) 
@cython.wraparound(False)
cpdef double cy_expsum2 ( double[:,:] x, int nx, int ny ):
    cdef: 
        double val = 0.0
        int ix, iy    
    for ix in range(nx):
        for iy in range(ny):
            val += exp(x[ix, iy])
    return val

播放大小为2000 x 1000的数组,并循环播放100次以上。对于numba,首次激活它的次数不会计入循环。

使用python 3(anaconda分发版),窗口10

               float64       /   float32
    1. numpy : 0.56 sec      /   0.23 sec
    2. numba : 0.93 sec      /   0.74 sec      
    3. cython: 0.83 sec
cython与numba接近。所以对我来说,最大的问题是,numba为什么不能击败numpy的运行时?我在这里做错了什么或想念什么?其他因素如何起作用?如何找到?

3 个答案:

答案 0 :(得分:5)

我们将看到,行为取决于所使用的numpy-distribution。

此答案将重点放在采用英特尔VML(矢量数学库)的Anacoda发行版上,在使用其他硬件和numpy版本的情况下,铣削可能会有所不同。

还将显示如何在不使用Anacoda-distribution的情况下通过Cython或numexpr利用VML的功能。Anacoda-distribution将VML插入后台进行一些小巧的操作。


对于以下尺寸,我可以复制您的结果

N,M=2*10**4, 10**3
a=np.random.rand(N, M)

我得到:

%timeit py_expsum(a)  #   87ms
%timeit nb_expsum(a)  #  672ms
%timeit nb_expsum2(a)  #  412ms

计算时间的大部分(约90%)用于评估exp-函数,并且正如我们将看到的,这是一项CPU密集型任务。

快速浏览top-统计数据,可以看到numpy的版本是并行执行的,但numba并非如此。但是,在我只有两个处理器的VM上,仅并行化无法解释因子7的巨大差异(如DavidW版本nb_expsum2所示)。

通过perf对两个版本的代码进行分析都显示以下内容:

nb_expsum

Overhead  Command  Shared Object                                      Symbol                                                             
  62,56%  python   libm-2.23.so                                       [.] __ieee754_exp_avx
  16,16%  python   libm-2.23.so                                       [.] __GI___exp
   5,25%  python   perf-28936.map                                     [.] 0x00007f1658d53213
   2,21%  python   mtrand.cpython-37m-x86_64-linux-gnu.so             [.] rk_random

py_expsum

  31,84%  python   libmkl_vml_avx.so                                  [.] mkl_vml_kernel_dExp_E9HAynn                                   ▒
   9,47%  python   libiomp5.so                                        [.] _INTERNAL_25_______src_kmp_barrier_cpp_38a91946::__kmp_wait_te▒
   6,21%  python   [unknown]                                          [k] 0xffffffff8140290c                                            ▒
   5,27%  python   mtrand.cpython-37m-x86_64-linux-gnu.so             [.] rk_random  

正如人们所看到的:numpy在引擎盖下使用了英特尔的并行化矢量化的mkl / vml-version,它的性能很容易超过numba使用的gnu-math-library(lm.so)版本(或并行版本) numba或cython处理)。通过使用并行化,可以稍微平整地面,但是mkl的矢量化版本仍然胜过numba和cython。

但是,仅查看一种尺寸的性能并不是很有启发性,对于exp(对于其他先验功能),有2个方面需要考虑:

  • 数组中的元素数量-缓存效果和针对不同大小的不同算法(numpy闻所未闻)可导致不同的性能。
  • 根据x值,需要不同的时间来计算exp(x)。通常,有三种不同类型的输入会导致不同的计算时间:非常小,标准和非常大(结果是不确定的)

我正在使用perfplot可视化结果(请参阅附录中的代码)。对于“正常”范围,我们获得以下性能:

enter image description here

尽管0.0的性能相似,但我们可以看到,结果变为无限时,英特尔的VML会产生相当大的负面影响:

enter image description here

不过,还有其他需要注意的地方:

  • 对于向量大小<= 8192 = 2^13,numpy使用exp的非并行glibc版本(也使用相同的numba和cython)。
  • Anaconda-distribution,我使用overrides numpy's functionality and plugs Intel's VML-library表示大于8192的大小,并对其进行了矢量化和并行化-这解释了大小约为10 ^ 4的运行时间的减少。
  • 对于较小的大小,numba轻松击败了常规的glibc版本(对于numpy而言开销太大),但是(如果numpy不切换到VML)对于较大的数组将没有太大的区别。
  • 这似乎是CPU限制的任务-我们在任何地方都看不到缓存边界。
  • 并行化的numba版本只有在元素数超过500时才有意义。

那会有什么后果?

  1. 如果元素不超过8192个,则应使用numba-version。
  2. 否则为numpy版本(即使没有可用的VML插件也不会损失太多)。

注意:numba无法自动使用英特尔VML中的vdExp(正如注释中的部分建议),因为它会单独计算exp(x),而VML在整个数组上运行。


可以减少写入和加载数据时的缓存丢失,这是由numpy-version使用以下算法执行的:

  1. 对一部分适合缓存的数据执行VML的vdExp,但它又不会太小(开销)。
  2. 总结得出的工作数组。
  3. 执行1. + 2。对于下一部分数据,直到处理完所有数据为止。

但是,与numpy的版本相比,我预计不会获得超过10%的收益(但也许我错了),因为无论如何,90%的计算时间都花在了MVL中。

尽管如此,这还是可以在Cython中实现快速而肮脏的实现:

%%cython -L=<path_mkl_libs> --link-args=-Wl,-rpath=<path_mkl_libs> --link-args=-Wl,--no-as-needed -l=mkl_intel_ilp64 -l=mkl_core -l=mkl_gnu_thread -l=iomp5
# path to mkl can be found via np.show_config()
# which libraries needed: https://software.intel.com/en-us/articles/intel-mkl-link-line-advisor

# another option would be to wrap mkl.h:
cdef extern from *:
    """
    // MKL_INT is 64bit integer for mkl-ilp64
    // see https://software.intel.com/en-us/mkl-developer-reference-c-c-datatypes-specific-to-intel-mkl
    #define MKL_INT long long int
    void  vdExp(MKL_INT n, const double *x, double *y);
    """
    void vdExp(long long int n, const double *x, double *y)

def cy_expsum(const double[:,:] v):
        cdef:
            double[1024] w;
            int n = v.size
            int current = 0;
            double res = 0.0
            int size = 0
            int i = 0
        while current<n:
            size = n-current
            if size>1024:
                size = 1024
            vdExp(size, &v[0,0]+current, w)
            for i in range(size):
                res+=w[i]
            current+=size
        return res

然而,numexpr确实可以做什么,它也使用Intel的vml作为后端:

 import numexpr as ne
 def ne_expsum(x):
     return ne.evaluate("sum(exp(x))")

关于计时,我们可以看到以下内容:

enter image description here

具有以下值得注意的细节:

  • numpy,numexpr和cython版本对于较大的阵列几乎具有相同的性能-这并不奇怪,因为它们使用相同的vml功能。
  • 这三个版本中,cython版本的开销最少,而numexpr最多
  • numexpr-version可能是最容易编写的(鉴于并非每个numpy发行版都具有mvl功能)。

列表:

图:

import numpy as np
def py_expsum(x):
    return np.sum(np.exp(x))

import numba as nb
@nb.jit( nopython=True)    
def nb_expsum(x):
    nx, ny = x.shape
    val = 0.0
    for ix in range(nx):
        for iy in range(ny):
            val += np.exp( x[ix, iy] )
    return val

@nb.jit( nopython=True, parallel=True)    
def nb_expsum2(x):
    nx, ny = x.shape
    val = 0.0
    for ix in range(nx):
        for iy in nb.prange(ny):
            val += np.exp( x[ix, iy]   )
    return val

import perfplot
factor = 1.0 # 0.0 or 1e4
perfplot.show(
    setup=lambda n: factor*np.random.rand(1,n),
    n_range=[2**k for k in range(0,27)],
    kernels=[
        py_expsum, 
        nb_expsum,
        nb_expsum2, 
        ],
    logx=True,
    logy=True,
    xlabel='len(x)'
    )

答案 1 :(得分:4)

添加并行化。在Numba中,只涉及制作外部循环prange并将parallel=True添加到jit选项:

@numba.jit( nopython=True,parallel=True)    
def nb_expsum2(x):
    nx, ny = x.shape
    val = 0.0
    for ix in numba.prange(nx):
        for iy in range(ny):
            val += np.exp( x[ix, iy]   )
    return val

在我的PC上,它的速度是非并行版本的3.2倍。就是说,在我的PC上,Numba和Cython都击败了Numpy。

您也可以parallelization in Cython-我在这里还没有测试过,但是我希望它的性能与Numba相似。 (另请注意,对于Cython,您可以从nxny获得x.shape[0]x.shape[1],因此您不必关闭边界检查,而完全依靠用户输入保持在范围之内。

答案 2 :(得分:2)

这取决于exp的实现和并行化

如果您在Numpy中使用Intel SVML,请在其他软件包(例如Numba,Numexpr或Cython)中使用它。 Numba performance tips

如果将Numpy命令并行化,请尝试在Numba或Cython中将其并行化。

代码

import os
#Have to be before importing numpy
#Test with 1 Thread against a single thread Numba/Cython Version and
#at least with number of physical cores against parallel versions
os.environ["MKL_NUM_THREADS"] = "1" 

import numpy as np
import numba as nb

def py_expsum(x):
    return np.sum( np.exp(x) )

@nb.njit(parallel=False) #set it to True for a parallel version  
def nb_expsum(x):
    val = 0.
    for ix in nb.prange(x.shape[0]):
        for iy in range(x.shape[1]):
            val += np.exp(x[ix,iy])
    return val

N,M=2000, 1000
#a=np.random.rand(N*M).reshape((N,M)).astype(np.float32)
a=np.random.rand(N*M).reshape((N,M))

基准

#float64
%timeit py_expsum(a) #os.environ["MKL_NUM_THREADS"] = "1" 
8.79 ms ± 85.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit py_expsum(a) #os.environ["MKL_NUM_THREADS"] = "6" 
5.78 ms ± 131 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_expsum(a) #parallel=false
9.51 ms ± 89.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_expsum(a) #parallel=True
1.77 ms ± 136 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

#float32
%timeit py_expsum(a) #os.environ["MKL_NUM_THREADS"] = "1" 
4.08 ms ± 86 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit py_expsum(a) #os.environ["MKL_NUM_THREADS"] = "6" 
3.09 ms ± 335 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_expsum(a) #parallel=false
7.35 ms ± 38 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit nb_expsum(a) #parallel=True
1.29 ms ± 31 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

Numba显然是由于自动键入系统而导致float32出现了一些问题,但在两种情况下,并行版本都比Numpy版本快。在这两种情况下,都使用了SVML。