Cython 最佳实践、约定和知识#

本文档介绍了在 scikit-learn 中开发 Cython 代码的技巧。

在 scikit-learn 中使用 Cython 进行开发的技巧#

简化开发的技巧#

  • 花时间阅读Cython 文档并非浪费时间。

  • 如果您打算使用 OpenMP:在 MacOS 上,系统的 clang 分发版未实现 OpenMP。您可以安装 conda-forge 上提供的 compilers 包,其中包含 OpenMP 的实现。

  • 启用检查 可能会有所帮助。例如,要启用 boundscheck,请使用

    export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
    
  • 从笔记本中从头开始 了解如何使用 Cython 并快速获得工作反馈。如果您计划在 Jupyter Notebook 中的实现中使用 OpenMP,请在 Cython 魔法命令中添加额外的编译器和链接器参数。

    # 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 代码库有很多未统一(融合)的类型(重新)定义。目前正在进行工作以简化和统一整个代码库中的这些类型。目前,请确保您了解最终使用的具体类型。

  • 您可能会发现这个编译单个 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=Truecython.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 is it were written in 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 库 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.pyd 中为最常用的类型提供了一个中心位置。理想情况下,您首先查看那里,并 cimport 您需要的类型,例如

from sklear.utils._typedefs cimport float32, float64