我第一次使用cython来获得某个功能的速度。该函数采用方形矩阵A
浮点数并输出单个浮点数。它正在计算的功能是permanent of a matrix
当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,因此基本优化已经有效。
这是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版本的时间(使用ipython),numpy版本和romeric对cython代码的改进。我已将种子设定为可重复性。
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。
答案 0 :(得分: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
修改强>
让我们通过展开内部SSE
循环执行一些基本的prod
向量化,即将上面代码中的循环更改为以下
# 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代码加速,还不错。
答案 1 :(得分: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
相对于NumPy版本的最终x232加速,加速与展开的Cython版本相当,没有太多的手动调整。
答案 2 :(得分: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
,因此您有时会得到不同的结果。另外,我将while
之前的两个循环和while
内的两个循环组合在一起。
最后,比较
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
答案 3 :(得分:0)
嗯,一个明显的优化是将d [i]设置为-2和+2并避免乘以2.我怀疑这不会产生任何影响,但仍然存在。
另一个是确保编译结果代码的C ++编译器启用了所有优化(特别是向量化)。
计算新v [i]的循环可以与Cython's support of OpenMP并行化。在30次迭代中,这也可能没有区别。