将 Cython 添加到 SciPy#

Cython 网站 所述:

Cython 是一个针对 Python 编程语言及其扩展的 Cython 编程语言(基于 Pyrex)的优化静态编译器。它使得为 Python 编写 C 扩展就像编写 Python 本身一样简单。

如果你的代码目前在Python中执行大量循环,使用Cython进行编译可能会带来好处。本文旨在做一个非常简短的介绍:足以了解如何将Cython与SciPy一起使用。一旦你的代码编译完成,你可以通过查阅 Cython文档 来了解更多关于如何优化它的信息。

要让 SciPy 使用 Cython 编译你的代码,你只需要做两件事:

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

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

示例#

scipy.optimize._linprog_rs.py 包含了 scipy.optimize.linprog 的修正单纯形法的实现。修正单纯形法在矩阵上执行许多基本行操作,因此它是一个自然的选择,可以被 Cython 化。

注意 scipy/optimize/_linprog_rs.py._bglu_dense 中导入 BGLULU 类,就像它们是常规的 Python 类一样。但它们不是。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 类。

练习#

查看此练习的视频演示: Cython化SciPy代码

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

  2. /scipy/optimize 目录下的 .py 文件中添加一些简单的 Python 代码,例如 /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. 计时,例如通过使用 python dev.py 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的强大之处。