為 SciPy 新增 Cython#

如同 Cython 網站 上所述

Cython 是一個針對 Python 程式語言和擴展的 Cython 程式語言(基於 Pyrex)的優化靜態編譯器。它使得為 Python 撰寫 C 擴展就像 Python 本身一樣容易。

如果您的程式碼目前在 Python 中執行大量迴圈,則可能受益於使用 Cython 編譯。本文檔旨在作為一個非常簡短的介紹:僅足以了解如何在 SciPy 中使用 Cython。一旦您的程式碼可以編譯,您可以透過查看 Cython 文件 來了解更多關於如何優化它的資訊。

為了讓 SciPy 使用 Cython 編譯您的程式碼,您只需要做兩件事

  1. 將您的程式碼包含在具有 .pyx 擴展名的檔案中,而不是 .py 擴展名。所有具有 .pyx 擴展名的檔案都會在 SciPy 建置時自動由 Cython 轉換為 .c.cpp 檔案。

  2. 將新的 .pyx 檔案新增到您的程式碼所在的子套件的 meson.build 建置配置中。通常,已經存在其他 .pyx 模式(如果沒有,請查看另一個子模組),因此有一個範例可以遵循,了解要將哪些確切內容新增到 meson.build 中。

範例#

scipy.optimize._linprog_rs.py 包含 scipy.optimize.linprog 的修訂單純形法的實作。_linprog_rs.py 修訂單純形法對矩陣執行許多基本列運算,因此自然而然地成為 Cython 化的候選者。

請注意,scipy/optimize/_linprog_rs.py._bglu_dense 匯入 BGLULU 類別,就像它們是常規 Python 類別一樣。但它們不是。BGLULU 是在 /scipy/optimize/_bglu_dense.pyx 中定義的 Cython 類別。它們的匯入或使用方式沒有任何表明它們是用 Cython 撰寫的;到目前為止,我們判斷它們是 Cython 類別的唯一方法是它們在具有 .pyx 擴展名的檔案中定義。

即使在 /scipy/optimize/_bglu_dense.pyx 中,大多數程式碼也類似於 Python。最顯著的差異是 cimportcdefCython 裝飾器 的存在。這些都不是絕對必要的。即使沒有它們,純 Python 程式碼仍然可以由 Cython 編譯。Cython 語言擴展*只是*為了提高效能而進行的調整。這個 .pyx 檔案會在 SciPy 建置時由 Cython 自動轉換為 .c 檔案。

剩下的唯一事情是新增建置配置,它看起來會像這樣

_bglu_dense_c = opt_gen.process('_bglu_dense.pyx')

py3.extension_module('_bglu_dense',
  _bglu_dense_c,
  c_args: cython_c_args,
  dependencies: np_dep,
  link_args: version_link_args,
  install: true,
  subdir: 'scipy/optimize'
)

當 SciPy 建置時,_bglu_dense.pyx 將由 cython 轉譯為 C 程式碼,然後產生的 C 檔案將被 Meson 視為 SciPy 中的任何其他 C 程式碼一樣處理 - 產生一個擴展模組,我們將能夠從中匯入和使用 LUBGLU 類別。

練習#

觀看此練習的影片演練: Cythonizing SciPy Code

  1. 更新 Cython 並建立一個新分支(例如,git checkout -b cython_test),在其中對 SciPy 進行一些實驗性變更

  2. /scipy/optimize 目錄中的 .py 檔案中新增一些簡單的 Python 程式碼,例如 /scipy/optimize/mypython.py。例如

    def myfun():
        i = 1
        while i < 10000000:
            i += 1
        return i
    
  3. 讓我們看看這個純 Python 迴圈需要多長時間,以便我們可以比較 Cython 的效能。例如,在 Spyder 的 IPython 主控台中

    from scipy.optimize.mypython import myfun
    %timeit myfun()
    

    我得到類似這樣的結果

    715 ms ± 10.7 ms per loop
    
  4. 將您的 .py 檔案另存為 .pyx 檔案,例如 mycython.pyx

  5. 以前一節中描述的方式,將 .pyx 新增到 scipy/optimize/meson.build

  6. 重建 SciPy。請注意,擴展模組(.so.pyd 檔案)已新增到 build/scipy/optimize/ 目錄。

  7. 計時它,例如,透過使用 python dev.py ipython 進入 IPython,然後

    from scipy.optimize.mycython import myfun
    %timeit myfun()
    

    我得到類似這樣的結果

    359 ms ± 6.98 ms per loop
    

    Cython 將純 Python 程式碼加速了大約 2 倍。

  8. 在整體方案中,這並不是很大的改進。為了了解原因,它有助於讓 Cython 建立程式碼的「註解」版本,以顯示瓶頸。在終端機視窗中,使用 -a 標誌在您的 .pyx 檔案上呼叫 Cython

    cython -a scipy/optimize/mycython.pyx
    

    請注意,這會在 /scipy/optimize 目錄中建立一個新的 .html 檔案。在任何瀏覽器中開啟 .html 檔案。

  9. 檔案中黃色標亮的行表示編譯後的程式碼和 Python 之間可能存在的交互,這會大大降低速度。標亮的強度表示交互的估計嚴重程度。在這種情況下,如果我們將變數 i 定義為整數,以便 Cython 不必考慮它作為一般 Python 物件的可能性,則可以避免大部分交互

    def myfun():
        cdef int i = 1  # our first line of Cython code
        while i < 10000000:
            i += 1
        return i
    

    重新建立註解的 .html 檔案顯示,大部分 Python 交互已消失。

  10. 重建 SciPy,開啟一個新的 IPython 主控台,然後 %timeit

from scipy.optimize.mycython import myfun
%timeit myfun()

我得到類似這樣的結果:68.6 ns ± 1.95 ns per loop。Cython 程式碼的執行速度比原始 Python 程式碼快約 1000 萬倍。

在這種情況下,編譯器可能優化掉了迴圈,只是傳回了最終結果。這種速度提升對於真實程式碼來說並不典型,但當替代方案是在 Python 中進行許多底層操作時,這個練習肯定說明了 Cython 的強大功能。