将 Cython 添加到 SciPy#
正如 Cython 网站 所述
Cython 是一个优化型静态编译器,支持 Python 编程语言和扩展的 Cython 编程语言(基于 Pyrex)。它使得为 Python 编写 C 扩展像编写 Python 本身一样容易。
如果您的代码目前在 Python 中执行大量循环,它可能会受益于 Cython 的编译。本文档旨在作为非常简短的介绍:仅够您了解如何将 Cython 与 SciPy 一起使用。一旦您的代码能够编译,您就可以通过查阅 Cython 文档 来了解如何优化它。
您只需要做两件事,SciPy 就会用 Cython 编译您的代码
将您的代码包含在一个扩展名为
.pyx的文件中,而不是.py扩展名。所有具有.pyx扩展名的文件在 SciPy 构建时都会被 Cython 自动转换为.c或.cpp文件。将新的
.pyx文件添加到您的代码所在的子包的meson.build构建配置中。通常,已经存在其他.pyx模式(如果没有,请查看另一个子模块),因此有一个示例可以遵循,了解要添加到meson.build的确切内容。
示例#
scipy.optimize._linprog_rs.py 包含 scipy.optimize.linprog 的修正单纯形法实现。修正单纯形法对矩阵执行许多基本行操作,因此它是 Cython 化得天独厚的候选。
请注意,scipy/optimize/_linprog_rs.py 正如同普通的 Python 类一样,从 ._bglu_dense 导入 BGLU 和 LU 类。但它们不是。 BGLU 和 LU 是在 /scipy/optimize/_bglu_dense.pyx 中定义的 Cython 类。从它们被导入或使用的方式来看,没有任何迹象表明它们是用 Cython 编写的;到目前为止,我们唯一能判断它们是 Cython 类的方式是它们定义在一个扩展名为 .pyx 的文件中。
即使在 /scipy/optimize/_bglu_dense.pyx 中,大部分代码也与 Python 相似。最显著的区别是 cimport、cdef 和 Cython 装饰器 的存在。这些都不是严格必需的。没有它们,纯 Python 代码仍然可以被 Cython 编译。Cython 语言扩展*仅仅*是为了提高性能而进行的调整。这个 .pyx 文件在 SciPy 构建时会自动被 Cython 转换为 .c 文件。
唯一剩下的是添加构建配置,它看起来会像这样
_bglu_dense_c = opt_gen.process('_bglu_dense.pyx')
py3.extension_module('_bglu_dense',
_bglu_dense_c,
c_args: cython_c_args,
dependencies: np_dep,
link_args: version_link_args,
install: true,
subdir: 'scipy/optimize'
)
当 SciPy 构建时,_bglu_dense.pyx 将被 cython 转译为 C 代码,然后这个生成的 C 文件会像 SciPy 中的任何其他 C 代码一样被 Meson 处理——生成一个我们将能够从中导入和使用 LU 和 BGLU 类的扩展模块。
练习#
观看本练习的视频演示: Cythonizing SciPy Code
更新 Cython 并创建一个新分支(例如,
git checkout -b cython_test),以便对 SciPy 进行一些实验性更改在
/scipy/optimize目录中,将一些简单的 Python 代码放在一个.py文件中,例如/scipy/optimize/mypython.py。例如:def myfun(): i = 1 while i < 10000000: i += 1 return i
让我们看看这个纯 Python 循环需要多长时间,以便我们可以比较 Cython 的性能。例如,在 Spyder 的 IPython 控制台中
from scipy.optimize.mypython import myfun %timeit myfun()
我得到类似这样的结果:
715 ms ± 10.7 ms per loop
将您的
.py文件另存为.pyx文件,例如mycython.pyx。按照上一节所述,将
.pyx添加到scipy/optimize/meson.build中。重新构建 SciPy。请注意,一个扩展模块(一个
.so或.pyd文件)已被添加到build/scipy/optimize/目录中。对其进行计时,例如通过使用
spin ipython进入 IPython 然后from scipy.optimize.mycython import myfun %timeit myfun()
我得到类似这样的结果:
359 ms ± 6.98 ms per loop
Cython 将纯 Python 代码加速了约 2 倍。
从整体上看,这并不是很大的改进。要了解原因,最好让 Cython 创建一个代码的“注释”版本,以显示瓶颈。在一个终端窗口中,使用
-a标志调用 Cython 来处理您的.pyx文件cython -a scipy/optimize/mycython.pyx
请注意,这会在
/scipy/optimize目录中创建一个新的.html文件。在任何浏览器中打开该.html文件。文件中的黄色高亮行表示已编译代码与 Python 之间可能存在的交互,这会大大减慢速度。高亮的强度表示交互的估计严重程度。在这种情况下,如果我们将变量
i定义为整数,就可以避免大部分交互,这样 Cython 就无需考虑它可能是一个通用 Python 对象的情况。def myfun(): cdef int i = 1 # our first line of Cython code while i < 10000000: i += 1 return i
重新创建注释的
.html文件显示,大部分 Python 交互已经消失。重新构建 SciPy,打开一个新的 IPython 控制台,然后使用
%timeit
from scipy.optimize.mycython import myfun
%timeit myfun()
我得到类似这样的结果:68.6 ns ± 1.95 ns per loop。Cython 代码的运行速度比原始 Python 代码快了大约 1000 万倍。
在这种情况下,编译器很可能优化掉了循环,直接返回了最终结果。这种速度提升对于实际代码来说并不常见,但这个练习确实说明了当替代方案是 Python 中大量的底层操作时,Cython 的强大之处。