編譯原理入門篇|一篇文章理解編譯全過程

編譯過程

編譯目標

目標:把源代碼變成目標代碼前端

1.若是源代碼在操做系統上運行:目標代碼就是「彙編代碼」。再經過彙編和連接的過程造成可執行文件,而後經過加載器加載到操做系統執行。算法

2.若是源代碼在虛擬機(解釋器)上運行:目標代碼就是「解釋器能夠理解的中間形式的代碼」,好比字節碼(中間代碼)IR、AST語法樹。數據結構

編譯過程能夠分爲這幾個階段,每一個階段作了必定的任務,層級的讓下一個階段進行。
架構

詞法分析

編譯器讀入源代碼,通過詞法分析器識別出Token,把字符串轉換成一個個Token。
Token的類型包括:關鍵字、標識符、字面量、操做符、界符等函數

好比下面的C語言代碼源文件,通過詞法分析器識別出的token有:int、foo、a、b、=、+、return、(){}等token工具

int foo(int a){
    int b = a + 3;
    return b;
}

爲何要這樣作呢,把代碼裏的單詞進行分類,編譯器後面的階段不就更好處理理解代碼了嘛!性能

語法分析

每個程序代碼,實際上能夠經過樹這種結構表現出其語法規則。優化

語法分析階段把Token串,轉換成一個體現語法規則的、樹狀數據結構,即抽象語法樹AST。
AST樹反映了程序的語法結構。操作系統

好比下面對應的一段C語言代碼,對應的AST抽象語法樹以下所示:翻譯

int foo(int a){
    int b = a + 3;
    return b;
}


AST抽象語法樹

AST樹長成什麼樣,由語法的結構有關。
好比 上面C語言代碼中對函數的語法定義以下:語法分析器就按照語法定義進行解析,就是從上到下匹配的過程。
也就是先匹配function的規則,匹配函數類型type、函數名name、函數參數parameters、函數體
當匹配函數參數時,就去匹配parameters的規則
當匹配函數體時,函數體由一個個語句組成,就去匹配各個語句stmt的規則。

function := type name parameters functionBody
parameters:= parameter*                          
functionBody:= stmt returnStatement

生成 AST 之後,程序的語法結構就很清晰了,但這棵樹到底表明了什麼意思,咱們目前仍然不能徹底肯定,要在語義分析階段肯定。

爲何要把程序轉換成AST這麼一顆樹,由於編譯器不像人能直接理解語句的含義,AST樹更有結構性,後續階段能夠針對這顆樹作各類分析!

語義分析

語義分析階段的任務:理解語義,語句要作什麼。
好比+號要執行加法、=號要執行賦值、for結構要去實現循環、if結構實現判斷。
因此語義階段要作的內容有:上下文分析(包括引用消解、類型分析與檢查等)

引用消解:找到變量所在的做用域,一個變量做用範圍屬於全局仍是局部。
類型識別:好比執行a+3,須要識別出變量a的類型,由於浮點數和整型執行不同,要執行不一樣的運算方式。
類型檢查:好比int b = a + 3,是否能夠進行定義賦值。等號右邊的表達式必須返回一個整型的數據、或則可以自動轉換成整型的數據,纔可以對類型爲整型的變量b進行復制。

好比以前的一段C語言代碼,通過語義分析後得到的信息(引用消解信息、類型信息),能夠在AST上進行標註,造成下面的「帶有標註的語法樹」,讓編譯器更好的理解程序的語義。

也會將這些上下文信息存入「符號表」結構中,便於各階段查詢上下文信息。
符號表是有層次的結構:咱們只須要逐級向上查找就能找到變量、函數等的信息(做用域、類型等)

接下來就能夠 解釋執行:實現一門解釋型的語言

Tip:編譯型語言須要生成目標代碼,而解釋性語言只須要解釋器去執行語義就能夠了。

實現AST的解釋器:在語法分析後有了程序的抽象語法樹,在語義分析後有了「帶有標註的AST」和符號表後,就能夠深度優先遍歷AST,而且一邊遍歷一邊執行結點的語義規則。整個遍歷的過程就是執行代碼的過程。

舉一個解釋執行的例子,好比執行下面的語義:

  • 遇到語法樹中的add 「+」節點:把兩個子節點的值進行相加,做爲「+」節點的值。
  • 遇到語法樹中的變量節點(右值):就取出變量的值。
  • 遇到字面量好比數字2:返回這個字面量表明的數值2。

中間代碼生成

在編譯前端完成後(編譯器已經理解了詞法和語義),編譯器能夠直接解釋執行、或則直接生成目標代碼。對於不一樣架構的CPU,還須要生成不一樣的彙編代碼,若是對每一種彙編代碼作優化就很繁瑣了。因此咱們須要增長一個環節:生成中間代碼IR,統一優化後中間代碼,再去將中間代碼生成目標代碼。

中間代碼IR的兩個用途:解釋執行 、代碼優化

解釋執行:解釋型語言,好比Python和Java,生成IR後就能直接執行了,也就是前面舉出的例子。

優化代碼:好比LLVM等工具;在生成代碼後須要作大量的優化工做,而不少優化工做不必使用匯編代碼來作(由於不一樣CPU體系的彙編語言不一樣),而能夠基於IR用統一的算法來完成,下降編譯器適配不一樣CPU的複雜性。

代碼優化

一種方案:基於基本塊做代碼優化

分類:本地優化、全局優化、過程間優化

本地優化:可用表達式分析、活躍性分析

全局優化:基於控制流圖CFG做優化。

控制流圖CFG :是一種有向圖,它體現了基本塊以前的指令流轉關係,若是從BLOCK1的最後一條指令是跳轉到 BLOCK2, 就連一條邊,若是經過分析 CFG,發現某個變量在其餘地方沒有被使用,就能夠把這個變量所在代碼行刪除。

過程間優化:跨越函數的優化,多個函數間做優化

優化案例:

代數優化:
好比刪除「x:=x+0 」,乘法優化掉「x:=x乘以0」 能夠簡化成「x:=0」,乘法優化成移位運算:「x:=x*8」能夠優化成「x:=x<<3」。

常數摺疊:

對常數的運算能夠在編譯時計算,好比 「x:= 20 乘以 3 」能夠優化成「x:=60」
刪除公共子表達式:做「可用表達式分析」

x := a + b
y := a + b //優化成y := x

拷貝傳播:做「可用表達式分析」

x := a + b
y := x
z := 2 * y //優化成z:= 2 * x

常數傳播:

x := 20
y := 10
z := x + y//優化成z := 30

死代碼刪除:做變量的「活躍性分析」
活躍性分析(優化刪除死代碼,沒用到的變量) 數據流分析:使用「半格理論」解決多路徑的V值計算集合問題,不在代碼下面集合的變量就是死代碼。

目標代碼生成

目標代碼生成,也就是生成虛擬機執行的字節碼,或則操做系統執行的彙編代碼
代碼生成的過程,其實很簡單,就是將中間代碼IR逐個翻譯成想要的彙編的代碼

那麼目標代碼生成階段的任務就有:

  • 選擇合適指令,生成性能最高的代碼。
  • 優化寄存器的分配,讓頻繁訪問的變量,好比循環語句中的變量放到寄存器中,寄存器比內存快
  • 在不改變運行結果下,對指令作重排序優化,從而充分運用CPU內部的多個功能部件的並行能力

本文參考:
編譯原理實戰課
編譯原理之美

相關文章
相關標籤/搜索