參考資料
- https://github.com/golang/go/blob/master/src/cmd/compile/README.md
- https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889
- 基于Golang 1.16版本
階段
Go編譯器由四個(gè)階段組成,可以分為兩類(lèi)
- frontend前端:這一階段對(duì)源碼進(jìn)行語(yǔ)法解析驱还,并生成AST
- backend后端:這一階段將把transform the representation of the source code into machine code, 并進(jìn)行數(shù)項(xiàng)優(yōu)化
為了更好地理解每個(gè)階段凸克,讓我們使用如下的示例程序
package main
func main() {
a := 1
b := 2
if true {
add(a, b)
}
}
func add(a, b int) {
println(a + b)
}
P1 解析
-
cmd/compile/internal/syntax
詞法萎战,解析器,語(yǔ)法樹(shù)
第一個(gè)階段非常簡(jiǎn)單直接:
第一階段蚂维,源碼經(jīng)過(guò)詞法分析虫啥、語(yǔ)法解析,對(duì)每個(gè)源碼文件涂籽,都構(gòu)造出相應(yīng)的語(yǔ)法樹(shù)
Lexer首先運(yùn)行,把源代碼轉(zhuǎn)化為詞法單元苔咪。我們可以通過(guò)這個(gè)程序來(lái)自己模擬運(yùn)行Lexer
package main
import (
"fmt"
"go/scanner"
"go/token"
"io/ioutil"
)
func main() {
// src is the input that we want to tokenize.
src, _ := ioutil.ReadFile(`main.go`)
// Initialize the scanner
var s scanner.Scanner
// positions are relative to fSet
fSet := token.NewFileSet()
file := fSet.AddFile("", fSet.Base(), len(src))
// nil means no error handler
s.Init(file, src, nil, scanner.ScanComments)
// Repeated calls to Scan yield the token sequence found in the input
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
fmt.Printf("%s\t%s\t%q\n", fSet.Position(pos), tok, lit)
}
}
截選輸出如下:
1:1 package "package"
1:9 IDENT "main"
1:13 ; "\n"
3:1 func "func"
3:6 IDENT "main"
3:10 ( ""
3:11 ) ""
3:13 { ""
4:2 IDENT "a"
4:4 := ""
4:7 INT "1"
4:8 ; "\n"
5:2 IDENT "b"
5:4 := ""
5:7 INT "2"
5:8 ; "\n"
6:2 if "if"
6:5 IDENT "true"
6:10 { ""
7:3 IDENT "add"
7:6 ( ""
7:7 IDENT "a"
一旦經(jīng)過(guò)詞法化敞恋,源碼被解析構(gòu)造成語(yǔ)法樹(shù)顽分。
語(yǔ)法樹(shù)還包含了代碼位置信息丝里,該信息可用于debug或錯(cuò)誤報(bào)告杯聚。
P2 類(lèi)型檢查和AST轉(zhuǎn)化
-
cmd/compile/internal/gc
創(chuàng)建編譯器AST抒痒,類(lèi)型檢查,AST轉(zhuǎn)換
AST是類(lèi)型檢查的故响。第一個(gè)步驟就是名字解析和類(lèi)型推斷彩届,確定對(duì)象和標(biāo)識(shí)符的對(duì)應(yīng)關(guān)系,表達(dá)式是何種類(lèi)型樟蠕。Type-checking這一階段還引入了額外的確定性步驟,例如吓懈,“聲明未使用”靡狞、函數(shù)是否終止等。
還有一些確定的轉(zhuǎn)換也在AST階段完成榕栏。一些節(jié)點(diǎn)會(huì)根據(jù)類(lèi)型信息進(jìn)行細(xì)化蕾各,比如字符串加法從算術(shù)加法節(jié)點(diǎn)中分離出來(lái)。其他一些示例是不可達(dá)代碼清除妨托、內(nèi)聯(lián)函數(shù)調(diào)用、逃逸分析内颗。
轉(zhuǎn)化到AST的步驟可以通過(guò)命令go tool compile -w
來(lái)展示出來(lái)敦腔,如果加上-l,則可以禁用內(nèi)聯(lián)找前。在我們的樣例代碼中判族,如果不禁用內(nèi)聯(lián),add方法會(huì)被內(nèi)聯(lián)掉槽惫。我們可以分別使用go tool compile -w example.o
和go tool compile -w -l example.o
進(jìn)行對(duì)比
禁用了內(nèi)聯(lián)的命令辩撑,會(huì)輸出這樣的AST
沒(méi)禁用內(nèi)聯(lián)的命令則不會(huì)生成,這里可以看出來(lái),編譯器做了內(nèi)聯(lián)的優(yōu)化氓仲。
SSA 生成
SSA 概念
-
cmd/compile/internal/gc
AST轉(zhuǎn)化到SSA -
cmd/compile/internal/ssa
SSA階段和規(guī)則
在這個(gè)階段敬扛,AST轉(zhuǎn)化為SSA的格式,這是一種具有特定屬性的更底層的IR谍珊,可以更輕松地在上面進(jìn)行優(yōu)化并最終生成機(jī)器碼急侥。階段應(yīng)用了內(nèi)聯(lián)函數(shù)。這些是編譯器被教導(dǎo)要根據(jù)具體情況用高度優(yōu)化的代碼替換的特殊函數(shù)贝润。在AST到SSA的轉(zhuǎn)換期間铝宵,某些確定的節(jié)點(diǎn)也被降低為更簡(jiǎn)單的組件华畏,使得編譯器的其余部分可以使用它們尊蚁。例如,內(nèi)置的copy函數(shù)被內(nèi)存移動(dòng)取代仑乌、范圍循環(huán)被重寫(xiě)為for循環(huán)叶撒。由于歷史原因,其中一些目前在SSA轉(zhuǎn)換之前發(fā)生压汪,但長(zhǎng)期計(jì)劃是將它們?nèi)恳频竭@里古瓤。
然后,應(yīng)用一系列的穿香、機(jī)器無(wú)關(guān)的階段和規(guī)則绎速。這些不涉及任何的計(jì)算機(jī)架構(gòu),因此可以在任何GOARCH
變體上運(yùn)行洒宝。
這些通用的階段包括:不可達(dá)代碼清除萌京、刪除不需要的nil檢查、移除無(wú)用的分支靠瞎。
通用的重寫(xiě)規(guī)則主要涉及表達(dá)式求妹,包括表達(dá)式替換為常量、優(yōu)化乘法和浮點(diǎn)運(yùn)算等丑勤。
SSA code可以用這個(gè)命令dump
并展示出來(lái)
GOSSAFUNC=main go tool compile main.go && open ssa.html
SSA階段
SSA優(yōu)化解析
start Tab上生成了最開(kāi)始的SSA
變量 a 和 b 與 if 條件一起在此處突出顯示法竞,以便我們稍后查看這些行是如何更改的。 代碼還向我們展示了編譯器如何管理 println
函數(shù)薛躬,它被分解為 4 個(gè)步驟:printlock
呆细、printint
、printnl
絮爷、printunlock
坑夯。 編譯器會(huì)自動(dòng)為我們加鎖,并根據(jù)參數(shù)的類(lèi)型調(diào)用相關(guān)方法正確打印柜蜈。
在我們的示例中淑履,由于 a 和 b 在編譯時(shí)已知,編譯器可以計(jì)算最終結(jié)果并將變量標(biāo)記為不再需要秘噪。 opt
階段 會(huì)優(yōu)化這部分:
這個(gè)階段v7被優(yōu)化計(jì)算成了3指煎。并且接下來(lái),因?yàn)関4和v5已經(jīng)沒(méi)有人聲明使用暖侨,在opt deadcode
階段崇渗,v4和v5也會(huì)被清除掉
等待所有階段完成之后京郑,Go編譯器將會(huì)生成中間匯編語(yǔ)言
下一階段會(huì)將匯編語(yǔ)言轉(zhuǎn)換為二進(jìn)制文件
機(jī)器代碼生成
-
cmd/compile/internal/ssa
SSA "lowering" 和 特定arch的階段 -
cmd/internal/obj
機(jī)器語(yǔ)言生成
機(jī)器相關(guān)的編譯階段從"lowering"階段開(kāi)始些举,它將通用的值替換成機(jī)器特定的變體。例如驶臊,在 amd64 內(nèi)存操作數(shù)上是可能的,因此可以組合許多加載-存儲(chǔ)操作扛门。
注意這些底層階段執(zhí)行了所有機(jī)器特定的規(guī)則纵寝,所以也應(yīng)用了很多優(yōu)化。
一旦SSA被"lowered"到更特定的目標(biāo)架構(gòu)爽茴,就開(kāi)始執(zhí)行最終的代碼優(yōu)化室奏。這包括另一個(gè)不可達(dá)代碼清除階段、將值更靠近它們的使用者窍奋、移除從未使用的本地變量琳袄、寄存器分配。
還有一部分重要工作包括堆棧幀布局窖逗,它將堆棧偏移分配給局部變量碎紊,以及指針存活分析,它計(jì)算每個(gè) GC 安全點(diǎn)上哪些堆棧上指針是活躍的仗考。
在 SSA 生成階段結(jié)束時(shí),Go 函數(shù)已轉(zhuǎn)換為一系列 obj.Prog 指令权均。 這些被傳遞給匯編器(cmd/internal/obj)锅锨,匯編器將它們轉(zhuǎn)換成機(jī)器代碼并寫(xiě)出最終的目標(biāo)文件。 目標(biāo)文件還將包含反射數(shù)據(jù)必指、導(dǎo)出數(shù)據(jù)和調(diào)試信息恕洲。
我們可以使用go tool objdump $binary
來(lái)查看匯編代碼梅割。當(dāng)compile的.o
文件生成之后炮捧,可以通過(guò)go tool link
來(lái)生成二進(jìn)制可運(yùn)行文件惦银。