摘要: 《Python性能分析与优化》笔记,Cython 层面如何做 Python 的性能优化。第一部分
【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings
写在前面
这是 Python 性能分析与优化系列的第 10 篇文章,主要参考 Python性能分析与优化 这本书。
往期回顾
第一期: Python性能分析基础
第二期: Python性能分析器 — cProfile
第三期: 使用cProfile进行性能分析与优化实践
第四期: Python性能分析器 — line_profiler
第五期: line_profiler性能分析实践 — 优化倒排索引
第六期: Python性能分析-可视化
第七期: Python性能优化-优化每一个细节
第八期: Python性能优化-多进程与多线程
第九期: Python性能优化-PyPy
在前面若干期中,我们都是在优化 Python 众多实现中的一种(CPython)。Python 还有其他实现方式,在这一期我们将学习其中的两种。
- PyPy: 它是Python解释器的另一个版本。
- Cython: 它是一种优化过的静态编译器,可以让我们写静态代码,并轻松借助C/C++的力量。
本文主要围绕 Cython 这种方法。Cython 的内容是很庞大的,详细内容可以参考 Cython User Guide。这里只关注一些皮毛,看看是怎么用的。
0. Cython
Cython 并没有使用另一种与 CPython 不同的解释器,但可以让我们直接将 Python 代码编译成 C 语言。(Cython 是一个转换器)
Cython 把 Python 的超集翻译成 C/C++ 然后被编译成 Python 模块。
- 用 Python 代码调用原生 C/C++
- 用静态类型声明把 Python 代码优化成 C 语言的性能
静态类型是Cython这个翻译器产生优化的C语言代码的主要特征
注意:这样做代码会变得啰嗦,降低维护性和可读性。因此通常不推荐。
- 可以使用所有的C类型
- Cython可以对变量赋值自动进行类型转换
- 当面对Python的任意长度整数时,如果转换成C类型出现了栈溢出,Python的溢出错误就会产生。
1. 安装
1 | pip install cython |
2. 建立 Cython 模块
(1) 用 Cython 把 .pyx 文件编译成 .c 文件
(2) .c 文件被 gcc 编译成 .so 库,这个库之后可以导入 Python
例子1: 建立 Cython 模块时编译代码的几种方法
- my_pyx_module.pyx
1 | # cython: language_level=3 |
(1) 用 pyximport
用 pyximport,像导入 .py 文件一样导入 .pyx 直接使用
- my_test.py
1 | # cython: language_level=3 |
(2) cython 命令
运行 cython 命令将 .pyx 文件编译成 .c 文件。然后用 gcc 编译成库文件
1 | cython my_pyx_module.pyx |
生成 my_pyx_module.c
,然后编译为库文件
1 | gcc -shared -o my_pyx_module.so -fPIC my_pyx_module.c |
生成 my_pyx_module.so
,然后就可以在 python 文件中 import my_pyx_module
了。
(3) distutils
创建 distutils 配置文件,它是创建其它模块的工具,可以生成自定义的 C 语言编译文件
- setup.py
1 | from distutils.core import setup |
在上面的代码中,要被导出的代码在 .pyx 文件中。
setup.py 文件可以通过不同的参数调用 setup 函数。最后,它会调用 test.py 文件,文件中导入并使用了库文件。
编译命令如下
1 | python setup.py build_ext -inplace |
生成的结果如下,除了翻译(Cython化)代码,且用 gcc 把代码编译成库文件 my_pyx_module.cpython-36m-x86_64-linux-gnu.so。
1 | Compiling my_pyx_module.pyx because it changed. |
之后就可以在 python 代码中直接 import my_pyx_module
了。
例子2: Cython 的定义文件(pxd)和实现文件(pyx)
面对复杂的情况时,Cython通常都需要导入两类文件。
- 定义文件:文件扩展名.pxd,是其他Cython文件要使用的变量、类型、函数名称的C语言声明。
- 实现文件:文件扩展名.pyx,包括在.pxd文件中已经定义好的函数实现
定义文件中通常包括C类型声明、外部C函数或变量声明,以及模块中定义的C函数声明。
定义文件不包含任何C或Python函数的实现,也不包含任何Python类的定义或可执行代码行。
以下例子是文档 Sharing Declarations Between Cython Modules 的例子。
文件 dishes.pxd:
1 | cdef enum otherstuff: |
文件 restaurant.pyx
1 | # cython: language_level=3 |
之后可以从三种编译 pyx 的方法中选一种,然后 import restaurant 并调用 serve(),可以输出结果如下:
1 | 42 oz spam, filler no. 0 |
- 默认情况下,当运行cimport时,它会在搜索路径中查找同名模块的modulename.pxd文件。
- 无论定义文件何时改变,导入的文件都需要重新编译。Cythin.Build.cythonize 功能可以解决这个问题。
(3) cython 中定义 c 语言的变量, 类型 — cdef
cdef
可以用于声明 C 语言的变量(local/module-level)
1 | cdef int i, j, k |
cdef
可以用于声明 C 语言的struct
, union
和 enum
,相当于定义类型。
1 | cdef struct Grail: |
cdef 可以用于定义常量, 可以用匿名 enum 实现
1 | cdef enum: |
cdef 可以用于定义函数
1 | cdef int eggs(unsigned long l, float f): |
cdef 可以用于定义类,然后将其变成 Extension Types,它的行为与 Python class 很接近。
1 | cdef class Shrubbery: |
关于在 Cython 中定义和类型,参考 Language Basics
3. 调用外部的 C 语言
(1) 在 Cython(.pyx) 中使用定义在头文件(.h) 中的变量
头文件 spam.h
1 |
|
Cython 文件 my.pyx
1 | # cython: language_level=3 |
test.py
1 | import my |
cdef extern from "spam.h"
的作用:
- 它让Cython知道如何在生成的C语言代码中放入#include语句来引用我们需要使用的库。
- 它会防止Cython为这段代码中的声明生成任何C代码。
- 它会把代码中的所有声明看成cdef extern,表示那些声明是定义在其他地方。
Cython在任何时刻都不会读取头文件的内容。所以你还需要把头文件的内容进行重新声明。你只需要重新你需要的那些,不需要关心代码中的其他内容。例如,如果你在头文件中声明了一个成员很多的大结构体,你只需要重新声明你需要使用的成员。在编译时,C编译器会使用完整的结构体源代码。
(2) 在 Cython 中使用在头文件声明, C 文件中定义的 C 变量或函数
spam.h
1 | int extern spam_counter; |
spam.c
1 |
|
head.pxd:
1 | cdef extern from "spam.c": |
注:上面这部分内容也可以直接放到 my.pyx 中
my.pyx
1 | # cython: language_level=3 |
test.py
1 | import my |
(3) cython 中调用 c 标准库函数 — cimport
与 Python 一样,Cython 也可以调用已经编译的 C 函数与 C 语言交互。
对于某些标准 C 库,直接 cimport 即可。
例子: 在 cython 中使用标准 C 库
在 cython 中调用 math 的 sin 和 stdlib 的 atoi
my.pxd 代码如下
1 | # cython: language_level=3 |
setup.py 代码(直接用 cython 命令也可以)
1 | from distutils.core import setup, Extension |
在 python 中调用 f
1 | import my |
关于调用 C 语言的更多内容,参考Interfacing with External C Code