概述
python为通过编译优化代码效率的方式提供了许多选项,包括纯粹的基于C的编译方式,比如Cython、 Shed Skin和 Pythran,凭借 Numba 的基于 LLVM 的编译方式,还有替代虚拟机的 PyPy,包含了一个内置的即时编译器(JIT)。
面向的提升对象
调用外部库(例如,正则表达式、字符串操作、调用数据库)的代码在编译后可能不会表现出任何速度提升。 I/O 密集型的程序同样不可能表现出明显的速度提升。因为这是机理问题。
类似地,如果你 Python 代码集中于调用向量化的 numpy ,那么在编译后就不大可能运行得更快。只有当被编译的代码主要是 Python(并且可能主要是循环)时才会运行得更快。
总结就是我们优化的主要目标是:循环
Pytho编译工具
-
提前编译工具( Cython、 Shed Skin、 Pythran)
-
“即时”编译工具( Numba、 PyPy)
区别:通过提前编译( AOT),会创建一个为你的机器定制的静态库。而对于即时编译,编译器在使用时只逐步编译用的部分代码,但这不适合处理短小频繁运行的脚本。
Cpython
Cython 是一个能把类型注解的 Python 转换为一个扩展编译模块的编译器。
Cpython编译优化案例:
- 首先编写三个文件
1、本程序为主测试(调用cpython优化模块)的文件。
-------------------------
# main.py
import time
import calculate # 与Cpython编译过程中setup.py里面定义的一致
zs = [i for i in range(10000)]
cs = [x**2 for x in range(10000)]
def calculate_z(maxiter, zs, cs):
"""Calculate output list using Julia update rule"""
output = [0] * len(zs)
for i in range(len(zs)):
n = 0
z = zs[i]
c = cs[i]
while n < maxiter and abs(z) < 2:
z = z * z + c
n += 1
output[i] = n
return output
def calc_pure_python(desired_width, max_iterations,zs,cs):
start_time = time.time()
output = calculate.calculate_z(max_iterations, zs, cs)
# output = calculate_z(max_iterations, zs, cs) Took 1.5417897701263428 seconds
end_time = time.time()
secs = end_time - start_time
print("Took", secs, "seconds")
calc_pure_python(500,10000000,zs,cs)
-------------------------
2、cpythonfunc.pyx是我们准备编译的CPU密集计算型函数;例如:
-------------------------
# cythonfn.pyx
def calculate_z(maxiter, zs, cs):
"""Calculate output list using Julia update rule"""
output = [0] * len(zs)
for i in range(len(zs)):
n = 0
z = zs[i]
c = cs[i]
while n < maxiter and abs(z) < 2:
z = z * z + c
n += 1
output[i] = n
return output
--------------------------
3、setup.py文件,定义了把 cythonfn.pyx (自己定义的名)转换为 calculate.so(自己定义的模块名) 的方法
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
setup(
cmdclass = {'build_ext': build_ext},
ext_modules = [Extension("calculate", ["cpythonfunc.pyx"])]
)
- 然后运行 setup.py 获得一个能够被导入的模块:
python setup.py build_ext --inplace
'''--inplace 参数让 Cython 在当前目录中构建编译模块,而不是在一个独立的构建
目录中。'''
注意:另外我们还可以在 cythonfn.pyx 中利用Cdef关键字在函数体内声明变量(函数顶部),因为增加原始C类型可以使得编译函数运行更快。
def calculate_z(int maxiter, zs, cs):
"""Calculate output list using Julia update rule"""
cdef unsigned int i, n
cdef double complex z, c
output = [0] * len(zs)
...
总结:CPython 的优化注意两部分:函数模块编译;加入Cdef类型可以使得优化更快。
Shed Skin
Shed Skin 是一个和 Python2.4~ 2.7 一起使用的实验性的把 Python 转为 C++的编译器。
实质上, Shed Skin 和 Cython 生成了相同的机器码,执行速度的差异归结于 Shed Skin 在一个独立的内存空间运行,还有就是
所需的拷进拷出数据的开销。硬币的另一面就是,使用 Shed Skin,你不必预先做注解工作,节省了相当多的时间。
总结就是:Shed Skin不一定比CPython快,是
所需的拷进拷出数据的开销。
并行化
与 Cython,Numba/pro,PyPy 一起,OpenMP 能够通过使用 prange(并行 range)操作符和给 setup.py增加-fopenmp 编译指令的方式加入进来。在一个 prange 循环中的工作就能够做到并行运行,因为我们禁止了全局解释器锁( GIL)。
Numba
Numba 是一个专用于 numpy 代码的即时编译器, 在运行时由 LLVM 编译器来编译。
怎么使用
from numba import jit
...
@jit()
def calculate_serial_purepython(*args,**kwargs)
可以看到,用法非常简单,导入包,在需要优化的计算型函数前面添加修饰器@jit(),其优化效果和cpython差不多。
问题在于:Numba第一次显得慢,之后再调用函数就快了。因为第一次编译过后,参数没变的话就继续用,就快了。
NumbaPro
Numba 的升级版本,更适合硬件,更快。
PyPy
PyPy 是一个 Python 语言的替代实现,包含了一个可跟踪的即时编译器。
PyPy能达到比CPython更快的性能。注意:PyPy不能处理numpy,特别慢。
回顾总结
一般情况下,可能最消耗 CPU 时间的代码行是下面这些:
• 在紧凑的内循环内。
• 解引用 list、 array 或者 np.array 这些项。
• 执行数学运算。
能用到的加速包:
Cpython、Numba/pro、Pypy、Openmp