在Cython中优化代码的技巧

时间:2013-09-03 13:14:46

标签: python optimization cython

我有一个相对简单的问题(我认为)。我正在研究一段Cython代码,当给定应变和特定方向时(即,对于一定量的应变,平行于给定方向的半径),计算应变椭圆的半径。在每个程序运行期间,此函数被称为几百万次,并且分析显示该功能是性能方面的限制因素。这是代码:

# importing math functions from a C-library (faster than numpy)
from libc.math cimport sin, cos, acos, exp, sqrt, fabs, M_PI

cdef class funcs:

    cdef inline double get_r(self, double g, double omega):
        # amount of strain: g, angle: omega
        cdef double l1, l2, A, r, g2, gs   # defining some variables
        if g == 0: return 1   # no strain means the strain ellipse is a circle
        omega = omega*M_PI/180   # converting angle omega to radians
        g2 = g*g
        gs = g*sqrt(4 + g2)
        l1 = 0.5*(2 + g2 + gs)   # l1 and l2: eigenvalues of the Cauchy strain tensor
        l2 = 0.5*(2 + g2 - gs)
        A = acos(g/sqrt(g2 + (1 - l2)**2))   # orientation of the long axis of the ellipse
        r = 1./sqrt(sqrt(l2)*(cos(omega - A)**2) + sqrt(l1)*(sin(omega - A)**2))   # the radius parallel to omega
        return r   # return of the jedi

运行此代码每次调用大约需要0.18微秒,我认为这对于这样一个简单的函数来说有点长。此外,math.h有一个方形(x)函数,但我无法从libc.math库中导入它,任何人都知道如何?有什么其他建议可以进一步改善这段小代码的性能吗?

更新2013/09/04:

似乎有更多的在游戏中而不是满足于眼睛。当我分析一个调用get_r 10万次的函数时,我得到的性能与调用另一个函数不同。我添加了部分代码的更新版本。当我使用get_r_profile进行分析时,get_r的每次调用都会得到0.073微秒,而MC_criterion_profile给我约0.164微秒/ get_r的调用,差异为50%似乎与return r的开销成本有关。

from libc.math cimport sin, cos, acos, exp, sqrt, fabs, M_PI

cdef class thesis_funcs:

    cdef inline double get_r(self, double g, double omega):
        cdef double l1, l2, A, r, g2, gs, cos_oa2, sin_oa2
        if g == 0: return 1
        omega = omega*SCALEDPI
        g2 = g*g
        gs = g*sqrt(4 + g2)
        l1 = 0.5*(2 + g2 + gs)
        l2 = l1 - gs
        A = acos(g/sqrt(g2 + square(1 - l2)))
        cos_oa2 = square(cos(omega - A))
        sin_oa2 = 1 - cos_oa2
        r = 1.0/sqrt(sqrt(l2)*cos_oa2 + sqrt(l1)*sin_oa2)
        return r

    @cython.profile(False)
    cdef inline double get_mu(self, double r, double mu0, double mu1):
        return mu0*exp(-mu1*(r - 1))

    def get_r_profile(self): # Profiling through this guy gives me 0.073 microsec/call
        cdef unsigned int i
        for i from 0 <= i < 10000000:
            self.get_r(3.0, 165)

    def MC_criterion(self, double g, double omega, double mu0, double mu1, double C = 0.0):
        cdef double r, mu, theta, res
        r = self.get_r(g, omega)
        mu = self.get_mu(r, mu0, mu1)
        theta = 45 - omega
        theta = theta*SCALEDPI
        res = fabs(g*sin(2.0*theta)) - mu*(1 + g*cos(2.0*theta)) - C
        return res

    def MC_criterion_profile(self): # Profiling through this one gives 0.164 microsec/call
        cdef double g, omega, mu0, mu1
        cdef unsigned int i
        omega = 165
        mu0 = 0.6
        mu1 = 2.0
        g = 3.0
        for i from 1 <= i < 10000000:
            self.MC_criterion(g, omega, mu0, mu1)

我认为get_r_profileMC_criterion之间可能存在根本区别,这会导致额外的开销成本。你能发现它吗?

3 个答案:

答案 0 :(得分:5)

根据您的评论,行计算r是最昂贵的。如果是这种情况,那么我怀疑是触发性能的trig函数调用。

毕达哥拉斯,cos(x)**2 + sin(x)**2 == 1所以你可以通过计算来跳过其中一个电话

cos_oa2 = cos(omega - A)**2
sin_oa2 = 1 - cos_oa2
r = 1. / sqrt(sqrt(l2) * cos_oa2 + sqrt(l1) * sin_oa2)

(或者可能翻转它们:在我的机器上,sin似乎比cos快。但可能是一个NumPy小故障。)

答案 1 :(得分:2)

的输出
cython -a

表示测试除以0。如果你确定不会发生这种情况,你可能想要删除这项检查。

要使用C分区,您可以将以下指令添加到文件顶部:

# cython: cdivision=True

我会链接官方文档,但我现在无法访问它。您在此处有一些信息(第15页):https://python.g-node.org/python-summerschool-2011/_media/materials/cython/cython-slides.pdf

答案 2 :(得分:0)

这个答案与Cython无关,但应该提一些可能有帮助的要点。

  1. 在真正知道他们是否需要之前定义变量可能并不理想。在“if g == 0”语句后移动“cdef double l1,l2,A,r,g2,gs”。
  2. 我会确保从“omega = omega * M_PI / 180”开始,M_PI / 180部分只计算一次。例如。一些Python代码:

    import timeit
    from math import pi
    
    def calc1( omega ):
        return omega * pi / 180
    
    SCALEDPI = pi / 180
    def calc2( omega ):
        return omega * SCALEDPI
    
    if __name__ == '__main__':
        took = timeit.repeat( stmt = lambda:calc1( 5 ), number = 10000 )
        print "Min. time: %.4f Max. time: %.4f" % ( min( took ), max( took ) )
        took = timeit.repeat( stmt = lambda:calc2( 5 ), number = 10000 )
        print "Min. time: %.4f Max. time: %.4f" % ( min( took ), max( took ) )
    

    calc1:Min。时间:0.0033最大时间:0.0034

    calc2:Min。时间:0.0029最大时间:0.0029

  3. 尝试自己优化计算。它们看起来相当复杂,我觉得它们可以简化。