為 SciPy 新增 Cython#
如同 Cython 網站 上所述
Cython 是一個針對 Python 程式語言和擴展的 Cython 程式語言(基於 Pyrex)的優化靜態編譯器。它使得為 Python 撰寫 C 擴展就像 Python 本身一樣容易。
如果您的程式碼目前在 Python 中執行大量迴圈,則可能受益於使用 Cython 編譯。本文檔旨在作為一個非常簡短的介紹:僅足以了解如何在 SciPy 中使用 Cython。一旦您的程式碼可以編譯,您可以透過查看 Cython 文件 來了解更多關於如何優化它的資訊。
為了讓 SciPy 使用 Cython 編譯您的程式碼,您只需要做兩件事
將您的程式碼包含在具有
.pyx
擴展名的檔案中,而不是.py
擴展名。所有具有.pyx
擴展名的檔案都會在 SciPy 建置時自動由 Cython 轉換為.c
或.cpp
檔案。將新的
.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
匯入 BGLU
和 LU
類別,就像它們是常規 Python 類別一樣。但它們不是。BGLU
和 LU
是在 /scipy/optimize/_bglu_dense.pyx
中定義的 Cython 類別。它們的匯入或使用方式沒有任何表明它們是用 Cython 撰寫的;到目前為止,我們判斷它們是 Cython 類別的唯一方法是它們在具有 .pyx
擴展名的檔案中定義。
即使在 /scipy/optimize/_bglu_dense.pyx
中,大多數程式碼也類似於 Python。最顯著的差異是 cimport
、cdef
和 Cython 裝飾器 的存在。這些都不是絕對必要的。即使沒有它們,純 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 程式碼一樣處理 - 產生一個擴展模組,我們將能夠從中匯入和使用 LU
和 BGLU
類別。
練習#
觀看此練習的影片演練: Cythonizing SciPy Code
更新 Cython 並建立一個新分支(例如,
git checkout -b cython_test
),在其中對 SciPy 進行一些實驗性變更在
/scipy/optimize
目錄中的.py
檔案中新增一些簡單的 Python 程式碼,例如/scipy/optimize/mypython.py
。例如def myfun(): i = 1 while i < 10000000: i += 1 return i
讓我們看看這個純 Python 迴圈需要多長時間,以便我們可以比較 Cython 的效能。例如,在 Spyder 的 IPython 主控台中
from scipy.optimize.mypython import myfun %timeit myfun()
我得到類似這樣的結果
715 ms ± 10.7 ms per loop
將您的
.py
檔案另存為.pyx
檔案,例如mycython.pyx
。以前一節中描述的方式,將
.pyx
新增到scipy/optimize/meson.build
。重建 SciPy。請注意,擴展模組(
.so
或.pyd
檔案)已新增到build/scipy/optimize/
目錄。計時它,例如,透過使用
python dev.py ipython
進入 IPython,然後from scipy.optimize.mycython import myfun %timeit myfun()
我得到類似這樣的結果
359 ms ± 6.98 ms per loop
Cython 將純 Python 程式碼加速了大約 2 倍。
在整體方案中,這並不是很大的改進。為了了解原因,它有助於讓 Cython 建立程式碼的「註解」版本,以顯示瓶頸。在終端機視窗中,使用
-a
標誌在您的.pyx
檔案上呼叫 Cythoncython -a scipy/optimize/mycython.pyx
請注意,這會在
/scipy/optimize
目錄中建立一個新的.html
檔案。在任何瀏覽器中開啟.html
檔案。檔案中黃色標亮的行表示編譯後的程式碼和 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 交互已消失。重建 SciPy,開啟一個新的 IPython 主控台,然後
%timeit
from scipy.optimize.mycython import myfun
%timeit myfun()
我得到類似這樣的結果:68.6 ns ± 1.95 ns per loop
。Cython 程式碼的執行速度比原始 Python 程式碼快約 1000 萬倍。
在這種情況下,編譯器可能優化掉了迴圈,只是傳回了最終結果。這種速度提升對於真實程式碼來說並不典型,但當替代方案是在 Python 中進行許多底層操作時,這個練習肯定說明了 Cython 的強大功能。