Cython 最佳实践、约定和知识#
本文档包含在 scikit-learn 中开发 Cython 代码的技巧。
在 scikit-learn 中使用 Cython 进行开发的技巧#
简化开发的技巧#
花时间阅读 Cython 的文档 是值得的。
如果您打算使用 OpenMP:在 MacOS 上,系统的
clang分发版本不支持 OpenMP。您可以安装conda-forge上提供的compilers包,该包附带 OpenMP 实现。激活 检查 可能有帮助。例如,要激活 boundscheck,请使用
export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
从 notebook 中从头开始,以了解如何使用 Cython 并快速获得反馈。如果您计划在 Jupyter Notebook 中使用 OpenMP 进行实现,请在 Cython magic 中添加额外的编译器和链接器参数。
# For GCC and for clang %%cython --compile-args=-fopenmp --link-args=-fopenmp # For Microsoft's compilers %%cython --compile-args=/openmp --link-args=/openmp
要调试 C 代码(例如段错误),请使用
gdb和gdb --ex r --args python ./entrypoint_to_bug_reproducer.py
要在
cdef (nogil)上下文中访问某些值进行调试,请使用with gil: print(state_to_print)
请注意,Cython 无法解析包含
{var=}表达式的 f-string,例如print(f"{test_val=}")
scikit-learn 代码库中有许多未统一(fused)的类型(重)定义。目前 正在努力简化和统一整个代码库中的类型。目前,请确保您了解最终使用了哪些具体类型。
您可能会发现这个编译单个 Cython 扩展的别名很方便
# You might want to add this alias to your shell script config. alias cythonX="cython -X language_level=3 -X boundscheck=False -X wraparound=False -X initializedcheck=False -X nonecheck=False -X cdivision=True" # This generates `source.c` as if you had recompiled scikit-learn entirely. cythonX --annotate source.pyx
使用此标志的
--annotate选项允许生成代码注释的 HTML 报告。此报告逐行指示与 CPython 解释器的交互。在算法的计算密集型部分,必须尽可能避免与 CPython 解释器交互。有关更多信息,请参阅 Cython 教程的这一部分# This generates a HTML report (`source.html`) for `source.c`. cythonX --annotate source.pyx
性能技巧#
了解 CPython 上下文中的 GIL(它解决了什么问题,有什么限制),并清楚地了解 Cython 何时映射到没有与 CPython 交互的 C 代码,何时不映射,以及何时不能映射(例如存在与 Python 对象的交互,包括函数)。在这方面,PEP073 提供了很好的概述、上下文和移除途径。
确保您已禁用 检查。
如果可能,始终首选 memoryview 而不是
cnp.ndarray:memoryview 是轻量级的。避免 memoryview 切片:在某些情况下,memoryview 切片可能代价高昂或产生误导,我们最好不要使用它,即使在某些上下文中处理较少的维度会更可取。
使用
@final修饰最终类或方法(这允许在需要时移除虚表)在有意义时内联方法和函数
如有疑问,如果可以,请阅读生成的 C 或 C++ 代码:“对于一行 Cython 代码,C 指令越少,间接寻址越少,就越好”是一个很好的经验法则。
nogil声明只是提示:将cdef函数声明为 nogil 意味着它们可以在不持有 GIL 的情况下被调用,但它在进入时不会释放 GIL。您必须通过显式地将nogil=True传递给cython.parallel.prange,或者使用显式上下文管理器来自己完成。cdef inline void my_func(self) nogil: # Some logic interacting with CPython, e.g. allocating arrays via NumPy. with nogil: # The code here is run as if it were written in C. return 0
此项基于 Stéfan Benhel 的此评论
可以通过
sklearn.utils._cython_blas中定义的接口直接调用 BLAS 例程。
使用 OpenMP#
由于 scikit-learn 可以在没有 OpenMP 的情况下构建,因此必须保护对 OpenMP 的每个直接调用。
sklearn/utils/_openmp_helpers.pyx 中提供的 _openmp_helpers 模块提供了 OpenMP 例程的受保护版本。要使用 OpenMP 例程,必须从该模块而不是直接从 OpenMP 库中 cimport 它们
from sklearn.utils._openmp_helpers cimport omp_get_max_threads
max_threads = omp_get_max_threads()
并行循环 prange 已经受到 Cython 的保护,可以直接从 cython.parallel 使用。
类型#
Cython 代码需要使用显式类型。这是您获得性能提升的原因之一。为了避免代码重复,我们在 sklearn/utils/_typedefs.pxd 中为最常用的类型设置了一个中心位置。理想情况下,您可以从那里开始查看,并 cimport 您需要的类型,例如
from sklearn.utils._typedefs cimport float32, float64