将 Cython 添加到 SciPy#

正如 Cython 网站 上所写

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

如果您的代码当前在 Python 中执行大量循环,则通过 Cython 编译可能会受益。 本文档旨在做一个非常简短的介绍:足以了解如何在 SciPy 中使用 Cython。 一旦您的代码编译完成,您可以通过查看 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 语言扩展*只是*提高性能的调整。 当 SciPy 构建时,此 .pyx 文件会自动被 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 代码,然后 Meson 将生成的 C 文件视为 SciPy 中的任何其他 C 代码——生成一个扩展模块,我们将能够从中导入和使用 LUBGLU 类。

练习#

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

  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 标志在您的 .pyx 文件上调用 Cython

    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 的强大功能。