使用Cython没有速度提升

28 投票
7 回答
10712 浏览
提问于 2025-04-16 07:32

我正在尝试定义一个函数,这个函数里面有一个循环,用来模拟一个积分。

问题是速度太慢了。在我的电脑上,评估这个函数一次可能需要30秒。因为我最终的目标是最小化这个函数,所以如果能加快速度就更好了。

为此,我尝试使用Cython,但我可能犯了很严重的错误(可能还有很多错误!)。根据Cython的文档,我尝试给我的变量加上类型。可是这样做后,代码的运行速度和纯Python一样慢。这让我觉得很奇怪。

这是我的代码:

import numpy as np 
cimport cython
cimport numpy as np
import minuit

data = np.genfromtxt('q6data.csv', usecols = np.arange(1, 24, 1), delimiter = ',')  

cdef int ns    = 1000                 # Number of simulation draws
cdef int K     = 5                    # Number of observed characteristics, including            constant
cdef int J     = len(data[:,1])       # Number of products, including outside
cdef double tol   = 0.0001            # Inner GMM loop tolerance
nu = np.random.normal(0, 1, (6, ns))  # ns random deviates

@cython.boundscheck(False)
@cython.wraparound(False)


def S(np.ndarray[double, ndim=1] delta, double s1, double s2, double s3, double s4,  double s5, double a):
    """Computes the simulated integrals, one for each good.
    Parameters: delta is an array of length J containing mean product specific utility levels
    Returns: Numpy array with length J."""
    cdef np.ndarray[double, ndim=2] mu_ij = np.dot((data[:,2:7]*np.array([s1, s2, s3, s4, s5])), nu[1:K+1,:])
    cdef np.ndarray[double, ndim=2] mu_y  = a * np.log(np.exp(data[:,21].reshape(J,1) +  data[:,22].reshape(J,1)*nu[0,:].reshape(1, ns)) - data[:,7].reshape(J,1))
    cdef np.ndarray[double, ndim=2] V = delta.reshape(J,1) + mu_ij + mu_y
    cdef np.ndarray[double, ndim=2] exp_vi = np.exp(V)
    cdef np.ndarray[double, ndim=2] P_i = (1.0 / np.sum(exp_vi[np.where(data[:,1] == 71)], 0)) * exp_vi[np.where(data[:,1] == 71)] 
    cdef int yrs = 19
    cdef int yr
    for yr in xrange(yrs):
        P_yr = (1.0 / np.sum(exp_vi[np.where(data[:,1]== (yr + 72))], 0)) *   exp_vi[np.where(data[:,1] == (yr + 72))]
        P_i  = np.concatenate((P_i, P_yr)) 
    cdef np.ndarray[double, ndim=1] S = np.zeros(dtype = "d", shape = J)
    cdef int j
    for j in xrange(ns):
        S += P_i[:,j]
    return (1.0 / ns) * S

def d_infty(np.ndarray[double, ndim=1] x, np.ndarray[double, ndim=1] y):
    """Sup norm."""
    return np.max(np.abs(x - y)) 

def T(np.ndarray[double, ndim=1] delta_exp, double s1, double s2, double s3, double s4,  double s5, double a):
    """The contraction operator.  This function takes the parameters and the exponential
    of the starting value of delta and returns the fixed point.""" 
    cdef int iter = 0
    cdef int maxiter = 200
    cdef int i
    for i in xrange(maxiter): 
        delta1_exp = delta_exp * (data[:, 8] / S(np.log(delta_exp), s1, s2, s3, s4, s5, a))                    
        print i
        if d_infty(delta_exp, delta1_exp) < tol:                                       
            break
        delta_exp = delta1_exp
    return np.log(delta1_exp)


def Q(double s1, double s2, double s3, double s4, double s5, double a):
    """GMM objective function."""  
    cdef np.ndarray[double, ndim=1] delta0_exp = np.exp(data[:,10])                                                     
    cdef np.ndarray[double, ndim=1] delta1 = T(delta0_exp, s1, s2, s3, s4, s5, a)
    delta1[np.where(data[:,10]==0)] = np.zeros(len(np.where(data[:,10]==0)))            
    cdef np.ndarray[double, ndim=1] xi =  delta1 - (np.dot(data[:,2:7],   np.linalg.lstsq(data[:,2:7], delta1)[0]))   
    cdef np.ndarray[double, ndim=2] g_J = xi.reshape(J,1) * data[:,11:21]
    cdef np.ndarray[double, ndim=1] G_J = (1.0 / J) * np.sum(g_J, 0) 
    return np.sqrt(np.dot(G_J, G_J))

我对代码进行了性能分析,发现是函数S,也就是积分模拟器,导致了性能下降。总之,我本以为给变量加类型至少能提高一些速度,但结果没有任何提升,这让我觉得我可能犯了一些基本的错误。

有没有人能看到Cython代码中明显的错误,导致了这样的结果?

哦,对了,因为我刚开始学编程,所以代码里肯定有很多不好的风格和导致代码变慢的地方。如果你有时间,也请帮我指出这些问题。

7 个回答

11

你可以通过更多地利用Numpy的功能来加快你的代码运行速度。

比如说:

cdef np.ndarray[double, ndim=1] S = np.zeros(dtype = "d", shape = J)
cdef int j
for j in xrange(ns):
    S += P_i[:,j]

这样写会更快,而且更容易理解:

S = P_i.sum(axis=1)

你还重复了一些计算,这样就浪费了两倍的时间。例如:

np.where(data[:,1]==(yr + 72))

这个计算其实只需要做一次,然后把结果存储在一个变量里,以后可以重复使用。

另外,你在代码中进行了很多数据的重塑和切片操作:如果一开始就让你的变量保持简单的格式,会更有帮助。如果可以的话,你的代码会更清晰,优化的地方也会更明显。

17

Cython 并不会自动让你的代码变快,你需要了解它的内部工作原理,并检查生成的 C 代码。

特别是如果你想提高循环的性能,就要避免在循环中调用 Python 函数,而在这个例子中你恰好做了很多这样的调用(所有的 np. 调用都是 Python 函数,切片操作,以及可能还有其他的事情)。

想要了解使用 Cython 进行性能优化的一般指导,可以查看 这一页(-a 这个选项在优化时非常有用),而关于优化 numpy 代码的具体内容,可以参考 这一页

35

Cython可以生成一个html文件来帮助你理解这个问题:

cython -a MODULE.py

这个文件会把每一行源代码用白色到不同深浅的黄色来标记。黄色越深,说明那一行的代码还在执行更多动态的Python操作。对于每一行有黄色的代码,你需要添加更多的静态类型声明。

在我做这个的时候,我喜欢把我遇到问题的源代码拆分成很多单独的行,每行只写一个表达式或操作符,这样可以更清楚地看到每个部分。

如果不这样做,很容易忽略一些变量、函数调用或操作符的静态类型声明。(比如,索引操作符x[y]在没有声明的情况下,仍然是一个完全动态的Python操作)

撰写回答