go語言編譯過程概述
總結自《go語言設計與實現》
名詞解釋:
-
中間程式碼
中間程式碼是編譯器或者虛擬機器使用的語言,它可以來幫助我們分析計算機程式。在編譯過程中,編譯器會在將原始碼轉換到機器碼的過程中,先把原始碼轉換成一種中間的表示形式,即中間程式碼。將程式語言到機器碼的過程拆成中間程式碼生成和機器碼生成兩個簡單步驟可以簡化該問題,中間程式碼是一種更接近機器語言的表示形式,對中間程式碼的優化和分析相比直接分析高階程式語言更容易。
-
SSA
靜態單賦值是中間程式碼的特性,如果中間程式碼具有靜態單賦值的特性,那麼每個變數就只會被賦值一次。在實踐中,我們通常會用下標實現靜態單賦值,這裡以下面的程式碼舉個例子:
x := 1 x := 2 y := x
經過簡單的分析,我們就能夠發現上述的程式碼第一行的賦值語句
x := 1
不會起到任何作用。下面是具有 SSA 特性的中間程式碼,我們可以清晰地發現變數y_1
和x_1
是沒有任何關係的,所以在機器碼生成時就可以省去x := 1
的賦值,通過減少需要執行的指令優化這段程式碼。x_1 := 1 x_2 := 2 y_1 := x_2
過程概述
-
詞法與語法分析
-
編譯過程其實都是從解析程式碼的原始檔開始的,詞法分析的作用就是解析原始碼檔案,它將檔案中的字串序列轉換成 Token 序列,方便後面的處理和解析,我們一般會把執行詞法分析的程式稱為詞法解析器。
-
而語法分析的輸入是詞法分析器輸出的 Token 序列,語法分析器會按照順序解析 Token 序列,該過程會將詞法分析生成的 Token 按照程式語言定義好的文法(Grammar)自下而上或者自上而下的規約,每一個 Go 的原始碼檔案最終會被歸納成一個 SourceFile 結構 。
詞法分析會返回一個不包含空格、換行等字元的 Token 序列,例如:package, json, import, (, io, ), …,而語法分析會把 Token 序列轉換成有意義的結構體,即語法樹。
-
-
型別檢查
當拿到一組檔案的抽象語法樹之後,Go 語言的編譯器會對語法樹中定義和使用的型別進行檢查,型別檢查會按照以下的順序分別驗證和處理不同型別的節點:
- 常量、型別和函式名及型別;
- 變數的賦值和初始化;
- 函式和閉包的主體;
- 雜湊鍵值對的型別;
- 匯入函式體;
- 外部的宣告;
通過對整棵抽象語法樹的遍歷(也會修改語法樹),我們在每個節點上都會對當前子樹的型別進行驗證,以保證節點不存在型別錯誤,所有的型別錯誤和不匹配都會在這一個階段被暴露出來,其中包括:結構體對介面的實現。
型別檢查階段不止會對節點的型別進行驗證,還會展開和改寫一些內建的函式,例如 make 關鍵字在這個階段會根據子樹的結構被替換成 runtime.makeslice或者 runtime.makechan等函式。
-
中間程式碼生成
當我們將原始檔轉換成了抽象語法樹、對整棵樹的語法進行解析並進行型別檢查之後,就可以認為當前檔案中的程式碼不存在語法錯誤和型別錯誤的問題了,Go 語言的編譯器就會將輸入的抽象語法樹轉換成中間程式碼。
在型別檢查之後,編譯器會通過 cmd/compile/internal/gc.compileFunctions編譯整個 Go 語言專案中的全部函式,這些函式會在一個編譯佇列中等待幾個 Goroutine 的消費,併發執行的 Goroutine 會將所有函式對應的抽象語法樹轉換成中間程式碼。由於 Go 語言編譯器的中間程式碼使用了 SSA 的特性,所以在這一階段我們能夠分析出程式碼中的無用變數和片段並對程式碼進行優化
-
機器碼生成
Go 語言原始碼的 src/cmd/compile/internal 目錄中包含了很多機器碼生成相關的包,不同型別的 CPU 分別使用了不同的包生成機器碼,其中包括 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm