自 Python 3.11 以來,我們一直在努力提高 Python 的速度,而且成果也很明顯。效能改進是實實在在的,這項工作還在繼續。
一種已有近 30 年曆史的語言的速度有如此顯著的提升,讓人感到耳目一新,也讓人感到驚訝。
然而天下沒有免費的午餐。
這些改進是經過深思熟慮的規劃和研究的結果。在實施開始之前,我們就已經設定了約 5 倍的加速目標。
Node.js 和 Java 等成熟語言早已使用過其中一些技術,也為人們提供了啟發。
在深入研究所使用的關鍵技術之前,讓我們首先回顧一下時間線,以全面瞭解效能改進:
1. 專業、自適應直譯器
Python VM 是一個基於堆疊的直譯器。從位元組碼中可以看出,要進行算術運算,它首先需要將變數推送到堆疊上,然後在堆疊上執行運算。
直譯器根據觀察到的模式將一個或多個指令替換為更高效的指令。我們稱之為更快的變體超級指令。
值得注意的是,Python 每次只專門處理一個位元組碼。
Python 不會嘗試理解應用程式正在做什麼的更廣泛背景上下文,以找到針對特定問題的最佳超級指令。
Python 的專業化更加有限,基於每個指令進行操作:
- 在基於堆疊的虛擬機器中,將值推送到堆疊和從堆疊彈出可能是最常見的操作。
加速:
- 在加速過程中,直譯器會識別可以從最佳化中受益的操作。
- 現代 CPU 具有專門的指令,可以高效處理相同型別的整數、浮點數甚至字串的算術運算。如果兩個運算元都是相同型別,直譯器將使用該指令的專門版本。
- 它將操作標記為自適應,這意味著如果觀察到一致的型別模式(多次執行),該指令可能會從未來執行中的專業化中受益。
這其中也存在一些權衡。
其中之一就是記憶體——每次特化都會帶來少量的記憶體成本,因為直譯器需要將執行時資訊與指令一起儲存。這是透過內聯快取來管理的。
內聯快取是動態管理的,以避免過度的記憶體消耗,以及當快取未命中頻繁發生時系統如何回退到通用執行。PEP還提供了與此過程相關的記憶體使用情況的詳細分析。
直譯器透過收集資料並根據該資訊進行位元組碼級調整來最佳化執行時的程式碼,從而加快執行速度!
2、更好的記憶體管理
更少的記憶體幾乎總是意味著更好的快取利用率,因此最佳化記憶體可以帶來複合效益。
在這次演講中,Mark Shannon 討論了他們如何減少基本 Python 物件的大小。由於 Python 中的所有內容都是物件,因此最小化 Python 物件的記憶體佔用幾乎總是有益的。演講詳細介紹了多年來如何實現這一目標。
總之,物件大小減少了約~75%,訪問變數的記憶體讀取量也減少了約~60%。
這個結果是我們之前提到的複合效應的一個真例項子:物件尺寸越小,訪問屬性時的間接定址就越少,從而進一步提高了效能。
減少記憶體仍然是首要任務,我們肯定會看到該領域的更多改進。Python 3.13 為垃圾收集 (GC) 週期引入了更好的啟發式方法,從而更有效地收集迴圈引用並減少 GC 暫停時間。
3. JIT
JIT 編譯器是3.13 中的一項實驗性功能, [url=https://peps.python.org/pep-0744/]PEP 744[/url]描述了其內部工作原理。
之前對專用自適應直譯器和每條指令的內聯快取的改進為 JIT 編譯鋪平了道路。現在,藉助 JIT,我們可以將專用程式碼編譯為機器程式碼。
機器程式碼翻譯過程使用一種稱為複製和修補的技術。它沒有執行時依賴項,但對 LLVM 有新的構建時依賴項。
在深入解釋這種複製和修補的含義之前,讓我們先退一步,從整體上來看一下 JIT 編譯器的工作原理:
Java、Node.js 和 Python 等解釋型語言對中間位元組碼進行操作。
- VM 執行此位元組碼,並在高效執行時分析器的幫助下識別程式碼中的熱點。然後,這些熱點被髮送到JIT(即時)編譯器,以編譯為機器碼。
- 例如,Java 虛擬機器 (JVM) 利用 LLVM 框架將這些熱點位元組碼轉換為機器程式碼,從而允許執行時切換到本機指令以實現更快的執行。
- 另一種方法稱為AOT(Ahead-Of-Time)編譯,也可用於補充 JIT 編譯。在 AOT 中,部分程式碼甚至在程式執行之前就已最佳化和編譯。由於沒有可用的執行時分析資料,AOT 依靠靜態程式碼分析和啟發式技術來預測潛在熱點。
另一方面,Python 採用一種獨特且略顯新穎的方法,稱為“複製和修補”。
有一篇論文詳細闡述了這項技術。
該概念類似於傳統的 JIT 編譯器:Python 執行跟蹤步驟來識別潛在熱點,然後將其轉換為機器程式碼。它不是動態編譯程式碼,而是將靜態預編譯的位元組碼複製到記憶體中並在執行時進行修補。這意味著在程式執行時,只有機器程式碼的特定部分會被即時修改 — — 這就是術語“複製和修補”的由來。
在這場富有洞察力的演講中,Brandt Bucher 完美地演示了複製和修補的底層工作原理。
- 選擇要最佳化的特定位元組碼,並將相應的 C 程式碼提取到單獨的檔案中,並進行修改以啟用執行時修補。
- 然後使用 LLVM 將修改後的檔案編譯為機器程式碼。
- 生成的機器程式碼被格式化為類似 shellcode結構的 C 標頭檔案。
- 最終產品是特定於平臺的可修補機器程式碼段,JIT 編譯器可以根據需要使用它。
這種方法有幾個好處:
- 無執行時依賴:使用複製和修補,不需要單獨的執行時編譯步驟,從而無需在直譯器中使用編譯器依賴項。
- 簡單性:此方法只需用 C 編寫模板 JIT 程式碼即可實現廣泛的平臺支援。Python 核心開發人員不必深入研究特定於平臺的彙編,從而使維護更容易,並且貢獻者更容易使用該方法。
- 效能:由於沒有全面的編譯步驟,因此開銷顯著減少。根據 Brandt 的演講,與傳統的 JIT 工具鏈相比,複製和修補方法可使程式碼生成速度提高約 100 倍,執行速度提高約 15%。
雖然它可能無法在所有情況下都與 LuaJIT 等手工製作的彙編 JIT 的效能相匹配,但它提供了相當的編譯速度,並且在某些基準測試中的執行速度僅慢約 35%。
這種方法還處於早期階段,但不可否認的是,它是一種有前途的、比實現成熟的 JIT 編譯器更可行的替代方法。
結果不言而喻。
來自官方發行說明(https://docs.python.org/3/whatsnew/3.11.html#faster-cpython):
使用 pyperformance 基準測試套件測量,在 Ubuntu Linux 上使用 GCC 編譯時,CPython 3.11 比 CPython 3.10 平均快 25%。根據您的工作負載,整體速度可能提高約 10-60%。 |
Python 3.13 引入了 JIT 編譯的初步基礎,但仍處於早期階段。
然而,隨著Python 3.14 的推出,JIT 有望顯著成熟,並帶來切實的效能改進。
還值得注意的是,這些效能改進不僅適用於純 Python 程式碼。NumPy 、Pandas 和其他流行庫等C 擴充套件也可以從更快的直譯器迴圈和減少的函式呼叫開銷中受益。
總而言之,升級到 Python 3.11 或 3.12 可以顯著提高效能,因此強烈建議在生產部署中使用這些版本。這一趨勢將延續到 Python 3.13 及更高版本。