Bootstrap

Python使用Cython实例,速度提升150倍以上

听过很多人说python速度慢,难以用于正式项目。本文介绍运用Cython编程,令Python代码的运行速度能提升数倍至数百倍。

1. 什么是 Cython?

Cython是Python编程语言的一个超集,Cython 允许Python 与 C/C++风格代码混合编程,编译后可以象普通 python模块那样导入使用。Python解释器运行时遇到Cython代码则以静态类型方式调用。这样,在性能上就得到很大提升。

目前,有许多知名的Python库,如NumPy和Pandas已经使用Cython来提高性能。在实现项目开发过程中,用cProfile 分析项目性能慢时,通常会发现,瓶颈通常只是由个别函数或API造成的。将1个Python函数改写为Cython风格,通常只需要做很少改动就可以完成。

写Cython代码并不复杂。通常不需要学习C++指针、模板语法、STL标准库等知识,这样一来, Cython使用到的C++语法就很简单了,与添加了类型提示的Python语法很相似,只是形式上稍有不同而已。所以Python中应用Cython还是挺容易的,非常值得Python程序员尝试。

如果性能慢是由于API接口, I/O等造成,这个问题在所有编程语言都存在, 请用异步,多线程方式来优化。

Cython与 types使用上的区别:

  • 如果需要对Python代码加速,或者在python中使用C/C++源码,应使用 Cython;
  • 如果只是需要在python项目中使用已经编译好的C/C++库文件,使用 types 来封装更简单一些。 当然Cython也支持,但二者运行速度上没有区别,这种场景 types使用上更简单。

下面我将演示如何用cython来改写1个python函数,并编译为二进制文件,测试比较前后性能变化。

2. 用 Cython 编写1个函数

1) 安装 cython

建议使用Python3.9 以上版本

pip3 install cython

2) 先编写1个纯python函数

我们编写1个函数,求圆周率近似值,数学公式如下
在这里插入图片描述
下面代码使用纯python实现此计算,默认n = 10000000, 并导入cProfile 模块来计算各函数消耗时间。

# 文件名: pi.py
import cProfile

def recip_square(i):
    return 1. / i ** 2

def approx_pi(n=10000000):
    val = 0.
    for k in range(1, n + 1):
        val += recip_square(k)
    pi = (6 * val) ** .5
    print("Approximate value of pi is: ", pi)
    return pi

if __name__ == '__main__':
	# 计算PI, 并统计耗时
    cProfile.run('approx_pi(10000000)')

3)使用cython重写该函数

cython 编程就是在python中使用C类型来申明变量,要先申明再使用。允许导入 C++原码。

cython代码:

  • 头文件(.pxd),与 C语言的.h 头文件作用相同,主要包含变量与函数声明
  • 实现文件(.pyx/.py)
    .pxd头文件不是必须的,只要有.pyx就可以了。

.pyx文件中通常包含以下内容:

  • cimport 引入语句:用于从其它cython模块、pxd头文件中引入Cython函数、类\、变量等,也可以引入c++ STL类型,或者外部c/c++ 模块。当然也可以用 import 引入python模块。
  • Cython静态类型声明变量: cdef 开头,使用c类型声明变量, 允许使用指针类型(无须手工释放)
  • Cython函数, 以 cdef 开头的函数
  • 允许包含 python 代码,python所有语法在.pyx中都支持
  • 可以使用Cython自定义类
  • 控制语句、表达式,与python相同。

用 Cython 语法将上一节示例做简单的改写如下

# 文件名 cpi.pyx 
# cython: profile=True
cimport cython

@cython.profile(False)
cdef inline double recip_square(long long i):
    return 1.0 / (i * i)

def approx_pi(int n=10000000):
    cdef double val = 0.
    cdef int k
    for k in range(1, n + 1):
        val += recip_square(k)
    pi = (6 * val) ** .5
    print("Approximate value of pi is: ", pi)
    return pi

代码分析

下面这段代码, 用cython语法声明val, k 为静态类型

    cdef double val = 0.
    cdef int k

其中 double, int 是 C 语言浮点数、整数类型, cdef 表示使用静态编译

cdef inline double recip_square(long long i)

这条语句说明,这是1个 cython 函数。

定义与使用 cython 函数时注意事项:

  • cython函数以 cdef 开, 其函数风格与C函数基本相同, 须用C类型申明函数参数与返回值。
  • for 循环的循环变量对运行速度很较大影响,必须在for循环之前有Cython语法声明循环变量,速度至少能提高几十倍。
  • cdef 定义的cython函数,不能被外部python直接调用,.pyx文件用Python语法写1个调用Cython函数的包装器,供外部python调用。 本例中,approx_pi() 相当于 recip_square()包装器, 外部调用的是approx_pi() .
def approx_pi(int n=10000000):

approx_pi 函数名前面无 cdef ,表示这个函数是python函数,但其中包含cython语法定义的静态变量,在运行时会按python方式运行,解释器执行到 cdef double val = 0. 语句时,会从编译后的pyd动态链接库中调用C语句。

    for k in range(1, n + 1):
        val += recip_square(k)

这个代码块, 因为循环变量与调用函数都是Cython语法定义的,所以它的速度非常快。 函数的返回值 pi 变量是1个python变量。 但只调用了2次,因此对总体速度影响非常有限。

4) 编译 .pyx 文件

Cython 使用 Python 最通用的 setuptools 构建工具进行编译,但必须提前安装好C++编译器,linux上安装gcc, Windows 系统安装Visual Studio 或者minGW均可。

第1步,按setuptools 要求,编写setup.py构建脚本,setup.py构建的详细配置不赘述了,仅说明与cython相关的配置

# setup.py
from setuptools import setup
# 导入Cython 构建相关模块
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("cpi.pyx"),
)

其中 ext_modules=cythonize("cpi.pyx") ,告知setuptools ,将cpi.pyx 按c来编译。

第2步,执行编译

python setup.py build_ext --inplace

这命令会调用C++编译器将 .c文件编译为可执行文件,保存在当前目录下扩展名为.pyd文件( linux操作系统下编译为.so文件)。

3. 运行 cython 函数

1) 导入 cython 模块

编写1个python测试函数,导入编译后的cython模块, 用 cProfile 模块收集运行时间

# test_cpi.py
import cpi
import cProfile

cProfile.run('cpi.approx_pi()')

2) 运行cython 函数

执行 test_cpi.py,总耗时 0.042秒

python test_cpi.py
Approximate value of pi is:  3.1415925580959025
         5 function calls in 0.042 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.042    0.042 <string>:1(<module>)
        1    0.042    0.042    0.042    0.042 cpi.pyx:15(approx_pi)
        1    0.000    0.000    0.042    0.042 {built-in method builtins.exec}
        1    0.000    0.000    0.042    0.042 {cython_demo.cpi.approx_pi}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

4. 与纯python函数进行性能比较

1) 运行纯python函数

执行python函数,总耗时 6.287秒,

python pi.py
Approximate value of pi is:  3.1415925580959025
         10000005 function calls in 6.287 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)       
        1    0.000    0.000    6.287    6.287 <string>:1(<module>)
 10000000    3.944    0.000    3.944    0.000 pi.py:2(recip_square)
        1    2.343    2.343    6.287    6.287 pi.py:5(approx_pi)
        1    0.000    0.000    6.287    6.287 {built-in method builtins.exec} 
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

2) 比较结果

结果相比较, 6.287 / 0.042 = 150 倍
这个计算圆周率的函数,用cython语法简单改写后,性能提升了150倍。

5. 总结

从示例可以看出,密集运算的函数经过cython简单修改为用C类型申明后,并没有增加更多代码,但速度提升非常明显,而且我们不需要C++的IDE开发环境。
因此说,Cython是提升Python程序性能的最佳方式,因此值得深入学习,并在实际项目上运用。

使用cython的一些建议

  • cython语法相比python基本语法,学习难度略微增加,在理解原理后,多做几遍练习,就能熟悉掌握。
  • 通常只需要在项目的少数代码中使用cython, 主要是计算密集型函数或代码块。
  • 在I/O密集型函数中使用cytthon效果不明显,或者可能没什么效果,如http消息收发,大文件读写。通过异步、多线程、多进程等技术来优化。
  • 如果要深入挖掘Cython潜力,建议还是掌握C++指针、STL标准库主要数据类型Vector, List, map 等类型,结合pandans, numpy,在数值运算、图像处理中可以充分发挥Cython的性能优势。

Cython中文文档:[在线阅读(Gitee)](https://apachecn.gitee.io/cython-doc-zh/)
;