淺析代碼編譯過程


一、編譯目標

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

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抽象語法樹如下所示:

  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上進行標注,形成下面的“帶有標注的語法樹”,讓編譯器更好的理解程序的語義。

  也會將這些上下文信息存入“符號表”結構中,便於各階段查詢上下文信息。

  符號表是有層次的結構:我們只需要逐級向上查找就能找到變量、函數等的信息(作用域、類型等)

  接下來就可以 解釋執行:實現一門解釋型的語言。

  注意:編譯型語言需要生成目標代碼,而解釋性語言只需要解釋器去執行語義就可以了。

  實現AST的解釋器:在語法分析后有了程序的抽象語法樹,在語義分析后有了“帶有標注的AST”和符號表后,就可以深度優先遍歷AST,並且一邊遍歷一邊執行結點的語義規則。整個遍歷的過程就是執行代碼的過程。

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

  • 遇到語法樹中的add “+”節點:把兩個子節點的值進行相加,作為“+”節點的值。

  • 遇到語法樹中的變量節點(右值):就取出變量的值。

  • 遇到字面量比如數字2:返回這個字面量代表的數值2。

五、中間代碼生成

  在編譯前端完成后(編譯器已經理解了詞法和語義),編譯器可以直接解釋執行、或則直接生成目標代碼。

  對於不同架構的CPU,還需要生成不同的匯編代碼,如果對每一種匯編代碼做優化就很繁瑣了。所以我們需要增加一個環節:生成中間代碼IR,統一優化后中間代碼,再去將中間代碼生成目標代碼。

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

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

  優化代碼:比如LLVM等工具;在生成代碼后需要做大量的優化工作,而很多優化工作沒必要使用匯編代碼來做(因為不同CPU體系的匯編語言不同),而可以基於IR用統一的算法來完成,降低編譯器適配不同CPU的復雜性。

六、代碼優化

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

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

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

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

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

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

1、優化案例:

  代數優化:比如刪除“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內部的多個功能部件的並行能力

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM