Cython 最佳實務、慣例與知識#

本文檔提供在 scikit-learn 中開發 Cython 程式碼的技巧。

在 scikit-learn 中使用 Cython 開發的技巧#

簡化開發的技巧#

  • 花時間閱讀 Cython 的文件並非浪費時間。

  • 如果您打算使用 OpenMP:在 MacOS 上,系統發行的 clang 沒有實作 OpenMP。您可以安裝 conda-forge 上提供的 compilers 套件,其中包含 OpenMP 的實作。

  • 啟用檢查可能有幫助。例如,要啟用邊界檢查,請使用

    export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
    
  • 從筆記本從頭開始,以了解如何使用 Cython 並快速獲得對您工作的回饋。如果您計劃在 Jupyter 筆記本中使用 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 程式碼(例如,segfault),請使用 gdb 搭配

    gdb --ex r --args python ./entrypoint_to_bug_reproducer.py
    
  • 若要在 cdef (nogil) 環境中存取某些值以進行除錯,請使用

    with gil:
        print(state_to_print)
    
  • 請注意,Cython 無法解析帶有 {var=} 運算式的 f 字串,例如

    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=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 is it were written in C.
    
        return 0
    

    此項目基於 Stéfan’s Benhel 的這則評論

  • 可以透過 sklearn.utils._cython_blas 中定義的介面直接呼叫 BLAS 常式。

使用 OpenMP#

由於 scikit-learn 可以在不使用 OpenMP 的情況下建置,因此必須保護每個對 OpenMP 的直接呼叫。

_openmp_helpers 模組,可在 sklearn/utils/_openmp_helpers.pyx 中取得,提供受保護版本的 OpenMP 常式。若要使用 OpenMP 常式,它們必須從此模組 cimported,而不是直接從 OpenMP 程式庫中 cimported

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