将 Cython 添加到 SciPy#

正如 Cython 网站 所述

Cython 是一个优化型静态编译器,支持 Python 编程语言和扩展的 Cython 编程语言(基于 Pyrex)。它使得为 Python 编写 C 扩展像编写 Python 本身一样容易。

如果您的代码目前在 Python 中执行大量循环,它可能会受益于 Cython 的编译。本文档旨在作为非常简短的介绍:仅够您了解如何将 Cython 与 SciPy 一起使用。一旦您的代码能够编译,您就可以通过查阅 Cython 文档 来了解如何优化它。

您只需要做两件事,SciPy 就会用 Cython 编译您的代码

  1. 将您的代码包含在一个扩展名为 .pyx 的文件中,而不是 .py 扩展名。所有具有 .pyx 扩展名的文件在 SciPy 构建时都会被 Cython 自动转换为 .c.cpp 文件。

  2. 将新的 .pyx 文件添加到您的代码所在的子包的 meson.build 构建配置中。通常,已经存在其他 .pyx 模式(如果没有,请查看另一个子模块),因此有一个示例可以遵循,了解要添加到 meson.build 的确切内容。

示例#

scipy.optimize._linprog_rs.py 包含 scipy.optimize.linprog 的修正单纯形法实现。修正单纯形法对矩阵执行许多基本行操作,因此它是 Cython 化得天独厚的候选。

请注意,scipy/optimize/_linprog_rs.py 正如同普通的 Python 类一样,从 ._bglu_dense 导入 BGLULU 类。但它们不是。 BGLULU 是在 /scipy/optimize/_bglu_dense.pyx 中定义的 Cython 类。从它们被导入或使用的方式来看,没有任何迹象表明它们是用 Cython 编写的;到目前为止,我们唯一能判断它们是 Cython 类的方式是它们定义在一个扩展名为 .pyx 的文件中。

即使在 /scipy/optimize/_bglu_dense.pyx 中,大部分代码也与 Python 相似。最显著的区别是 cimportcdefCython 装饰器 的存在。这些都不是严格必需的。没有它们,纯 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 处理——生成一个我们将能够从中导入和使用 LUBGLU 类的扩展模块。

练习#

观看本练习的视频演示: Cythonizing SciPy Code

  1. 更新 Cython 并创建一个新分支(例如,git checkout -b cython_test),以便对 SciPy 进行一些实验性更改

  2. /scipy/optimize 目录中,将一些简单的 Python 代码放在一个 .py 文件中,例如 /scipy/optimize/mypython.py。例如:

    def myfun():
        i = 1
        while i < 10000000:
            i += 1
        return i
    
  3. 让我们看看这个纯 Python 循环需要多长时间,以便我们可以比较 Cython 的性能。例如,在 Spyder 的 IPython 控制台中

    from scipy.optimize.mypython import myfun
    %timeit myfun()
    

    我得到类似这样的结果:

    715 ms ± 10.7 ms per loop
    
  4. 将您的 .py 文件另存为 .pyx 文件,例如 mycython.pyx

  5. 按照上一节所述,将 .pyx 添加到 scipy/optimize/meson.build 中。

  6. 重新构建 SciPy。请注意,一个扩展模块(一个 .so.pyd 文件)已被添加到 build/scipy/optimize/ 目录中。

  7. 对其进行计时,例如通过使用 spin ipython 进入 IPython 然后

    from scipy.optimize.mycython import myfun
    %timeit myfun()
    

    我得到类似这样的结果:

    359 ms ± 6.98 ms per loop
    

    Cython 将纯 Python 代码加速了约 2 倍。

  8. 从整体上看,这并不是很大的改进。要了解原因,最好让 Cython 创建一个代码的“注释”版本,以显示瓶颈。在一个终端窗口中,使用 -a 标志调用 Cython 来处理您的 .pyx 文件

    cython -a scipy/optimize/mycython.pyx
    

    请注意,这会在 /scipy/optimize 目录中创建一个新的 .html 文件。在任何浏览器中打开该 .html 文件。

  9. 文件中的黄色高亮行表示已编译代码与 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 交互已经消失。

  10. 重新构建 SciPy,打开一个新的 IPython 控制台,然后使用 %timeit

from scipy.optimize.mycython import myfun
%timeit myfun()

我得到类似这样的结果:68.6 ns ± 1.95 ns per loop。Cython 代码的运行速度比原始 Python 代码快了大约 1000 万倍。

在这种情况下,编译器很可能优化掉了循环,直接返回了最终结果。这种速度提升对于实际代码来说并不常见,但这个练习确实说明了当替代方案是 Python 中大量的底层操作时,Cython 的强大之处。