2016-12-24 187 views
5

我第一次使用cython來獲得一些函數的速度。該函數採用方形矩陣A浮點數並輸出單個浮點數。它是計算功能是permanent of a matrix這個cython代碼可以優化嗎?

enter image description here

當A爲30×30我的代碼目前需要大約60秒我的電腦上。

在下面的代碼中,我已經爲維基頁面上的永久實現了Balasubramanian-Bax/Franklin-Glynn公式。我稱之爲矩陣M.

代碼的一個複雜部分是數組f,它用於保存下一個要在數組d中翻轉的位置的索引。數組d保存的值是+ -1。循環中f和j的操作只是快速更新格雷碼的聰明方法。

from __future__ import division 
import numpy as np 
cimport numpy as np 
cimport cython 


DTYPE_int = np.int 
ctypedef np.int_t DTYPE_int_t 
DTYPE_float = np.float64 
ctypedef np.float64_t DTYPE_float_t 

@cython.boundscheck(False) # turn off bounds-checking for entire function 
@cython.wraparound(False) # turn off negative index wrapping for entire function 
def permfunc(np.ndarray [DTYPE_float_t, ndim =2, mode='c'] M): 
    cdef int n = M.shape[0] 
    cdef np.ndarray[DTYPE_float_t, ndim =1, mode='c' ] d = np.ones(n, dtype=DTYPE_float) 
    cdef int j = 0 
    cdef int s = 1 
    cdef np.ndarray [DTYPE_int_t, ndim =1, mode='c'] f = np.arange(n, dtype=DTYPE_int) 
    cdef np.ndarray [DTYPE_float_t, ndim =1, mode='c'] v = M.sum(axis=0) 
    cdef DTYPE_float_t p = 1 
    cdef int i 
    cdef DTYPE_float_t prod 
    for i in range(n): 
     p *= v[i] 
    while (j < n-1): 
     for i in range(n): 
      v[i] -= 2*d[j]*M[j, i] 
     d[j] = -d[j] 
     s = -s 
     prod = 1 
     for i in range(n): 
      prod *= v[i] 
     p += s*prod 
     f[0] = 0 
     f[j] = f[j+1] 
     f[j+1] = j+1 
     j = f[0] 
    return p/2**(n-1) 

我已經使用了我在cython教程中找到的所有簡單優化。我不得不承認我不完全理解的一些方面。例如,如果我將數組d整數,因爲這些值只有+1,所以代碼運行速度大約慢了10%,所以我將其作爲float64s。

有什麼我可以做的,以加快代碼?


這是用Cython -a的結果。正如你可以看到循環中的所有內容都被編譯爲C,所以基本的優化已經奏效。

Result of cython -a

這裏是numpy的同樣的功能比我目前用Cython版本慢100倍以上。

def npperm(M): 
    n = M.shape[0] 
    d = np.ones(n) 
    j = 0 
    s = 1 
    f = np.arange(n) 
    v = M.sum(axis=0) 
    p = np.prod(v) 
    while (j < n-1): 
     v -= 2*d[j]*M[j] 
     d[j] = -d[j] 
     s = -s 
     prod = np.prod(v) 
     p += s*prod 
     f[0] = 0 
     f[j] = f[j+1] 
     f[j+1] = j+1 
     j = f[0] 
    return p/2**(n-1) 

時序更新

這裏是定時我用Cython版本,該版本numpy的和romeric的改進到用Cython代碼(使用IPython中)。我爲可重複性設定了種子。

from scipy.stats import ortho_group 
import pyximport; pyximport.install() 
import permlib # This loads in the functions from permlib.pyx 
import numpy as np; np.random.seed(7) 
M = ortho_group.rvs(23) #Creates a random orthogonal matrix 
%timeit permlib.npperm(M) # The numpy version 
1 loop, best of 3: 44.5 s per loop 
%timeit permlib.permfunc(M) # The cython version 
1 loop, best of 3: 273 ms per loop 
%timeit permlib.permfunc_modified(M) #romeric's improvement 
10 loops, best of 3: 198 ms per loop 
M = ortho_group.rvs(28) 
%timeit permlib.permfunc(M) # The cython version run on a 28x28 matrix 
1 loop, best of 3: 15.8 s per loop 
%timeit permlib.permfunc_modified(M) # romeric's improvement run on a 28x28 matrix 
1 loop, best of 3: 12.4 s per loop 

能否用Cython代碼可以加速呢?

我用gcc和CPU是AMD FX 8350.

+1

是:你可以問這個上[codereview.se。 – usr2564301

+2

@RadLexus謝謝。然而,看起來在這裏很少有cython問題。曾經有過30次! – eleanora

+5

@eleanora:這樣的推理使得這個數字一直很低。 – Mat

回答

1

此答案是基於之前發佈的@romeric的代碼。我糾正了代碼並簡化了它,並添加了cdivision編譯器指令。

@cython.boundscheck(False) 
@cython.wraparound(False) 
@cython.cdivision(True) 
def permfunc_modified_2(np.ndarray [double, ndim =2, mode='c'] M): 
    cdef: 
     int n = M.shape[0], s=1, i, j 
     int *f = <int*>malloc(n*sizeof(int)) 
     double *d = <double*>malloc(n*sizeof(double)) 
     double *v = <double*>malloc(n*sizeof(double)) 
     double p = 1, prod 

    for i in range(n): 
     v[i] = 0. 
     for j in range(n): 
      v[i] += M[j,i] 
     p *= v[i] 
     f[i] = i 
     d[i] = 1 
    j = 0 
    while (j < n-1): 
     prod = 1. 
     for i in range(n): 
      v[i] -= 2.*d[j]*M[j, i] 
      prod *= v[i] 
     d[j] = -d[j] 
     s = -s    
     p += s*prod 
     f[0] = 0 
     f[j] = f[j+1] 
     f[j+1] = j+1 
     j = f[0] 

    free(d) 
    free(f) 
    free(v) 
    return p/pow(2.,(n-1)) 

@romeric的原始代碼沒有初始化v,所以你可能會遇到不同的結果。此外,我分別在whilewhile內部的兩個環路之前組合了兩個環路。

最後,比較

In [1]: from scipy.stats import ortho_group 
In [2]: import permlib 
In [3]: import numpy as np; np.random.seed(7) 
In [4]: M = ortho_group.rvs(5) 
In [5]: np.equal(permlib.permfunc(M), permlib.permfunc_modified_2(M)) 
Out[5]: True 
In [6]: %timeit permfunc(M) 
10000 loops, best of 3: 20.5 µs per loop 
In [7]: %timeit permlib.permfunc_modified_2(M) 
1000000 loops, best of 3: 1.21 µs per loop 
In [8]: M = ortho_group.rvs(15) 
In [9]: np.equal(permlib.permfunc(M), permlib.permfunc_modified_2(M)) 
Out[9]: True 
In [10]: %timeit permlib.permfunc(M) 
1000 loops, best of 3: 1.03 ms per loop 
In [11]: %timeit permlib.permfunc_modified_2(M) 
1000 loops, best of 3: 432 µs per loop 
In [12]: M = ortho_group.rvs(28) 
In [13]: np.equal(permlib.permfunc(M), permlib.permfunc_modified_2(M)) 
Out[13]: True 
In [14]: %timeit permlib.permfunc(M) 
1 loop, best of 3: 14 s per loop 
In [15]: %timeit permlib.permfunc_modified_2(M) 
1 loop, best of 3: 5.73 s per loop 
+0

太棒了!我無法弄清楚爲什麼代碼有時會給出錯誤的答案。謝謝。我認爲乘以2可以移出循環,對吧? – eleanora

+0

是的,你可以寫'''d [i] = 2.'''和''v [i] - = d [j] * M [j,i]''',但不會改變代碼的性能 – sebacastroh

0

嗯,一個明顯的優化設置d [I] -2到+2和2.避免乘法我猜想這不會有什麼區別,但依然如此。

另一種方法是確保編譯生成代碼的C++編譯器已啓用所有優化(特別是向量化)。

計算新v [i] s的循環可以與Cython's support of OpenMP並行化。在30次迭代中,這也可能沒有什麼區別。

+0

感謝您的想法。你對2的因子是正確的。cython代碼是C(不是C++)。我會嘗試使用編譯器標誌,看看它是否有任何區別。我想我會需要並行化主while循環,因爲如果我使用不同的方法來計算格雷碼,那麼你會建議哪些是可行的。 – eleanora

3

您的cython函數功能不多,因爲它已經很好的優化了。但是,您仍然可以通過完全避免撥打numpy來獲得適度的加速。

import numpy as np 
cimport numpy as np 
cimport cython 
from libc.stdlib cimport malloc, free 
from libc.math cimport pow 

cdef inline double sum_axis(double *v, double *M, int n): 
    cdef: 
     int i, j 
    for i in range(n): 
     for j in range(n): 
      v[i] += M[j*n+i] 


@cython.boundscheck(False) 
@cython.wraparound(False) 
def permfunc_modified(np.ndarray [double, ndim =2, mode='c'] M): 
    cdef: 
     int n = M.shape[0], j=0, s=1, i 
     int *f = <int*>malloc(n*sizeof(int)) 
     double *d = <double*>malloc(n*sizeof(double)) 
     double *v = <double*>malloc(n*sizeof(double)) 
     double p = 1, prod 

    sum_axis(v,&M[0,0],n) 

    for i in range(n): 
     p *= v[i] 
     f[i] = i 
     d[i] = 1 

    while (j < n-1): 
     for i in range(n): 
      v[i] -= 2.*d[j]*M[j, i] 
     d[j] = -d[j] 
     s = -s 
     prod = 1 
     for i in range(n): 
      prod *= v[i] 
     p += s*prod 
     f[0] = 0 
     f[j] = f[j+1] 
     f[j+1] = j+1 
     j = f[0] 

    free(d) 
    free(f) 
    free(v) 
    return p/pow(2.,(n-1)) 

這裏有必要檢查和定時:

In [1]: n = 12 
In [2]: M = np.random.rand(n,n) 
In [3]: np.allclose(permfunc_modified(M),permfunc(M)) 
True 
In [4]: n = 28 
In [5]: M = np.random.rand(n,n) 
In [6]: %timeit permfunc(M) # your version 
1 loop, best of 3: 28.9 s per loop 
In [7]: %timeit permfunc_modified(M) # modified version posted above 
1 loop, best of 3: 21.4 s per loop 

EDIT 允許通過展開內prod循環執行一些基本SSE向量化,也就是在上面的代碼改變環路以下

# define t1, t2 and t3 earlier as doubles 
t1,t2,t3=1.,1.,1. 
for i in range(0,n-1,2): 
    t1 *= v[i] 
    t2 *= v[i+1] 
# define k earlier as int 
for k in range(i+2,n): 
    t3 *= v[k] 
p += s*(t1*t2*t3) 

現在時機

In [8]: %timeit permfunc_modified_vec(M) # vectorised 
1 loop, best of 3: 14.0 s per loop 

所以幾乎2X加速比原先的優化用Cython代碼,還不錯。

+0

這太好了。謝謝。兩個問題。 a)如果我使用單精度浮點數而不是雙精度浮點數,b)有沒有辦法幫助編譯器向量化代碼? – eleanora

+0

根據體系結構和編譯器的不同,雙重更改爲單一可能會有幫助,也可能無幫助。關於矢量化,請查看我更新的答案 – romeric

+0

@eleanora哦。展開時,必須確保for循環的大小是步長的倍數(在這種情況下爲2)。我現在已經更新了答案。另外,你的代碼很快就會崩潰,因此對於變量'p'有溢出問題。結果,「SSE」和標量代碼將導致對於較大的「M」結果不同。 – romeric

3

免責聲明:我下面

提到作爲替代用Cython工具的核心開發,你可以給一個嘗試pythran。一個單一的註釋到原始numpy的代碼:

#pythran export npperm(float[:,:]) 
import numpy as np 
def npperm(M): 
    n = M.shape[0] 
    d = np.ones(n) 
    j = 0 
    s = 1 
    f = np.arange(n) 
    v = M.sum(axis=0) 
    p = np.prod(v) 
    while (j < n-1): 
     v -= 2*d[j]*M[j] 
     d[j] = -d[j] 
     s = -s 
     prod = np.prod(v) 
     p += s*prod 
     f[0] = 0 
     f[j] = f[j+1] 
     f[j+1] = j+1 
     j = f[0] 
    return p/2**(n-1) 

編譯時:

> pythran perm.py 

產生類似一個與用Cython的加速:

> # numpy version 
> python -mtimeit -r3 -n1 -s 'from scipy.stats import ortho_group; from perm import npperm ; import numpy as np; np.random.seed(7);M = ortho_group.rvs(23)' 'npperm(M)' 
1 loops, best of 3: 21.7 sec per loop 
> # pythran version 
> pythran perm.py 
> python -mtimeit -r3 -n1 -s 'from scipy.stats import ortho_group; from perm import npperm ; import numpy as np; np.random.seed(7);M = ortho_group.rvs(23)' 'npperm(M)' 
1 loops, best of 3: 171 msec per loop 

在不需要重新實現sum_axis的(它已經由pythran完成了)。

更有趣,pythran能夠識別若干向量化的(在產生SSE/AVX內在的意義上)的圖案,通過一個選項標誌:

> pythran perm.py -DUSE_BOOST_SIMD -march=native 
> python -mtimeit -r3 -n10 -s 'from scipy.stats import ortho_group; from perm import npperm ; import numpy as np; np.random.seed(7);M = ortho_group.rvs(23)' 'npperm(M)' 
10 loops, best of 3: 93.2 msec per loop 

這使得最終x232加速與numpy的版本,與展開的cython版本相媲美,而且沒有太多的手動調整。

+0

這非常有趣謝謝你!我會在幾天內正確測試您的代碼並回報。 – eleanora