死碼消除(dead-code elimination)
/ DCE /
如果你算出一個值卻從不使用它,算它就是浪費;如果一行程式碼永遠到不了,留著它毫無意義。死碼消除就是找出並移除這類工作的最佳化——結果從未被觀察的計算,以及永遠不會執行的程式碼。
被刪掉的其實是兩件不同的事,值得分清楚。資料流意義上的死碼是結果在任何被觀察的路徑上都從未被使用的計算:int t = a * b; 若 t 之後不再被讀,就是一個死儲存(dead store),於是乘法被移除。不可達程式碼是任何流程控制路徑都到不了的程式碼:if (0) { ... } 的本體,或 return 之後的任何東西。編譯器用分析證明這些——活躍性分析(liveness analysis)告訴它哪些值仍被需要,可達性告訴它哪些區塊會被進入。SSA 形式讓活躍性特別容易,因為每個值的單一定義與其使用都很明確。DCE 常常是其他 pass 之後的清潔隊:常數傳播把一個條件摺成 false,DCE 隨後刪除如今不可達的分支。
它重要在於:DCE 縮小程式碼、移除浪費的執行期工作,也是讓分層抽象變便宜的原因——你只在概念上「付費」的程式碼路徑,未使用時會被刪除。對系統程式設計師有一個尖銳的提醒:編譯器只有在能證明一個儲存「不可觀察」時才可以刪它。對 volatile 記憶體、記憶體映射的裝置暫存器、或其他執行緒或信號處理常式可能讀取之值的寫入,都是可觀察的,所以它「不」是死的、會被保留;這正是 volatile 與原子操作存在的原因。而未定義行為能以令你意外的方式讓程式碼變「死」——如果到達一個分支會是 UB,最佳化器可能把它當成不可達而刪除,包括一個安全檢查。
int f(int a, int b) { int unused = a * b; // 結果從未被讀 -> 死儲存,乘法被移除 if (0) return -1; // 不可達 -> 被刪除 return a + b; }
未使用的乘法與 if (0) 分支都消失了;只有 a + b 存活。
一個儲存只有在不可觀察時才是死的:對 volatile 或記憶體映射暫存器的寫入、或其他執行緒或信號處理常式可能讀取的值,都是可觀察的、會被保留;而 UB 能讓一個檢查看似「不可達」,於是 DCE 可能默默刪掉它。