背景

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,你必须明智地使用它,并理解它的局限性:

  1. 常规 Python 代码的加速很少
  2. 原生 Python 数据结构有一点加速
  3. Cython 代码运行速度最快的时候就是全部使用 C 实现