原文链接
加速你的PY代码,让我们来简单上手一波Cython呀~mp.weixin.qq.com导语
众所周知,Python是一种非常简单易上手的胶水语言(胶水语言的意思就是用来连接软件组件的程序设计语言,通常是脚本语言)。尽管近年来Python越来越火,也被各种吹捧,但它的执行速度始终逃不出被人所诟病的窘境。不过好在目前已经有不少较为成熟的解决方案来为Python提速,今天我们就来简单介绍并上手一下其中一种非常不错的解决方案,也就是Cython。废话不多说,让我们愉快地开始吧~
简介介绍
这里我们先来简单介绍一下Cython吧。显然,Cython也是一种编程语言,按照官方文档里的说法,它可以通过类似Python的语法来编写可以被Python调用的C扩展。从而,在保留Python快速开发的优点的同时,可以提升Python速度并方便地调用外部C库。
为了证明我没有在胡说八道,还是把文档里对应的段落截出来吧:
总结一下,利用Cython,我们可以:
- 利用Python的语法实现Python和C/C++的混合编程,从而可以提升Python的执行效率;
- 调用C/C++代码。
最后,扫个盲,请注意Cython≠CPython。CPython的意思是,用C语言开发的Python解释器。换句话说,就是Python只是一种编程语言,具体实现的时候,如果用C来实现,那就叫CPython,如果用Java来实现,那就叫Jython,如果用C#来实现,那就叫IronPython,如果用Python自己来实现,那就叫PyPy。
OK,大概了解了Cython是啥之后,接下来让我们来简单上手一波吧~
简单上手
0.环境声明
懒得连Linux了,所以本文中所有例子的运行环境为:
- 操作系统: windows10
- Python版本: 3.6.4
1.安装Cython库
只需要在命令行执行如下命令就OK啦:
pip install cython
当然对于Anaconda用户,你也可以执行如下命令安装:
conda install cython
2.写两段Python代码并测速
这里,我们先用Python来写两段简单的代码,并测试一下它的运行速度。第一段代码是:
# fibonacci.py
def fib(n):
if not isinstance(n, int):
raise ValueError('n is incorrect')
if n <= 1: return n
return fib(n-1) + fib(n-2)
显然,这段纯Python代码是用来计算斐波那契数列的,我们将它放在一个名为fibonacci.py的文件中。让我们来执行并测试一下它的运行时间吧:
接下来,我们再来写一段代码:
# matrixdot.py
import numpy as np
def dot(m1, m2):
if m1.shape[1] != m2.shape[0]:
raise ValueError('m1 and m2 dimension mismatch')
r = np.zeros((m1.shape[0], m2.shape[1]), dtype=np.float32)
for i in range(m1.shape[0]):
for j in range(m2.shape[1]):
s = 0
for k in range(m1.shape[1]):
s += m1[i, k] * m2[k, j]
r[i, j] = s
return r
显然,这是一段用来计算矩阵乘法的代码,我们将它放在matrixdot.py文件中。让我们也来测试一下它的执行时间吧:
3.简单的Cython加速
现在,我们来试试Cython是否可以有效地加速上面两个程序。对于第一段代码,我们直接新建一个fibonacci.pyx文件,里面的内容为:
# fibonacci.pyx
def fib(n):
if not isinstance(n, int):
raise ValueError('n is incorrect')
if n <= 1: return n
return fib(n-1) + fib(n-2)
即fibonacci.py和fibonacci.pyx中的内容完全一致。唯一不同的只是程序的扩展名发生了改变,这是因为Cython程序的扩展名是.pyx。那么如何来调用这个Cython程序呢?一般地,其流程为:
- 编译Cython代码,并生成对应的动态链接库;
- Python解释器载入动态链接库。
为了完成第一步,我们需要写一个setup_fib.py程序:
# setup_fib.py
from Cython.Build import cythonize
from distutils.core import setup, Extension
setup(
ext_modules=cythonize(Extension('fibonacci_cython',
sources=['fibonacci.pyx'],
language='c',
include_dirs=[],
library_dirs=[],
libraries=[],
extra_compile_args=[],
extra_link_args=[]))
)
简单解释一下:
(1) fibonacci_cython
生成的动态链接库名字。
(2) sources
需要包括的.pyx文件以及.c/.cpp文件。
(3) language
默认是c, 当然也可以是c++。
(4) include_dirs
相当于gcc的-I参数。
(5) library_dirs
相当于gcc的-L参数。
(6) libraries
相当于gcc的-l参数。
(7) extra_compile_args
相当于传给gcc的额外编译参数。
(8) extra_link_args
相当于传给gcc的额外链接参数。
想要进一步了解的可以阅读这个:
https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html
然后在终端执行如下命令把Cython程序编译成动态链接库:
python setup_fib.py build_ext --inplace
不出意外的话,可以看到当前目录多出来了两个文件:
上面那个红框是生成的C程序,下面那个是编译好了的动态链接库。换句话说,Cython编译器先把Cython代码编译成调用了Python源码的C/C++代码,然后再将生成的代码编译成动态链接库。
注意,我们这里使用的是Windows系统,对于Mac/Linux系统,生成的动态链接库扩展名为.so。
让我们再来测试一下代码的执行时间:
可以发现,我们在没有修改任何代码的情况下,仅仅将其编译成动态链接库,我们的代码就获得了5倍以上的提速。
利用相同的流程再来试试另外一段代码,新建matrixdot.pyx,内容与之前matrixdot.py一致。然后写一个setup_dot.py脚本:
# setup_dot.py
import numpy
from Cython.Build import cythonize
from distutils.core import setup, Extension
setup(
ext_modules=cythonize(Extension('matrixdot_cython',
sources=['matrixdot.pyx'],
language='c',
include_dirs=[numpy.get_include()],
library_dirs=[],
libraries=[],
extra_compile_args=[],
extra_link_args=[]))
)
编译后调用测试一下时间:
可以发现,对于稍微复杂一些的第二个程序(这里的复杂主要是指程序中的变量更多了),虽然执行时间有所下降,但效果并不显著(从1.18s降到了1.01s)。
4.添加类型注释
现在,我们来修改一下之前的fibonacci.pyx文件,为里面的变量添加类型注释:
# fibonacci.pyx
import cython
@cython.boundscheck(False)
@cython.wraparound(False)
cdef int _fib(int n):
if not isinstance(n, int):
raise ValueError('n is incorrect')
if n <= 1: return n
return fib(n-1) + fib(n-2)
def fib(n):
return _fib(n)
简单解释一下:
(1) @cython.boundscheck(False)和cython.wraparound(False)
关闭Cython的边界检查。
(2) cdef
定义函数, 并且可以给所有参数以及返回值指定类型。
(3) def
因为在python程序中, 我们是看不到cdef函数的, 所以我们这里要
再用def定义一个fib函数, 来调用cdef的_fib函数。
我们再来编译执行一下这个程序:
可以发现,对于这个简单的程序(这里的简单是指程序中的变量相对较少),添加了类型注释之后,执行的效率提升并不明显,但还是有提升的。接着再来修改我们的第二个程序:
# matrixdot.py
import numpy as np
cimport cython
cimport numpy as np
@cython.boundscheck(False)
@cython.wraparound(False)
cdef np.ndarray[np.float32_t, ndim=2] _dot(np.ndarray[np.float32_t, ndim=2] m1, np.ndarray[np.float32_t, ndim=2] m2):
cdef np.ndarray[np.float32_t, ndim=2] r
cdef int i, j, k
cdef np.float32_t s
if m1.shape[1] != m2.shape[0]:
raise ValueError('m1 and m2 dimension mismatch')
r = np.zeros((m1.shape[0], m2.shape[1]), dtype=np.float32)
for i in range(m1.shape[0]):
for j in range(m2.shape[1]):
s = 0
for k in range(m1.shape[1]):
s += m1[i, k] * m2[k, j]
r[i, j] = s
return r
def dot(m1, m2):
return _dot(m1, m2)
简单解释一下之前没有的内容:
(1) cimport
用来引入.pxd文件(相当于c/c++中的头文件)的命令。
(2) 在函数内部, 我们可以使用cdef typename varname这样的语法
来声明变量。
编译执行一下看看:
哇,我们可以发现程序的执行效率大概提升了300倍!!!
于是我们可以得出一个结论,在Cython中,类型声明对于提升程序执行效率至关重要。
5.Cython分析工具
既然类型声明对于Cython效率至关重要,那么如何检查我们的程序有没有漏加类型声明呢?我们可以在命令行执行如下命令:
cython -a matrixdot.pyx
可以看到当前目录会生成一个.html文件:
打开可以看到:
这里黄色标出的部分就是程序中拖累Cython性能的部分。如果不加声明的话,代码就全黄了:
当然,我们没有必要绞尽脑汁地把所有黄色部分去掉。实际上,我们只需要保证核心代码部分执行速度足够快就行了。不然就谈不上最开始说的用Cython是为了兼顾开发效率和执行效率了。
6.直接调用C代码
最后再来补充一下简单的Cython调用C代码的例子。我们把斐波那契数列那个代码写成C的形式(在cfib.c文件中):
#include "stdio.h"
static int cfib(int n){
if(n <= 1)
{
return n;
}
return cfib(n-1) + cfib(n-2);
}
修改一下fibonacci.pyx中的内容:
cdef extern from "cfib.c":
int cfib(int n)
def fib(n):
return cfib(n)
重新编译执行一下:
提升了多少倍我就不多说了,别老想着骗我写c/c++。
今天就先这样呗,有空我们再来聊聊怎么进一步上手Cython吧。相关文件我就不提供了,反正正文里都贴出来了。
参考文献
[1]. https://zhuanlan.zhihu.com/p/24311879
[2]. https://github.com/cython/cython
[3]. http://docs.cython.org/en/latest/index.html
[4]. https://cython.readthedocs.io/en/latest/index.html