當前位置:
首頁 > 知識 > 對於一個很複雜的常量表達式,編譯器會算出結果再編譯嗎?

對於一個很複雜的常量表達式,編譯器會算出結果再編譯嗎?

把常量表達式的值求出來作為常量嵌在最終生成的代碼中,這種優化叫做常量摺疊(constant folding)。題主的問題:


對於一個很複雜的常量表達式,編譯器會算出結果再編譯嗎?


抑或是很忠實地把這個表達式完全翻譯成機器碼,留給最終的程序去解決?

這種問題會有幾個維度。挑兩個來說說:


涉及的常量摺疊是否為語言規範所強制要求的。如果是的話,那麼符合規範的編譯器就一定要做這樣的常量摺疊。


如果不屬於上一條的情況,那麼這就是編譯器的實現質量的問題。一個編譯器可以自由選擇是否做常量摺疊(或其它常量相關)優化。同一個編譯器有可能可以配置在不同的優化層級上工作,或許有些在低優化層級沒有被常量摺疊的代碼,在高優化層級會被優化。


常量摺疊的概念自身很簡單。對此沒啥了解的同學可以先跳個老傳送門:Optimizing C++ Code : Constant-Folding

=========================================


語言規範要求做常量摺疊的情況


Java


例如說在Java語言中,下面這些情況都會被認為是語言規範所規定的編譯時常量表達式:


Java原始類型(byte、boolean、short、char、int、float、long、double)以及java.lang.String的字面量

聲明為static final的Java原始類型或java.lang.String類型的靜態欄位,並且帶有以常量表達式來初始化的情況(如果沒有初始化表達式或者初始化表達式不是常量表達式的話則不算)


聲明為final修飾的原始類型或java.lang.String類型的局部變數,並且帶有以常量表達式來初始化的情況(類似於上一條)


以常量表達式為操作數的算術和關係運算表達式,以及String拼接運算符( + )


所以,在Java中,下面的表達式都是語言規範強制要求的常量表達式:


然後下面的Foo類中的幾個聲明:

像這樣的情況,Java語言編譯器(例如javac、ECJ)在遇到相關的運算符時,必須要檢查其操作數是否都為常量表達式,如果是的話就必須在編譯時對該運算符做常量摺疊。


C#在語言規範里也有類似的規定。


當然,即便一個表達式從Java語言規範的角度看不是強制的常量表達式,編譯器還是有自由在語義允許的前提下把代碼優化為在編譯時求值。例如說,像下面的例子,目前主流的桌面/伺服器端JVM里的JIT編譯器都可以徹底優化掉:


可以被一些JIT編譯器優化到等價於:

C


C語言當然也有類似的規定。請參考Constant expressions。


最典型的,整型字面量、sizeof()運算符(當參數不是VLA時),以及各種算術、關係/比較運算符在所有操作數都是整型常量時,整個表達式都會算是整型常量表達式。所以像是說 40 + 1 + (4 > 3) 這個表達式就是一個整型常量表達式,必須在編譯時求值。


C語言里有些語言結構是要求操作數一定要是常量表達式的,例如說非VLA的數組聲明裡面指定數組長度的表達式。如果有局部變數聲明 int a[10 + 2]; ,而10 + 2被允許在運行時才求值的話,那麼這個數組的大小就要在運行時才知道,就變成一個VLA了。


C++


早期版本的C++的常量表達式的規定跟C語言是頗為相似的。加入模版支持後,通過模版元編程實現複雜的編譯時求值的技巧被人發掘出來,變得很有趣。


C++11開始支持的constexpr修飾符則進一步擴大了非模版語法下能表達的編譯時求值的語法結構的範圍,C++14、C++17都對此做了進一步擴展。


D語言


D語言也有跟C語言相似的常量表達式,此外它還特彆強調它的編譯器支持「CTFE」(Compile-time Funciton Evaluation),也就是說如果一個函數調用傳入的所有參數都是編譯時常量,而且這個函數滿足一些(不那麼多的)限制,那麼這個函數調用就會被編譯時求值。


放倆傳送門:


編譯器提供的特殊查詢支持


GCC有一個編譯器內建函數(builtin function),


可以檢測傳入的參數表達式是否為編譯器可以在編譯時做常量摺疊的表達式。請參考官方文檔的描述:


Other Builtins - Using the GNU Compiler Collection (GCC)


Clang也支持這個編譯器內建函數。


一個C語言的綜合例子


舉個綜合例子,來看看從語言規範上看並沒有要求在編譯時徹底求值,但編譯器自身通過優化實現了徹底求值的情況。


用Clang 3.0到3.9.1測個遍,這些版本的Clang在-O2下都可以把foo()完全靜態求值,變成等價於:


這裡沒有任何const修飾,也不依賴於諸如C++14開始支持的擴展版constexpr,全靠Clang編譯器自己決定做的優化而達到這樣的效果。其它主流的C/C++優化編譯器在-O3、/Ox之類的優化層級上對這個例子似乎都還不會把foo()徹底的靜態求值掉,例如說試了下GCC 4.4.7到6.0,然後ICC 13、17,然後MSVC 19,都做了一些優化但是沒有完全靜態求值。


編譯器中常量相關優化的極入門介紹


簡單說說這三種優化:


常量摺疊(constant folding)


常量傳播(constant propagation)


條件常量傳播(conditional constant propagation)及其改良版,稀疏條件常量傳播(sparse conditional constant propagation,SCCP)


1、常量摺疊


常量摺疊是編譯器里最基本最常見的優化,沒有之一。連很多基本上不做優化或者只做很簡單優化的編譯器都會實現常量摺疊,例如TCC(Tiny C Compiler)。


本文前面講的很多東西都是跟常量摺疊相關的。看似很直觀明了對不對?


簡單的常量摺疊確實是很簡單的。例如說,如果用C++寫一個類C語言的編譯器,在它做語法分析/構建AST的時候,可以做類似這樣的事情:


這樣,對於下面的表達式,


在做語法分析+構建AST的時候就可以將其常量摺疊為8了,簡略步驟是:


如果把完整的AST構造出來再做摺疊的話可能看起來更明顯一些:


可以很明顯地看到,這個AST上的兩個加法的操作數都是常量表達式。


但是上面的做法碰到稍微複雜那麼一丁點的情況就紗布了:


假如x是一個局部變數,不是常量表達式,那麼按照上面的做法,如果構造出完整AST的話會是:


雖然字面上看1 + 2是一個常量加法,但實際編譯器看到的結構卻是 (x + 1) + 2 而不是 x + (1 + 2) ,所以1和2並不是相鄰的。


可以看到,對下面的加法,x + 1由於x不是常量所以這個加法也不是常量表達式,然後對上面的加法由於左手邊操作數不是常量表達式,所以這個加法也不是常量表達式。於是上面的簡易常量摺疊就無法把這段代碼優化為 x + 3。


那要怎麼辦呢?很簡單,讓常量摺疊的邏輯多看一層就好了。就這個例子而言,對上面的加法,它應該看看其操作數是否是常量,如果不是的話該操作數是否有部分操作數是常量,是的話就根據加法的結合律來旋轉AST的結構並摺疊常量。


這就是一種模式匹配。但如果編譯器需要為優化去匹配太多的模式,實現起來就會很繁瑣。所以貫穿於優化編譯器里的一個思想就是代碼的規範化(canonicalize)——儘可能把多種等價形式的代碼規範化為一種統一的形式,這樣後續的模式匹配就只要匹配較少形狀的代碼了。


例如說,再稍微複雜一點的表達式:


對應的完整AST:


這個無論從源碼字面上還是從AST上看,常量都沒有緊挨在一起。所以要怎麼辦?


(順帶:TCC在遇到這樣的代碼時就無法把它常量摺疊為 x + 3,咳咳)


一種思路就是:在創建AST節點的時候,對於二元運算,在滿足交換律的情況下,總是把常量放在右手邊。所以就會有:


後面就跟上一個例子一模一樣了,不再贅述。


綜上,對這組簡單的常量摺疊例子,我們可以對AST定義下述改寫規則:


c1 + c2 => c3


其中c3的數值等於c1 + c2的數值


c + x => x + c


根據交換律把常量移到右手邊


(x + c1) + c2 => x + (c1 + c2)


向下看一層,將常量向右推


(x + c) + y => (x + y) + c


x + (y + c) => (x + y) + c


(x + c1) + (y + c2) => (x + y) + (c1 + c2)


重複應用這組改寫規則直到不能進一步改寫,我們就能把一串加法表達式中所有常量加法都儘可能向右邊推並且摺疊起來了。


常量摺疊還可以利用上某些算術恆等性,例如說 x + 0 == x ,x * 0 == 0 , x * 1 == x ,等等。這樣,即便參與運算的操作數只有一邊是常量(另一邊是不是常量都無所謂),利用這些特殊性質我們還是可以做常量摺疊。


2、常量傳播


3、條件常量傳播


(待續…洗澡前沒寫完,廢話還是寫太多了orz)

您的贊是小編持續努力的最大動力,動動手指贊一下吧!


本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 知乎 的精彩文章:

晚上坐飛機不應該是滿天星星么?為什麼實際上很少看到?
洗腳的時候腳放在水裡面,為什麼開始不覺得燙,而你的腳動一下就會感覺很燙?
把螃蟹在捆綁狀態下用蒸氣蒸死,而不是先殺死再烹飪,是否算是虐殺動物?
大火成岩省或者說溢流玄武岩現象是怎麼形成的?近年地學界的主流對此研究有沒有新的進展?
海拉細胞與其它癌細胞相比有什麼特殊之處?為什麼?

TAG:知乎 |

您可能感興趣

c語言中經常使用到的預處理編譯指令,你都知道嗎?
iOS 編譯過程的原理和應用
編譯和解釋的區別在哪?
你知道「編譯」與「解釋」的區別嗎?
小編閑談:區分「編輯器」與「編譯器」
Java代碼編譯和執行的整 個過程
美貌與智慧並存的女人:發明首批電腦編譯程序,一生獲無數榮譽
如何加快C加加代碼的編譯速度
狂拽炫酷吊炸天 黑客編譯器,5秒你也能擁有!
《知識分子》編譯小組再召集 | 美好的靈魂在此相遇
輕鬆理解C語言相關的編譯器gcc和g加加
Perl程序的編譯和執行
科學家成功「重新編譯」免疫系統,讓癱瘓老鼠再次行走
專註編譯速度,Twitter發布了一個實驗性Scala編譯器
全新研究解釋編譯大腦功能新突破
Ryzen與Intel處理器最大差別在這裡:後者編譯器更強
針對ADAS應用優化編譯程序
Echo編譯:你缺乏安全感嗎?看看月土相位就知道啦!
將git版本號編譯進程序