.. _cython:
Cython 最佳实践、约定和知识
================================================
本文档提供了在 scikit-learn 中开发 Cython 代码的技巧。
在 scikit-learn 中使用 Cython 开发的技巧
-----------------------------------------------
简化开发的技巧
^^^^^^^^^^^^^^^^^^^^^^^^
* 花时间阅读 `Cython 的文档 `_ 绝不会浪费时间。
* 如果你打算使用 OpenMP:在 MacOS 上,系统分发的 ``clang`` 不支持 OpenMP。
你可以安装 ``conda-forge`` 上的 ``compilers`` 包,它自带 OpenMP 的实现。
* 激活 `检查 `_ 可能会有所帮助。例如,要激活边界检查,请使用:
.. code-block:: bash
export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
* `从零开始在笔记本中 `_ 了解如何使用 Cython 并快速获得反馈。
如果你计划在 Jupyter Notebook 中使用 OpenMP 实现,请在 Cython 魔法中添加额外的编译器和链接器参数。
.. code-block:: python
# 对于 GCC 和 clang
%%cython --compile-args=-fopenmp --link-args=-fopenmp
# 对于微软的编译器
%%cython --compile-args=/openmp --link-args=/openmp
* 要调试 C 代码(例如段错误),请使用 ``gdb`` :
.. code-block:: bash
gdb --ex r --args python ./entrypoint_to_bug_reproducer.py
* 要在 ``cdef (nogil)`` 上下文中访问某些值进行调试,请使用:
.. code-block:: cython
with gil:
print(state_to_print)
* 注意 Cython 无法解析带有 ``{var=}`` 表达式的 f-string,例如:
.. code-block:: bash
print(f"{test_val=}")
* scikit-learn 代码库中有许多非统一的(融合的)类型(重新)定义。
目前正在进行 `简化并统一整个代码库的工作
`_ 。
目前,请确保您了解最终使用了哪些具体类型。
* 您可能会发现这个别名对于编译单个 Cython 扩展很方便:
.. code-block::
# 您可能希望将这个别名添加到您的 shell 脚本配置中。
alias cythonX="cython -X language_level=3 -X boundscheck=False -X wraparound=False -X initializedcheck=False -X nonecheck=False -X cdivision=True"
# 这将生成 `source.c` ,就像您重新编译了整个 scikit-learn 一样。
cythonX --annotate source.pyx
* 使用这个标志的 ``--annotate`` 选项可以生成代码注释的 HTML 报告。
该报告按行指示与 CPython 解释器的交互。
在算法的计算密集型部分,应尽可能避免与 CPython 解释器的交互。
更多信息,请参阅 `Cython 教程的这一部分 `_
.. code-block::
# 这将生成 `source.c` 的 HTML 报告 ( `source.html` )。
cythonX --annotate source.pyx
性能优化提示
^^^^^^^^^^^^
* 了解 CPython 中的 GIL(它解决的问题,它的局限性)
并深入理解 Cython 何时会映射到没有与 CPython 交互的 C 代码,何时不会,以及何时不能(例如,存在与 Python 对象的交互,包括函数)。
在这方面, `PEP073 `_ 提供了很好的概述和上下文以及移除的路径。
* 确保已停用 `检查 `_ .
* 尽可能优先使用 memoryviews 而不是 ``cnp.ndarray`` :memoryviews 是轻量级的。
* 避免使用 memoryview 切片:memoryview 切片在某些情况下可能代价高昂或产生误导,
我们最好不要使用它,即使在某些上下文中处理较少的维度会更可取。
* 使用 ``@final`` 装饰最终的类或方法(这允许在需要时移除虚表)
* 在有意义的情况下内联方法和函数
* 如有疑问,如果可以的话,阅读生成的 C 或 C++ 代码:“Cython 代码行对应的 C 指令和间接操作越少越好”是一个不错的经验法则。
* ``nogil`` 声明只是提示:当将 ``cdef`` 函数声明为 nogil 时,意味着它们可以在不持有 GIL 的情况下被调用,但这并不在进入它们时释放 GIL。你必须自己处理,要么通过显式传递 ``nogil=True`` 给 ``cython.parallel.prange`` ,要么使用显式的上下文管理器:
.. code-block:: cython
cdef inline void my_func(self) nogil:
# 一些与 CPython 交互的逻辑,例如通过 NumPy 分配数组。
with nogil:
# 这里的代码如同直接用 C 编写一样运行。
return 0
此项基于 `Stéfan 的 Benhel 的这条评论 `_
* 可以通过 ``sklearn.utils._cython_blas`` 中定义的接口直接调用 BLAS 例程。
使用 OpenMP
^^^^^^^^^^
由于 scikit-learn 可以在不使用 OpenMP 的情况下构建,因此需要保护每个直接调用 OpenMP 的操作。
`_openmp_helpers` 模块,可在
`sklearn/utils/_openmp_helpers.pyx `_ 中找到
提供受保护版本的 OpenMP 例程。要使用 OpenMP 例程,必须从该模块中 ``cimport`` ,而不是直接从 OpenMP 库中导入:
.. code-block:: cython
from sklearn.utils._openmp_helpers cimport omp_get_max_threads
max_threads = omp_get_max_threads()
并行循环 `prange` 已经受到 Cython 的保护,可以直接从 `cython.parallel` 中使用。
类型
~~~~
Cython 代码需要使用显式类型。这是你获得性能提升的原因之一。为了避免代码重复,我们在 `sklearn/utils/_typedefs.pyd `_ 中集中了最常用的类型。理想情况下,你首先查看那里并 `cimport` 你需要的类型,例如
.. code-block:: cython
from sklear.utils._typedefs cimport float32, float64