5

我一直在尝试优化一段涉及大型多维数组计算的python代码。我用 numba 得到了违反直觉的结果。我在 MBP 上运行,2015 年中,2.5 GHz i7 四核,OS 10.10.5,python 2.7.11。考虑以下:

 import numpy as np
 from numba import jit, vectorize, guvectorize
 import numexpr as ne
 import timeit

 def add_two_2ds_naive(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @jit
 def add_two_2ds_jit(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @guvectorize(['float64[:,:],float64[:,:],float64[:,:]'],
    '(n,m),(n,m)->(n,m)',target='cpu')
 def add_two_2ds_cpu(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @guvectorize(['(float64[:,:],float64[:,:],float64[:,:])'],
    '(n,m),(n,m)->(n,m)',target='parallel')
 def add_two_2ds_parallel(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 def add_two_2ds_numexpr(A,B,res):
     res = ne.evaluate('A+B')

 if __name__=="__main__":
     np.random.seed(69)
     A = np.random.rand(10000,100)
     B = np.random.rand(10000,100)
     res = np.zeros((10000,100))

我现在可以在各种功能上运行 timeit:

%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.16 ms per loop

%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.19 ms per loop

%timeit add_two_2ds_parallel(A,B,res)
100 loops, best of 3: 6.9 ms per loop

%timeit add_two_2ds_numexpr(A,B,res)
1000 loops, best of 3: 1.62 ms per loop

似乎“并行”甚至没有使用单个核心的大部分,因为它的使用top表明 python 的“并行”达到了 ~40% cpu,“cpu”达到了 ~100%,numexpr 达到了 ~300% .

4

1 回答 1

6

您的 @guvectorize 实现存在两个问题。首先是您正在 @guvectorize 内核中执行所有循环,因此 Numba 并行目标实际上没有任何东西可以并行化。@vectorize 和 @guvectorize 都在 ufunc/gufunc 中的广播维度上并行化。由于您的 gufunc 的签名是 2D 的,并且您的输入是 2D 的,因此只有一次调用内部函数,这解释了您看到的唯一 100% CPU 使用率。

编写上述函数的最佳方法是使用常规 ufunc:

@vectorize('(float64, float64)', target='parallel')
def add_ufunc(a, b):
    return a + b

然后在我的系统上,我看到了这些速度:

%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.87 ms per loop

%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.81 ms per loop

%timeit add_two_2ds_parallel(A,B,res)
The slowest run took 11.82 times longer than the fastest. This could mean that an intermediate result is being cached 
100 loops, best of 3: 2.43 ms per loop

%timeit add_two_2ds_numexpr(A,B,res)
100 loops, best of 3: 2.79 ms per loop

%timeit add_ufunc(A, B, res)
The slowest run took 9.24 times longer than the fastest. This could mean that an intermediate result is being cached 
1000 loops, best of 3: 2.03 ms per loop

(这是一个与您的非常相似的 OS X 系统,但使用的是 OS X 10.11。)

尽管 Numba 的并行 ufunc 现在胜过 numexpr(我看到add_ufunc它使用了大约 280% 的 CPU),但它并没有胜过简单的单线程 CPU 案例。我怀疑瓶颈是由于内存(或缓存)带宽造成的,但我还没有进行测量来检查这一点。

一般来说,如果您对每个内存元素进行更多的数学运算(例如余弦),您将从并行 ufunc 目标中获得更多好处。

于 2016-02-11T22:43:02.630 回答