1. 程式人生 > >編譯原理之程式碼優化

編譯原理之程式碼優化

前面介紹完了詞法分析、語法分析和語義分析,以及各階段如何利用符號表來實現程式碼合理性確認以及程式碼地址拉鍊式回填等工作。編譯原理出於程式碼編譯的模組化組裝考慮,一般會在語義分析的階段生成平臺無關的中間程式碼,經過中間程式碼級的程式碼優化,而後作為輸入進入程式碼生成階段,產生最終執行機器平臺上的目的碼,再經過一次目的碼級別的程式碼優化(一般和具體機器的硬體結構高度耦合,複雜且不通用)。故而出於理解編譯原理的角度考慮,程式碼優化一般都是以中間程式碼級程式碼優化手段作為研究物件。


程式碼優化按照優化的程式碼塊尺度分為:區域性優化、迴圈優化和全域性優化。即
1. 區域性優化:只有一個控制流入口、一個控制流出口的基本程式塊上進行的優化;
2. 迴圈優化:對迴圈中的程式碼進行的優化;
3. 全域性優化:在整個程式範圍內進行的優化。

1. 常見的程式碼優化手段

常見的程式碼優化技術有:刪除多餘運算、合併已知量和複寫傳播,刪除無用賦值等。採用轉載自《編譯原理》教材中關於這些優化技術的圖例快速地展示下各優化技術的具體內容。

針對目的碼:

P := 0
for I := 1 to 20 do 
    P := P + A[I]*B[I] 

假設其翻譯所得的中間程式碼如下



1. 刪除多餘運算
分析上圖的中間程式碼,可以發現(3)和式(6)屬於重複計算(因為I並沒有發生變化),故而式(6)是多餘的,完全可以採用T4∶=T1代替。

2. 程式碼外提
減少迴圈中程式碼總數的一個重要辦法是迴圈中不變的程式碼段外提。這種變換把迴圈不變運算,即結果獨立於迴圈執行次數的表示式,提到迴圈的前面,使之只在迴圈外計算一次。針對改定的例子,顯然陣列A和 B的首地址在計算過程中並不改變,則作出的改動如下

3. 強度削弱
強度削弱的本質是把強度大的運算換算成強度小的運算,例如將乘法換成加法運算。針對上面的迴圈過程,每迴圈一次,I的值增加1T1的值與I保持線性關係,每次總是增加4。因此,可以把迴圈中計算T1值的乘法運算變換成在迴圈前進行一次乘法運算,而在迴圈中將其變換成加法運算。

4. 變換迴圈控制條件
IT1始終保持T1=4*I的線性關係,因此可以把四元式(12)的迴圈控制條件I≤20變換成T1≤80,這樣整個程式的執行結果不變。這種變換稱為變換迴圈控制條件。經過這一變換後,迴圈中I的值在迴圈後不會被引用,四元式(11)成為多餘運算,可以從迴圈中刪除。變換迴圈控制條件可以達到程式碼優化的目的。

5. 合併已知量和複寫傳播
四元式(3)計算4*I時,I必為1。即4*I的兩個運算物件都是編碼時的已知量,可在編譯時計算出它的值,即四元式(3)可變為T1=4,這種變換稱為合併已知量。

四元式(6)T1的值複寫到T4中,四元式(8)要引用T4的值,而從四元式(6)到四元式(8)之間未改變T4T1的值,則將四元式(8)改為T6∶=T5[T1],這種變換稱為複寫傳播。

6. 刪除無用賦值
(6)T4賦值,但T4未被引用;另外,(2)(11)對I賦值,但只有(11)引用I。所以,只要程式中其它地方不需要引用T4I,則(6)(2)(11)對程式的執行結果無任何作用。我們稱之為無用賦值,無用賦值可以從程式中刪除。至此,我們可以得到刪減後簡潔的程式碼

2. 基本塊內的區域性優化

1. 基本塊的劃分
  入口語句的定義如下:
  ① 程式的第一個語句;或者,
  ② 條件轉移語句或無條件轉移語句的轉移目標語句;
  ③ 緊跟在條件轉移語句後面的語句。
有了入口語句的概念之後,就可以給出劃分中間程式碼(四元式程式)為基本塊的演算法,
  其步驟如下:
  ① 求出四元式程式中各個基本塊的入口語句。
  ② 對每一入口語句,構造其所屬的基本塊。它是由該入口語句到下一入口語句(不包括下一入口語句),或到一轉移語句(包括該轉移語句),或到一停語句(包括該停語句)之間的語句序列組成的。
  ③ 凡未被納入某一基本塊的語句、都是程式中控制流程無法到達的語句,因而也是不會被執行到的語句,可以把它們刪除。

2. 基本塊的優化手段
由於基本塊內的邏輯清晰,故而要做的優化手段都是較為直接淺層次的。目前基本塊內的常見的塊內優化手段有:
1. 刪除公共子表示式
2. 刪除無用程式碼
3. 重新命名臨時變數 (一般是用來應對建立過多臨時變數的,如t2 := t1 + 3如果後續並沒有對t1的引用,則可以t1 := t1 + 3來節省一個臨時變數的建立
4. 交換語句順序
5. 在結果不變的前提下,更換代數操作(x∶=y**2是需要根據**運算子過載指數函式的,這是挺耗時的操作,故而可以用強度更低的x∶=y*y來代替
根據以上原則,對如下程式碼進行優化

t1 := 4 - 2
t2 := t1 / 2 
t3 := a * t2
t4 := t3 * t1
t5 := b + t4
 c := t5 * t5

給出優化的終版程式碼

   t1 := a + a
   t1 := b + t1
    c := t1 * t1

顯然程式碼優化的工作不能像上面那樣的人工一步步確認和遍歷,顯然必然要將這些優化工作公理化。而一般到涉及到資料流和控制流簡化的這種階段,都是到了圖論一展身手的時候。

3. DAG(無環路有向圖)應用於基本塊的優化工作
在DAG圖中,通過節點間的連線和層次關係來表示表示式或運算的歸屬關係:
① 圖的葉結點,即無後繼的結點,以一識別符號(變數名)或常數作為標記,表示這個結點代表該變數或常數的值。如果葉結點用來代表某變數A的地址,則用addr(A)作為這個結點的標記。
② 圖的內部結點,即有後繼的結點,以一運算子作為標記,表示這個結點代表應用該運算子對其後繼結點所代表的值進行運算的結果。
(注:該部分內容轉載自教材《編譯原理》第11章DAG無環路有向圖應用於程式碼優化)

DAG構建的流程如下

對基本塊的每一四元式,依次執行:
  1. 如果NODE(B)無定義,則構造一標記為B的葉結點並定義NODE(B)為這個結點;
  如果當前四元式是0型,則記NODE(B)的值為n,轉4。
  如果當前四元式是1型,則轉2.(1)。
  如果當前四元式是2型,則:(Ⅰ)如果NODE(C)無定義,則構造一標記為C的葉結點並定義NODE(C)為這個結點,(Ⅱ)轉2.(2)。
  2. 
  (1) 如果NODE(B)是標記為常數的葉結點,則轉2.(3),否則轉3.(1)。
  (2) 如果NODE(B)和NODE(C)都是標記為常數的葉結點,則轉2.(4),否則轉3.(2)。
  (3) 執行op B(即合併已知量),令得到的新常數為P。如果NODE(B)是處理當前四元式時 新構造出來的結點,則刪除它。如果NODE(P)無定義,則構造一用P做標記的葉結點n。置NODE(P)=n,轉4.。
  (4) 執行B op C(即合併已知量),令得到的新常數為P。如果NODE(B)或NODE(C)是處理當前四元式時新構造出來的結點,則刪除它。如果NODE(P)無定義,則構造一用P做標記的葉結點n。置NODE(P)=n,轉4.。
  3.
  (1) 檢查DAG中是否已有一結點,其唯一後繼為NODE(B),且標記為op(即找公共子表示式)。如果沒有,則構造該結點n,否則就把已有的結點作為它的結點並設該結點為n,轉4.。
  (2) 檢查DAG中是否已有一結點,其左後繼為NODE(B),右後繼為NODE(C),且標記為op(即找公共子表示式)。如果沒有,則構造該結點n,否則就把已有的結點作為它的結點並設該結點為n。轉4.。
  4.
  如果NODE(A)無定義,則把A附加在結點n上並令NODE(A)=n;否則先把A從NODE(A)結點上的附加識別符號集中刪除(注意,如果NODE(A)是葉結點,則其標記A不刪除),把A附加到新結點n上並令NODE(A)=n。轉處理下一四元式。

說著很複雜,下面看一個案例

(1) T0∶=3.14
(2) T1∶=2 * T0
(3) T2∶=R + r
(4) A∶=T1 * T2
(5) B∶=A
(6) T3∶=2 * T0
(7) T4∶=R + r
(8) T5∶=T3 * T4
(9) T6∶=R - r
(10) B∶=T5 * T6

其DAG圖的構建過程如下

通過DAG圖可以發現諸多的優化資訊,如重複定義、無用定義等,則根據上圖的DAG圖可以構建最後的優化程式碼序列

  (1) S1∶=R+r
  (2) A∶=6.28*S1
  (3) S2∶=R-r
  (4) B∶=A *S2
3.迴圈優化

根據上面基本塊的定義,我們將諸多基本塊組裝在一起,構建成程式迴圈圖,如針對下面這個例子
  (1) read x
  (2) read y
  (3) r∶=x mod y
  (4) if r=0 goto (8)
  (5) x∶=y
  (6) y∶=r
  (7) goto (3)
  (8) write y
  (9) halt

則按照上面基本塊的劃分,可以分成四個部分,四個部分的控制流分析可知可以得到一個迴圈圖

迴圈塊最主要的特點是隻有一個數據流和控制流入口,而出口可能有多個。迴圈優化的主要手段有:迴圈次數無關性程式碼外提、刪除歸納變數和運算強度削弱。關於這三種手段的理解可以藉助此前的描述進行類比,基本並無太多差異。