当我们使用cython来加速python模块时,我们需要到底在什么地方需要写c/c++代码来进行来加速,在什么地方写python来保持代码的灵活性。
当我们优化cython代码的时候,首先需要知道什么地方的代码需要进行改变,可以使用内置的分析工具profile模块(更快的C执行,cProfile),来查看个行代码的运行时间。
当使用分析工具的时候不需要对pure-python代码进行任何改动,当python代码中调用了c/c++的库的时候,这些分析工具不能跨语言进行分析,所有在c-level的分析信息都丢失了。
Cython解决了这个问题,它生成的C代码和这些工具兼容,让这些工具认为c-level的调用都是常规的python调用.
为了说明如何使用分析工具,来看一个例子. 首先我们有一个pure-python的代码,intergrate.py
def integrate(a, b, f, N=2000):
dx = (b-a)/N
s = 0.0
for i in range(N):
s += f(a+i*dx)
return s * dx
再建立一个main.py来使用这个函数
from integrate import integrate
from math import pi, sin
def sin2(x):
return sin(x)**2
def main():
a, b = 0.0, 0.2*pi
return integrate(a, b, sin2, N=400000)
# we use cProfile in profile our function, sorting by internal time spent in each function
if __name__ == '__main__':
import cProfile
cProfile.run('main()', sort='time')
本地运行代码
可以看到cProfile输出的结果。ncalls代表函数总条用次数,tottine列代表这个函数上总消耗的时间(不包含调用函数的时间),percall等于tottime除以ncalls. cumtime是这个函数上消耗的总时间(包含调用函数的时间),第二个percall等于cumtime除以ncalls。
接下来用静态类型声明来改进integrate函数。
def integrate(double a, double b, f, int N=2000):
cdef:
int i
double dx = (b-a)/N
double s = 0
for i in range(N):
s += f(a+i*dx)
return s*dx
可以对上述代码进行编译,产生.so文件,然后在main函数中调用,用cProfile进行运行时间分析,可以看出性能有显著提高
结果分析
对于main.py中的sin2, 它是一个纯python函数,但是可以将它挪到integrate.pyx执行文件中,经过编译之后速度会更快
from math import sin
def sin2(x):
return sin(x)**2
def integrate(double a, double b, f, int N=2000):
cdef:
int i
double dx = (b-a)/N
double s = 0
for i in range(N):
s += f(a+i*dx)
return s*dx
结果分析发现sin2函数需要很长的运行时间,那么有没有更快的方法呢?可以使用C标准苦衷额sin函数来进行加速, 只需要改进执行文件integrate.pyx中的import语句
from libc.math cimport sin
def sin2(x):
return sin(x)**2
def integrate(double a, double b, f, int N=2000):
cdef:
int i
double dx = (b-a)/N
double s = 0
for i in range(N):
s += f(a+i*dx)
return s*dx
可以看到,性能又有进一步的提升
使用cProfile和Cython的profile运行时间分析工具可以直接告诉我们哪些代码运行时间很长,需要进行改进。但是为了理解为什么有些函数运行很慢,Cython提供了compile-time-annotations. 运行时间分析工具和编译注释为我们提供优化cython代码的一种方法。
对于编译注释,一个直观理解是如果一行Cython代码产生和很多调用python/c的API, 那么很有可能这行代码操作和很多python对象从而消耗了大量的时间。反之,如果一行cython代码只编译成比较短的C代码而且不调用C API, 那么这行代码会很快。
cython编译器有一个选项–annotate(short form: -a), 让cython生成一个HTML文件来表示Cython的源代码,一般叫做代码注释。如果一行代码有很多的C API调用,那么该行代码是深黄的,如果一行代码没有C API调用,那么该行代码没有高亮。
重新考虑integrate.pyx文件
def integrate(a, b, f, N=2000):
dx = (b-a)/N
s = 0.0
for i in range(N):
s += f(a+i*dx)
return s * dx
可以用如下命令生成代码注释
$ cython --annotage integrate.pyx
可以打开看看生成的HTML文件。最后大家可以动手对integrate.pyx文件进行修改,用静态类型定义,然后重新生成代码注释,看看有什么变化。