本章是系列文章的第六章,介紹了迴圈的分析方法。迴圈優化的邏輯相對簡單,但對效能提升的效果卻非常明顯。迴圈優化的分析還產生了一個圖靈獎。
本文中的所有內容來自學習DCC888的學習筆記或者自己理解的整理,如需轉載請註明出處。周榮華@燧原科技
對於下面的C程式碼,分析一下有幾重回圈?怎麼從控制流圖中定義迴圈?
1 #include <stdio.h> 2 int main(int argc, char **argv) { 3 int sum = 0; 4 int i = 1; 5 while (i < argc) { 6 char *c = argv[i++]; 7 while (*c != '\0') { 8 c++; 9 sum++; 10 } 11 } 12 printf("sum = %d\n", sum); 13 }
控制流圖的生成方法就不多說了,忘記的同學可以回過頭去看看第二章(2.1.3 LLVM),生成的svg圖如下:
控制流圖中的自然迴圈是具有下列屬性的節點的集合S:
編譯器中說的迴圈(loop)和拓撲意義上的環(cycle)是不同的。編譯器領域中的環只能有一個入口,多個入口的環在編譯器領域不叫做迴圈,因為絕大多數對迴圈的優化在多入口的環中都不適用。
多個入口的環在編碼過程中也非常罕見,所以也不是編譯器需要關心的場景。
如果對於邊(n1, n2),n1是n2的唯一前驅,或者n1和n2是強連通圖的一部分,可以用下面的方法簡化:
重複上述操作,直到控制流圖保持不變。
例如下面的控制流圖:
簡化流程是這樣的:
為什麼要簡化控制流圖:
節點d是節點n的支配節點,當且僅當所有從控制流圖入口到n的所有路徑都經過d。
D[s0] = {s0} D[n] = {n} ∪ (∩ p∈ pred[n]D[p]), for n ≠ s0
支配節點的計算:
每個階段n都 只有唯一一個直接支配節點idom(n),定義如下:
把每個節點的直接支配節點畫一條邊到該節點,就形成了圖的支配節點樹:
迴圈的頭節點h:在迴圈的節點集中,存在一個節點n,h是它的支配節點,並且存在邊(n, h)。
如果兩個迴圈的頭結點存在支配關係,則被支配的頭節點所在的迴圈稱為內迴圈,支配的頭節點所在的迴圈稱為外迴圈。
如果某個計算在迴圈的每次迭代中都產生同樣的值,則該計算時迴圈不變的。
迴圈不變表示式的通常優化方法是將該表示式提升到迴圈外。
滿足下面任意一條要求的表示式是迴圈不變表示式:
將回圈不變表示式提升到迴圈外的做法稱為程式碼提升。
在程式點d,如果滿足下面3個條件,可以對錶示式t = a + b 安全的進行程式碼提升:
將常規的while迴圈轉換成repeat-util迴圈的做法稱為迴圈倒置。倒置後的迴圈可以安全的進行不變程式碼提升。
repeat-utill迴圈在迴圈過程中每次迭代只需要進行一次跳轉,所以效能也比常規的while迴圈要好。
基本因變數(Basic induction variable):如果一個變數i在迴圈內部僅定義一次,並且每次定義都是在原有值基礎上增加或者減少迴圈不變數的值。
派生因變數(Derived induction variables):如果一個變數k在迴圈內部僅定義一次,並且k是一個因變數與迴圈不變數的乘積或者和。
i系列的派生因變數(a derived induction variable in the family of i):如果一個變數k定義中使用的因變數j僅定義一次,並且定義在迴圈內部,在j和k之間沒有i的定義。
將乘法運算換算成加法運算。例如下面的優化:
強度削減的演演算法基本上就是將派生因變數轉換成基本因變數。演演算法過程一般如下:
首先刪除的是j',因為k'已經完成了類似的功能:
由於i除了定義就只有和迴圈不變數的比較,所以實際上i也是可以刪除的:
刪除冗餘拷貝:
迴圈倒置:
初始版本和最終優化版本的對比:
迴圈展開是通過減少迴圈次數並增加回圈內部的計算來優化的一種方式。例如對下面的程式碼:
以2為因子進行迴圈展開之後的結果是這樣的: