最近工作中的一些项目需要用到cython做加速,但网上都是一些零散的教程,唯一参考的文献是官方的文档和OReilly的书,但都是英文的,对于英文不好的同学可能阅读起来比较吃力。所以我个人花了一些时间,根据参考文献,系统的梳理了一下cython的用法,希望能够帮助到更多的同学。
Cython是一种拓展的python, 融合了静态类型的c/c++, 其文件扩展名为.pyx, 这种类型的文件经过编译之后可以变成供python直接调用的动态链接库(.so). Cython程序需要先编译之后才能被python调用,流程是:
Cython的作用在于它结合了python和c的优点。 python是高级语言,动态类型,很灵活,解释性语言,但是相比与静态类型的编译语言,速度慢好几个数量级。python代码在运行之前会转换为python字节码,通过python虚拟机将高级的字节码转化为低级别的操作,可以被cpu执行。这样做的好处是python代码可以不需要编译运行,但缺点就是相比于编译好的代码,运行速度比较慢。
C语言是广泛使用的静态编译语言,速度很快,但是相比与高级语言很难使用。C代码必须经过编译之后才可与被cpu直接运行,但是运行效率更高
为了加速python程序, Cython将python的一些变量定义为静态类型实现加速。
我们可以将cython的源代码编译成一个扩展模块让python调用。
def fib(n):
a, b = 0, 1
for i in range(n):
a, b = a + b, a
return a
def fib(int n):
cdef int i
cdef int a=0, b=1
for i in range(n):
a, b = a + b, a
return a
纯python的代码也是有效的cython代码,可以通过cython编译,但是纯python代码经过cython编译中性能提升不大。
我们使用cdef来声明静态类型变量。因为python是动态类型的编程语言,所以不能进行基于类型的优化。类似与a+b的表达式,在python中,需要要先推断a和b的类型,找到该类型下的__add__方法,然后在调用。而cython代码定义了变量的类型,因此不需要做类型检查,所以速度会更快。
大家可以动手尝试去跑一跑看看性能的提升。在此之前先看看cython代码如何编译
首先需要配置cython环境
$pip install cython
$cython -V
python的标准库中包含distutils packages, 用于打包python的项目, 它们可以将编译好的c代码打包成python的一个扩展模块。来看一个实际例子。
1 . 新建一个fib.pyx文件,包含如下代码:
def fib(int n):
cdef int i
cdef int a=0, b=1
for i in range(n):
a, b = a + b, a
return a
2 . 新建一个setup.py文件
from distutils.core import setup, Extension
from Cython.Build import cythonize
# name表示扩展模块的名称
ext = Extension(name='fib', source=['fib.pyx'])
setup(ext_modules=cythonize(ext))
cythonize()函数调用cython编译器编译.pyx源文件,setup()函数将编译好的c/c++代码打包成python拓展模块
3 . 进行编译
$ python setup.py build_ext --inplace
build_ext参数代表要构建一个扩展模块,–inplace参数代表将扩展模块放在.pyx文件夹下
编译好之后,我们可以看到在.pyx文件夹有一个.so文件,这个.so文件可以被python调用,我们通过如下代码观察cython的加速
import time
from functools import wraps
# 导入cython生成的.so文件
import fib
def time_wrapper(func):
@wraps(func)
def measure_time(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print(f"{func.__name__} took {t2 - t1} seconds")
return result
return measure_time
@time_wrapper
def fib_py(n):
a = 0
b = 0
for _ in range(n):
a, b = a + b, a
return a
@time_wrapper
def fib_c(n):
# 调用.so
return fib.fib(n)
if __name__ == "__main__":
n = 10000
fib_py(n)
fib_c(n)
"""
fib_py took 0.00047588348388671875 seconds
fib_c took 3.814697265625e-06 seconds
"""
可以看出,我们仅仅在pyx中声明了变量类型,就取得了几百倍的加速效果
cython一个主要特点是可以与外部的代码进行交互,包括python,c/c++代码。 因为cython可以理解c/c++的变量声明,可以与外部的库进行交互,所以可以生成高度优化的代码。看个例子:
1 . 新建头文件cfib.h
#ifndef __CFIB_H_
#define __CFIB_H_
double cfib(int n);
#endif
2 . 新建cfib.c完成函数体的定义
# include "cfib.h"
double cfib(int n ) {
int i ;
double a = 0.0, b = 1.0, tmp;
for (i=0; i<n; ++i) {
tmp = a; a = a + b; b = tmp;
}
return a
}
3 . 编译cython代码wrap_fib.pyx
cdef extern from "cfib.h":
double cfib(int n)
# 对外的接口
def fib(n):
return cfib(n)
4 . 编写setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
# name表示扩展模块的名称
ext = Extension(name='wrap_fib', source=['cfib.c', 'wrap_fib.pyx'])
setup(ext_modules=cythonize(ext))
同样的,在terminal运行如下的命令来生成.so文件, 可供python进行调用
# python setup.py build_ext --inplace
大家可以按照上述步骤进行实践,速度应该会提高狠多。
值得注意的是,当我们要提高python代码性能的时候,根据二八定律,80%的运行时间都是有20%的代码造成的,没有必要去优化所有的python代码,至于如何找到那20%的代码,后续的教程会介绍