Python優化機制:常量摺疊

英文://arpitbhayani.me/blogs/constant-folding-python

作者:arprit

譯者:豌豆花下貓(「Python貓」公眾號作者)

聲明:本翻譯是出於交流學習的目的,基於 CC BY-NC-SA 4.0 授權協議。為便於閱讀,內容略有改動。

每種程式語言為了表現出色,並且實現卓越的性能,都需要大量編譯器級的優化。

一種著名的優化技術是「常量摺疊」(Constant Folding):在編譯期間,編譯器會設法識別出常量表達式,對其進行求值,然後用求值的結果來替換表達式,從而使得運行時更精簡。

在本文中,我們深入探討了什麼是常量摺疊,了解了它在 Python 世界中的適用範圍,最後解讀了 Python 的源程式碼(即 CPython),並分析出 Python 是如何優雅地實現它。

常量摺疊

所謂常量摺疊,指的是在編譯時就查找並計算常量表達式,而不是在運行時再對其進行計算,從而會使運行時更加精簡和快速。

>>> day_sec = 24 * 60 * 60

當編譯器遇到一個常量表達式時,如上所述,它將對表達式求值,並作替換。

通常而言,表達式會被「抽象語法樹」( Abstract Syntax Tree,簡寫為 AST)中的計算值所替換,但是這完全取決於語言的實現。

因此,上述表達式可以等效地被執行為:

>>> day_sec = 86400

Python 中的常量摺疊

在 Python 中,我們可以使用反彙編模組(Disassembler)獲取 CPython 位元組碼,從而更好地了解程式碼執行的過程。

當使用dis模組反彙編上述常量表達式時,我們會得到以下位元組碼:

>>> import dis
>>> dis.dis("day_sec = 24 * 60 * 60")

        0 LOAD_CONST               0 (86400)
        2 STORE_NAME               0 (day_sec)
        4 LOAD_CONST               1 (None)
        6 RETURN_VALUE

從位元組碼中可以看出,它只有一個LOAD_CONST ,以及一個已經計算好的值86400

這表明 CPython 解釋器在解析和構建抽象語法樹期間,會摺疊常量表達式 24 * 60 * 60,並將其替換為計算值 86400。

常量摺疊的適應範圍

Python 會嘗試摺疊每一個常量表達式,但在某些情況下,即使該表達式是常量,但是 Python 並不會對其進行摺疊。

例如,Python 不會摺疊x = 4 ** 64,但會摺疊 x = 2 ** 64

除了算術表達式,Python 還會摺疊涉及字元串和元組的表達式,其中,長度不超過 4096 的字元串常量表達式會被摺疊。

>>> a = "-" * 4096   # folded
>>> a = "-" * 4097   # not folded
>>> a = "--" * 4096  # not folded

常量摺疊的內部細節

現在,我們將重點轉移到內部的實現細節,即關注 CPython 在哪裡以及如何實現常量摺疊。

所有的 AST 優化(包括常量摺疊)都可以在 ast_opt.c 文件中找到。基本的開始函數是 astfold_expr,它會摺疊 Python 源碼中包含的所有表達式。

這個函數以遞歸方式遍歷 AST,並試著摺疊每個常量表達式,如下面的程式碼片段所示:

astfold_expr 在摺疊某個表達式之前,會嘗試摺疊其子表達式(操作對象),然後將摺疊操作代理給特定的表達式摺疊函數。

特定操作的摺疊函數對表達式求值,並返回計算後的常數,然後將其放入 AST 中。

例如,每當 astfold_expr 遇到二值運算時,它便調用 fold_binop,遞歸地計算兩個子操作對象(表達式) 。

fold_binop 函數返回計算後的常量值,如下面的程式碼片段所示:

fold_binop 函數通過檢查當前運算符的種類,然後調用其相應的處理函數來摺疊二值運算。例如,如果當前的操作是加法運算,為了計算最終值,它會對其左側和右側操作數調用 PyNumber_Add。

怎樣優雅?

為了有效地摺疊某些模式或類型的常量表達式,CPython 不會寫特殊的邏輯,而是調用相同的通用程式碼。例如,在摺疊時,它會調用通用的 PyNumber_Add 函數,跟執行常規的加法操作一樣。

因此,CPython 通過確保其通用程式碼/計算過程可以處理常量表達式的求值,從而消除了編寫特殊函數來處理常量摺疊的需要。

參考材料