将 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._bglu_dense 中导入 BGLULU 类,就像它们是普通的 Python 类一样。但事实并非如此。 BGLULU 是在 /scipy/optimize/_bglu_dense.pyx 中定义的 Cython 类。导入或使用它们的方式没有任何迹象表明它们是用 Cython 编写的;到目前为止,我们判断它们是 Cython 类的唯一方法是它们是在扩展名为 .pyx 的文件中定义的。

即使在 /scipy/optimize/_bglu_dense.pyx 中,大多数代码也类似于 Python。最显着的区别是 cimportcdefCython 装饰器 的存在。这些都不是严格必要的。如果没有它们,Cython 仍然可以编译纯 Python 代码。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 代码,然后 Meson 会像对待 SciPy 中的任何其他 C 代码一样对待生成的 C 文件,从而生成一个扩展模块,我们可以导入该模块并使用来自它的 LUBGLU 类。

练习#

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

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

  2. /scipy/optimize 目录中的 .py 文件(例如 /scipy/optimize/mypython.py)中添加一些简单的 Python 代码。例如

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