迴圈不變式外提(loop-invariant code motion)
/ LICM /
想像一件你重複 1000 次的雜務,而在裡面你一直重算一個在各次重複間從不改變的事實——像每次蓋信封郵戳前都重查一次日期。顯然的修法是開始前算一次並重用。迴圈不變式外提就是編譯器精確地做這件事:把一個結果在各次迭代間不變的計算搬出迴圈,在迴圈前執行一次,而不是每次繞迴圈都做。
若一個計算在每次迭代都產生相同的值——它的輸入在迴圈內未被修改——它就是迴圈不變的。編譯器辨識這類計算,並把它外提到前置標頭(preheader),那是一個在迴圈開始前剛好執行一次的區塊。例如在 for (i = 0; i < n; i++) a[i] = b * c + i; 中,乘積 b * c 不依賴 i,卻每次都被無謂地重算;LICM 把 t = b * c 提到迴圈上方,本體變成 a[i] = t + i。這個分析仰賴證明被搬動程式碼的輸入在整個迴圈中確實是常數,且外提是安全的(例如不能把一個會出錯或有副作用的操作搬到「迴圈本來會執行零次時」也可能執行的地方)。
它重要在於:熱迴圈的本體是程式中最寶貴的地段;在那裡哪怕只移除一個操作都能回報 n 次。LICM 是 -O2 迴圈最佳化的標準部分,常與強度削減、共同子運算式消除合作。一個提醒:編譯器只能外提它能證明是不變且安全的東西。如果迴圈內的指標寫入可能別名輸入、或被呼叫的函式可能改變它們,這個值就無法被證明不變、會留在原地——弱的別名資訊是「明顯」可外提的計算卻被留在迴圈裡的常見原因。
for (i = 0; i < n; i++) a[i] = b * c + i; // b*c 每次迭代都重算 // LICM 之後: t = b * c; // 外提:在前置標頭算一次 for (i = 0; i < n; i++) a[i] = t + i;
b * c 不隨 i 改變,所以在迴圈前算一次,而非在迴圈內算 n 次。
外提需要「證明」不變性與安全性:如果迴圈內透過可能別名的指標的儲存、或一次函式呼叫可能改變輸入,編譯器會保守地把計算留在原地。