【问题标题】:Tips for optimising code in Cython在 Cython 中优化代码的技巧
【发布时间】:2013-09-05 01:43:08
【问题描述】:

我有一个相对简单的问题(我认为)。我正在研究一段 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 有一个 square(x) 函数,但我无法从libc.math 库中导入它,有人知道怎么做吗?对于进一步提高这段代码的性能还有其他建议吗?

2013 年 9 月 4 日更新:

似乎有比看上去更多的东西。当我分析一个调用get_r 1000 万次的函数时,我得到的性能与调用另一个函数不同。我添加了部分代码的更新版本。当我使用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 之间可能存在根本区别,这会导致额外的间接费用。你能看出来吗?

【问题讨论】:

  • 您的&lt;math.h&gt;square 功能?这不是标准功能,您在哪个平台上?在任何情况下,您都可以定义自己的cdef inline square(x): return x * x 以摆脱对pow 的所有调用。
  • 如果您将get_r 从类中解耦,函数call 可能会更快。不需要的话就不用self了。
  • @larsmans 尝试了这两个建议,但没有明显的收获。倒数第二行r = 1./... 是迄今为止最贵的,所以我需要先降低那一行的成本。
  • 为了比较速度,我用纯 C 编译了你的 get_rget_r_profile 函数。纯 C get_r_profile 似乎比 Cython 快 2.5-3,所以也许它是一个编译选项get_r 单独并从 Cython 链接到它?有趣的是,根据优化标志,编译器似乎完全跳过了对get_r 的调用!不知道这如何与 Cython 生成的 C 代码一起发挥作用,但也许这样的事情会导致您看到的时间差异。

标签: python optimization cython


【解决方案1】:

根据您的评论,计算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 故障。)

【讨论】:

  • 谢谢,这将性能提高了大约 10%。我刚才还注意到return r 消耗了大约 50% 的计算时间!如果我将 return r 替换为 return 0.5,我会获得巨大的加速,所以我怀疑这涉及到一些 C 到 Python 的开销。
  • @MPA:奇怪,我认为这不应该发生在具有声明返回类型的 cdef 函数中。
  • 我已经用一些附加信息更新了我原来的问题
  • @MPA:尝试将每个函数的结果分配给适当类型的虚拟变量。 Cython 可能会将输出转换为 PyObject*
  • 试过了,没什么区别。
【解决方案2】:

输出

cython -a

表示测试除以 0。如果您 200% 确定它不会发生,您可能希望删除此检查。

要使用 C 部门,您可以在文件顶部添加以下指令:

# cython: cdivision=True

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

【讨论】:

  • 感谢您的回复。使用该装饰器并没有带来任何性能提升。此外,我不确定除以零永远不会发生。
  • 我链接的 PDF 中还有其他提示,例如对编译器使用更积极的优化。如果输入值可能在程序运行一百万次中重复,您可以尝试将结果缓存在字典中。另一个想法是对 cos() 和 sin() 使用表,但这取决于您需要的数值精度。
【解决方案3】:

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

  1. 在真正知道是否需要变量之前定义变量可能并不理想。将“cdef double l1, l2, A, r, g2, gs”移到“if g == 0”语句之后。
  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:最小。时间:0.0033 最大。时间:0.0034

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

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

【讨论】:

  • M_PI/180 部分应由编译器处理。
  • 感谢您的回复。前两个建议没有带来任何性能提升。至于第三个:我不认为这些方程可以进一步简化:(
  • cdefs 实际上并不执行计算,它们必须位于函数的开头。
猜你喜欢
  • 2012-08-30
  • 1970-01-01
  • 2016-04-25
  • 2010-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-30
  • 1970-01-01
相关资源
最近更新 更多