背景
Python作为最方便的编程语言和丰富的配置而被大家推崇。 但是当我们的模型较复杂,运算量较大的时候,python的短板就会出现,执行速度并不那么理想,加上GIL的限制,让Python开发人员大为担忧,如何摆脱Python的这个短板而又不摒弃使用Python的快感呢?答案就是使用Cython。使用Cython,你可以避开Python的许多原生限制,或者完全超越Python,而无需放弃Python的简便性和便捷性。
Cython出现就是让Python也可以被编译,然后执行。大家要区别Cpython和Cython,Cpython大家可以认为是python的一种,其实大家平时使用的基本都是cpython。而Cython大家可以直接理解为一种语言,Cython是一种部分包含和改变C语言,以及完全包含pyhton语言的一个语言集合。
Cython可以在Python中掺杂C和C++的静态类型,cython编译器可以把Cython源码编译成C或C++代码,编译后的代码可以单独执行或者作为Python中的模型使用。Cython中的强大之处在于可以把Python和C结合起来,它使得看起来像Python语言的Cython代码有着和C相似的运行速度。
1. Cython 的简介和安装
Cython 是让 Python 脚本支持 C 语言扩展的编译器,Cython 能够将 Python + C 混合编码的 .pyx 脚本转换为 C 代码,主要用于优化Python脚本性能或Python调用C函数库。由于Python固有的性能差的问题,用C扩展Python成为提高Python性能常用方法,Cython 算是较为常见的一种扩展方式。
安装 Cython 的方法也是非常简单 直接使用 pip 工具即可直接在线安装。如下图:
Windows 和 Linux 下安装过程一样,这里展示一下在 Windows 下的安装命令(途中显示我已经安装过,版本号是 0.27.3)。后续的章节中实例实在虚拟机虚拟的 Ubuntu18.04 的 64 位的操作系统上运行的。运行的实际效果和各自的电脑有密切关系。
2. 将纯 Python 程序转换后的运行效率对比
首先编写一个测试程序来测试运行消耗的时长,这里实现了两个功能,一个是计算圆周率的功能,一个是函数的递归调用。具体实现代码如下:
# coding=utf-8
import math
def pi(n):
s = 0.0
for i in range(n+1):
s+= 1.0/(2*i+1)/(2*i+1)
return math.sqrt(8*s)
def flb(n):
if n == 0:
return 0
if n == 1:
return 1
return flb(n-1)+flb(n-2)
这两个功能一目了然,保存为 compute.py。
下面看一下测试这两个功能的测试程序 test.py,代码如下:
# coding=utf-8
import compute
import time
start_time = time.clock()
ret = compute.pi(20000000)
print(ret)
end_time = time.clock()
print ("pi time: %f s" % (end_time - start_time))
start_time = time.clock()
ret = compute.flb(40)
print(ret)
end_time = time.clock()
print ("flb time: %f s" % (end_time - start_time))
运行一下就知道这两个功能所消耗的时间了,结果如下:
计算圆周率循环 20000000 次花费时间 2.7s 左右。递归调用 40 次的时间是 29s 多。
下面我们将 compute.py 复制成 compute.pyx
然后编写一个 setup.py 的文件,如下:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
setup(
name = 'compute',
ext_modules=cythonize([
Extension("compute", ["compute.pyx"]),
]),
)
再编写一个 Makefile 文件 ,如下:
all:
python3 setup.py build_ext --inplace
clean:
@echo Cleaning compute
@rm -f compute.c *.o *.so
@rm -rf build
三个文件放在同一个目录下,然后运行 make 命令即可编译出一个 .so 的文件(当然你也可以不使用 Makefile 直接使用 python3 setup.py build_ext --inplace 也是可以的),如下图:
注意: 在 Windows 下生成的是一个 .pyd 的文件, Linux 下生成的是 .so 文件,这些文件可以直接被 Python 调用的。
同样运行 test.py 文件,效果如下:
从结果看,效果还是挺明显的,计算圆周率从 2.7 秒 到了 1.9 秒。递归调用从 29 秒到了 6 秒。不要骄傲,下面我们继续,还有提升的空间。
3. Cython 和 Python 混合编程的效率
上面仅仅是将原生的 Python 程序用 Cython 编译了一下效率就提高了不少。下面我们继续。
将上面那个 compute.pyx 文件稍作修改,如下:
# coding=utf-8
import math
cpdef double pi(int n):
cdef double s = 0.0
for i in range(n+1):
s+= 1.0/(2*i+1)/(2*i+1)
return math.sqrt(8*s)
cpdef int flb( int n):
if n == 0:
return 0
if n == 1:
return 1
return flb(n-1)+flb(n-2)
然后重新编译,然后运行 test.py ,效果如下:
通过对比可发现,计算圆周率的并没有很大的优化,但是 递归调用的那个所用的时间已经不到半秒钟了,从最初的 29 秒到现在的 0.36秒 可以说是一个非常棒的优化了。
4. 引入 C 库后的运行效率
从上面我们可以发现,计算圆周率的时候使用了 Python 的数学库 math ,现在我们使用 C 语言的 math.h 库试一试。将 compute.pyx 文件稍作修改,如下:
# coding=utf-8
cdef extern from "math.h":
double sqrt(double theta)
cpdef double pi(int n):
cdef double s = 0.0
for i in range(n+1):
s+= 1.0/(2*i+1)/(2*i+1)
return sqrt(8*s)
cpdef int flb( int n):
if n == 0:
return 0
if n == 1:
return 1
return flb(n-1)+flb(n-2)
然后使用 Makefile 编译一下,然后运行 test.py ,效果如下:
效果并没有太大改进嘛。也就可以说明在这个程度上 Python 中的数学库的 sqrt 和 C 语言的 sqrt 运行效率相当。下面我们继续优化之旅。
5. 自定义 C 语言编译成 Python 模块
如果我们完全使用 C 语言实现这两个功能运算效果会如何呢? 首先我们使用 C 语言实现这两个功能,代码如下( fun.c ):
#include "stdio.h"
#include "math.h"
static double pi_fun(int n){
double s = 0.0;
for (int i = 0; i < n+1; ++i)
{
s += 1.0/(2*i+1)/(2*i+1);
}
return sqrt(8*s);
}
static int flb_fun(int n){
if (n == 0){
return 0;
}
if (n == 1){
return 1;
}
return flb_fun(n-1)+flb_fun(n-2);
}
将 compute.pyx 做如下修改:
# coding=utf-8
cdef extern from "fun.c":
double pi_fun(int n)
int flb_fun(int n)
cpdef double pi(int n):
return pi_fun(n)
cpdef int flb( int n):
return flb_fun(n)
然后编译,运行 test.py ,效果如下:
这个结果还是挺喜人的,20000000 次的循环计算圆周率从先前的 2.7 秒到现在的 0.05 秒, 40 次的递归调用耗时从先前的 29 秒 优化到 0.36 秒;
所有源码都已经上传 GitHub (https://github.com/EricLmy/demo.git) 可以自行下载。
6. 如何巧妙的避开局限
Cython也是有局限的,毕竟Cython不是万能的。它不会自动将每一个 Python 代码变成极速的 C 代码。为了充分利用Cython,你必须明智地使用它,并理解它的局限性:
- 常规 Python 代码的加速很少
- 原生 Python 数据结构有一点加速
- Cython 代码运行速度最快的时候就是全部使用 C 实现