在 SciPy 中添加 Cython#

Cython 网站 上所述

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

如果您的代码目前在 Python 中执行大量循环,那么它可能从使用 Cython 编译中获益。本文档旨在作为非常简短的介绍:足以了解如何在 SciPy 中使用 Cython。一旦您的代码编译完成,您可以通过查看 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._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 类。

练习#

观看此练习的视频演示: 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. 计时,例如,通过使用 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 的强大之处。