Cython最佳实践、惯例和知识#

本文档包含在scikit-learn中开发Cython代码的技巧。

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

简化开发的提示#

  • 时间花在阅读 Cython's documentation 不是失去的时间。

  • 如果您打算使用BEP:在MacOS上,系统的分布 clang 不实现BEP。您可以安装 compilers 套餐可在 conda-forge 它随BEP的实现而附带。

  • 激活 checks 可能会有所帮助。例如,用于激活boundscheck用途:

    export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
    
  • Start from scratch in a notebook 了解如何使用Cython并快速获取工作反馈。如果您计划在Deliveryter Notebook中使用BEP进行实现,请在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代码(例如,segfault),请使用 gdb 与:

    gdb --ex r --args python ./entrypoint_to_bug_reproducer.py
    
  • 要访问某些可用的值来调试 cdef (nogil) 上下文,用途:

    with gil:
        print(state_to_print)
    
  • 请注意,Cython无法解析f字符串 {var=} 表达,例如

    print(f"{test_val=}")
    
  • scikit-learn代码库有很多非统一(融合)类型(重新)定义。目前有 ongoing work to simplify and unify that across the codebase .目前,请确保您了解最终使用哪些具体类型。

  • 您可能会发现这个别名可以很方便地编译各个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解释器的交互。欲了解更多信息,请参阅 this section of Cython's tutorial

    # This generates a HTML report (`source.html`) for `source.c`.
    cythonX --annotate source.pyx
    

性能提示#

  • 在CPython的上下文中了解GIL(它解决了哪些问题,它的局限性有哪些),并很好地了解Cython何时会映射到不与CPython交互的C代码,何时不会,以及何时不能(例如,与Python对象(包括函数)的交互)。在这方面, PEP073 提供了很好的概述、上下文和删除途径。

  • 确保您已停用 checks .

  • 总是更喜欢记忆视图,而不是 cnp.ndarray 如果可能的话:记忆视图是轻量级的。

  • 避免内存视图切片:在某些情况下,记忆视图切片可能成本高昂或具有误导性,我们最好不要使用它,即使在某些上下文中处理更少的维度会更好。

  • 装饰最终课程或方法 @final (this允许在需要时删除虚拟表)

  • 当有意义时,内联方法和函数

  • 如果有疑问,请阅读生成的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 if it were written in C.
    
        return 0
    

    该物品基于 this comment from Stéfan's Benhel

  • 可以通过中定义的接口直接调用BLAS例程 sklearn.utils._cython_blas .

使用OpenMP#

由于scikit-learn可以在不使用BEP的情况下构建,因此有必要保护对BEP的每次直接调用。

_openmp_helpers module, available in sklearn/utils/_openmp_helpers.pyx 提供受保护版本的OpenMP例程。要使用OpenMP例程,它们必须 cimported 来自此模块,而不是直接来自BEP库:

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 sklearn.utils._typedefs cimport float32, float64