每種編程語言為了表現出色,并且實現卓越的性能,都需要大量編譯器級的優化。
一種著名的優化技術是“常量折疊
”(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 通過確保其通用代碼/計算過程可以處理常量表達式的求值,從而消除了編寫特殊函數來處理常量折疊的需要。
-
模塊
+關注
關注
7文章
2696瀏覽量
47436 -
字符串
+關注
關注
1文章
578瀏覽量
20508 -
編譯器
+關注
關注
1文章
1624瀏覽量
49109 -
python
+關注
關注
56文章
4793瀏覽量
84631
發布評論請先 登錄
相關推薦
評論