在 SciPy 中添加 Cython#
如 Cython 网站 上所述
Cython 是一个针对 Python 编程语言和扩展的 Cython 编程语言(基于 Pyrex)的优化静态编译器。它使为 Python 编写 C 扩展变得像 Python 本身一样容易。
如果您的代码目前在 Python 中执行大量循环,那么它可能从使用 Cython 编译中获益。本文档旨在作为非常简短的介绍:足以了解如何在 SciPy 中使用 Cython。一旦您的代码编译完成,您可以通过查看 Cython 文档 来了解有关如何优化它的更多信息。
为了让 SciPy 使用 Cython 编译您的代码,您只需要做两件事
将您的代码包含在一个扩展名为
.pyx
的文件中,而不是扩展名为.py
的文件中。所有扩展名为.pyx
的文件在构建 SciPy 时会由 Cython 自动转换为.c
或.cpp
文件。将新的
.pyx
文件添加到代码所在的子包的meson.build
构建配置中。通常,已经存在其他.pyx
模式(如果没有,请查看另一个子模块),因此您可以参考示例来了解要添加到meson.build
中的具体内容。
示例#
scipy.optimize._linprog_rs.py
包含了用于 scipy.optimize.linprog
的修正单纯形法的实现。修正单纯形法对矩阵执行许多基本行操作,因此它是 Cython 化的自然选择。
请注意,scipy/optimize/_linprog_rs.py
从 ._bglu_dense
中导入 BGLU
和 LU
类,就像它们是普通的 Python 类一样。但事实并非如此。 BGLU
和 LU
是在 /scipy/optimize/_bglu_dense.pyx
中定义的 Cython 类。它们导入或使用的方式没有任何迹象表明它们是用 Cython 编写的;到目前为止,我们唯一能判断它们是 Cython 类的方法是它们是在扩展名为 .pyx
的文件中定义的。
即使在 /scipy/optimize/_bglu_dense.pyx
中,大多数代码也类似于 Python。最显著的区别是存在 cimport
、cdef
和 Cython 装饰器。这些都不是严格必要的。没有它们,纯 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 处理 - 生成一个扩展模块,我们可以从中导入和使用 LU
和 BGLU
类。
练习#
观看此练习的视频演示: Cythonizing SciPy Code
更新 Cython 并创建一个新分支(例如,
git checkout -b cython_test
),在其中对 SciPy 进行一些实验性更改。在
/scipy/optimize
目录中添加一些简单的 Python 代码,放在一个.py
文件中,例如/scipy/optimize/mypython.py
。例如def myfun(): i = 1 while i < 10000000: i += 1 return i
让我们看看这个纯 Python 循环需要多长时间,以便我们可以比较 Cython 的性能。例如,在 Spyder 中的 IPython 控制台中
from scipy.optimize.mypython import myfun %timeit myfun()
我得到类似的结果
715 ms ± 10.7 ms per loop
将您的
.py
文件保存到一个.pyx
文件中,例如mycython.pyx
。将
.pyx
添加到scipy/optimize/meson.build
中,方法与上一节中描述的方法相同。重新构建 SciPy。请注意,扩展模块(一个
.so
或.pyd
文件)已添加到build/scipy/optimize/
目录中。计时,例如,通过使用
python dev.py ipython
进入 IPython,然后from scipy.optimize.mycython import myfun %timeit myfun()
我得到类似的结果
359 ms ± 6.98 ms per loop
Cython 将纯 Python 代码的速度提高了约 2 倍。
这在整体方案中并没有太大的改进。为了了解原因,最好让 Cython 创建我们代码的“带注释”版本,以显示瓶颈。在终端窗口中,使用
-a
标志对您的.pyx
文件调用 Cythoncython -a scipy/optimize/mycython.pyx
请注意,这将在
/scipy/optimize
目录中创建一个新的.html
文件。在任何浏览器中打开.html
文件。文件中用黄色突出显示的行表示编译代码与 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 交互已消失。重新构建 SciPy,打开一个新的 IPython 控制台,并使用
%timeit
命令。
from scipy.optimize.mycython import myfun
%timeit myfun()
我得到类似于 68.6 ns ± 1.95 ns per loop
的结果。Cython 代码的运行速度比原始 Python 代码快约 1000 万倍。
在这种情况下,编译器可能优化了循环,直接返回最终结果。这种速度提升对于实际代码来说并不典型,但这个练习确实说明了当 Python 中存在大量底层操作时,Cython 的强大之处。