前文
- golang快速入門[2.1]-go語(yǔ)言開(kāi)發(fā)環(huán)境配置-windows
- golang快速入門[2.2]-go語(yǔ)言開(kāi)發(fā)環(huán)境配置-macOS
- golang快速入門[2.3]-go語(yǔ)言開(kāi)發(fā)環(huán)境配置-linux
- golang快速入門[3]-go語(yǔ)言helloworld
- 在上文中,我們?cè)敿?xì)介紹了第一個(gè)helloworld程序
package main
import "fmt"
func main() {
fmt.Println("Hello, world")
}
- 在本文中,我們將介紹初學(xué)者比較關(guān)心的話題:go語(yǔ)言如何編譯為機(jī)器碼
- 本文的目標(biāo)是希望讀者對(duì)go語(yǔ)言的編譯過(guò)程有一個(gè)全面的理解
- 一段程序要運(yùn)行起來(lái),需要將go代碼生成機(jī)器能夠識(shí)別的二進(jìn)制代碼
- go代碼生成機(jī)器碼需要編譯器經(jīng)歷:
詞法分析 => 語(yǔ)法分析 => 類型檢查 => 中間代碼 => 代碼優(yōu)化 => 生成機(jī)器碼 - Go語(yǔ)言的編譯器入口是
src/cmd/compile/internal/gc
包中的main.go
文件调卑,此函數(shù)會(huì)先獲取命令行傳入的參數(shù)并更新編譯的選項(xiàng)和配置 - 隨后就會(huì)開(kāi)始運(yùn)行 parseFiles 函數(shù)對(duì)輸入的所有文件進(jìn)行詞法與語(yǔ)法分析
func Main(archInit func(*Arch)) {
// ...
lines := parseFiles(flag.Args())
- 接下來(lái)我們將對(duì)各個(gè)階段做深入介紹
詞法分析
- 所有的編譯過(guò)程都是從解析代碼的源文件開(kāi)始的
- 詞法分析的作用就是解析源代碼文件杉编,它將文件中的字符串序列轉(zhuǎn)換成
Token
序列,方便后面的處理和解析 - 我們一般會(huì)把執(zhí)行詞法分析的程序稱為詞法解析器(lexer)
-
Token
可以是關(guān)鍵字赌厅,字符串穷绵,變量名,函數(shù)名 - 有效程序的"單詞"都由
Token
表示特愿,具體來(lái)說(shuō)仲墨,這意味著"package","main"揍障,"func" 等單詞都為Token
- Go語(yǔ)言允許我們使用go/scanner和go/token包在Go程序中執(zhí)行解析程序目养,從而可以看到類似被編譯器解析后的結(jié)構(gòu)
- 如果在語(yǔ)法解析的過(guò)程中發(fā)生了任何語(yǔ)法錯(cuò)誤,都會(huì)被語(yǔ)法解析器發(fā)現(xiàn)并將消息打印到標(biāo)準(zhǔn)輸出上毒嫡,整個(gè)編譯過(guò)程也會(huì)隨著錯(cuò)誤的出現(xiàn)而被中止
- helloworld程序解析后如下所示
1:1 package "package"
1:9 IDENT "main"
1:13 ; "\n"
2:1 import "import"
2:8 STRING "\"fmt\""
2:13 ; "\n"
3:1 func "func"
3:6 IDENT "main"
3:10 ( ""
3:11 ) ""
3:13 { ""
4:3 IDENT "fmt"
4:6 . ""
4:7 IDENT "Println"
4:14 ( ""
4:15 STRING "\"Hello, world!\""
4:30 ) ""
4:31 ; "\n"
5:1 } ""
5:2 ; "\n"
5:3 EOF ""
- 我們可以看到癌蚁,詞法解析器添加了分號(hào),分號(hào)常常是在C語(yǔ)言等語(yǔ)言中一條語(yǔ)句后添加的
- 這解釋了為什么Go不需要分號(hào):詞法解析器可以智能地加入分號(hào)
語(yǔ)法分析
- 語(yǔ)法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會(huì)按照順序被語(yǔ)法分析器進(jìn)行解析努释,語(yǔ)法的解析過(guò)程就是將詞法分析生成的 Token 按照語(yǔ)言定義好的文法(Grammar)自下而上或者自上而下的進(jìn)行規(guī)約碘梢,每一個(gè) Go 的源代碼文件最終會(huì)被歸納成一個(gè) SourceFile 結(jié)構(gòu):
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
- 標(biāo)準(zhǔn)的 Golang 語(yǔ)法解析器使用的就是 LALR(1) 的文法,語(yǔ)法解析的結(jié)果生成了抽象語(yǔ)法樹(Abstract Syntax Tree伐蒂,AST)
- 抽象語(yǔ)法樹(Abstract Syntax Tree煞躬,AST),或簡(jiǎn)稱語(yǔ)法樹(Syntax tree)逸邦,是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示恩沛。它以樹狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)缕减。
- 之所以說(shuō)語(yǔ)法是“抽象”的复唤,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如烛卧,嵌套括號(hào)被隱含在樹的結(jié)構(gòu)中佛纫,并沒(méi)有以節(jié)點(diǎn)的形式呈現(xiàn);而類似于 if-condition-then 這樣的條件跳轉(zhuǎn)語(yǔ)句总放,可以使用帶有三個(gè)分支的節(jié)點(diǎn)來(lái)表示呈宇。
- 與AST相對(duì)應(yīng)的是CST(Concrete Syntax Trees),讀者可以在參考資料中拓展閱讀二者的差別
- 在AST中,我們能夠看到程序結(jié)構(gòu)局雄,例如函數(shù)和常量聲明
- Go為我們提供了用于解析程序和查看AST的軟件包:go/parser 和 go/ast
- helloworld程序生成的AST如下所示
0 *ast.File {
1 . Package: 1:1
2 . Name: *ast.Ident {
3 . . NamePos: 1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: 3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: 5:6
26 . . . . Name: "main"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "main"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: 5:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: 5:10
37 . . . . . Closing: 5:11
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: 5:13
42 . . . . List: []ast.Stmt (len = 1) {
43 . . . . . 0: *ast.ExprStmt {
44 . . . . . . X: *ast.CallExpr {
45 . . . . . . . Fun: *ast.SelectorExpr {
46 . . . . . . . . X: *ast.Ident {
47 . . . . . . . . . NamePos: 6:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: 6:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: 6:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: 6:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello, world!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: 6:29
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: 7:1
69 . . . }
70 . . }
71 . }
.. . .. // Left out for brevity
83 }
- 如上的輸出中我們能夠看出一些信息
- 在Decls字段中甥啄,包含文件中所有聲明的列表,例如import炬搭,常量蜈漓,變量和函數(shù)
- 為了進(jìn)一步理解,我們看一下對(duì)其的圖形化抽象表示
- 紅色表示與節(jié)點(diǎn)相對(duì)應(yīng)的代碼
- main函數(shù)包含了3個(gè)部分宫盔,名稱, 聲明, 主體
- 名稱是單詞main的標(biāo)識(shí)
- 由Type字段指定的聲明將包含參數(shù)列表和返回類型
- 主體由一系列語(yǔ)句組成融虽,其中包含程序的所有行。在本例中灼芭,只有一行
- fmt.Println語(yǔ)句由AST中的很多部分組成有额,由
ExprStmt
聲明。 -
ExprStmt
代表一個(gè)表達(dá)式彼绷,其可以是本例中的函數(shù)調(diào)用巍佑,也可以是二進(jìn)制運(yùn)算(例如加法和減法等) - 我們的
ExprStmt
包含一個(gè)CallExpr
,這是我們的實(shí)際函數(shù)調(diào)用寄悯。這又包括幾個(gè)部分萤衰,其中最重要的是Fun
和Args
- Fun包含對(duì)函數(shù)調(diào)用的引用,由
SelectorExpr
聲明猜旬。在AST中脆栋,編譯器尚未知道fmt是一個(gè)程序包胳螟,它也可能是AST中的變量 -
Args
包含一個(gè)表達(dá)式列表,這些表達(dá)式是該函數(shù)的參數(shù)筹吐。在本例中糖耸,我們已將文字字符串傳遞給函數(shù),因此它由類型為STRING
的BasicLit
表示丘薛。
類型檢查
構(gòu)造AST之后嘉竟,將會(huì)對(duì)所有import的包進(jìn)行解析
接著Go語(yǔ)言的編譯器會(huì)對(duì)語(yǔ)法樹中定義和使用的類型進(jìn)行檢查,類型檢查分別會(huì)按照順序?qū)Σ煌愋偷墓?jié)點(diǎn)進(jìn)行驗(yàn)證洋侨,按照以下的順序進(jìn)行處理:
常量舍扰、類型和函數(shù)名及類型
變量的賦值和初始化
函數(shù)和閉包的主體
哈希鍵值對(duì)的類型
導(dǎo)入函數(shù)體
外部的聲明
通過(guò)對(duì)每一棵抽象節(jié)點(diǎn)樹的遍歷,我們?cè)诿恳粋€(gè)節(jié)點(diǎn)上都會(huì)對(duì)當(dāng)前子樹的類型進(jìn)行驗(yàn)證保證當(dāng)前節(jié)點(diǎn)上不會(huì)出現(xiàn)類型錯(cuò)誤的問(wèn)題希坚,所有的類型錯(cuò)誤和不匹配都會(huì)在這一個(gè)階段被發(fā)現(xiàn)和暴露出來(lái)边苹。
類型檢查的階段不止會(huì)對(duì)樹狀結(jié)構(gòu)的節(jié)點(diǎn)進(jìn)行驗(yàn)證,同時(shí)也會(huì)對(duì)一些內(nèi)建的函數(shù)進(jìn)行展開(kāi)和改寫裁僧,例如 make 關(guān)鍵字在這個(gè)階段會(huì)根據(jù)子樹的結(jié)構(gòu)被替換成 makeslice 或者 makechan 等函數(shù)个束。
類型檢查不止對(duì)類型進(jìn)行了驗(yàn)證工作,還對(duì) AST 進(jìn)行了改寫以及處理Go語(yǔ)言內(nèi)置的關(guān)鍵字
生成中間代碼
- 在上面的步驟完成之后聊疲,可以明確代碼是正確有效的
- 接著將AST轉(zhuǎn)換為程序的低級(jí)表示形式茬底,即靜態(tài)單一賦值形式(Static Single Assignment Form,SSA)形式获洲,核心代碼位于
gc/ssa.go
- SSA不是程序的最終狀態(tài),其可以更輕松地應(yīng)用優(yōu)化阱表,其中最重要的是始終在使用變量之前定義變量,并且每個(gè)變量只分配一次
- 例如下面的代碼我們可以看到第一個(gè)x的賦值沒(méi)有必要的
x = 1
x = 2
y = 7
- 編輯器會(huì)將上面的代碼變?yōu)槿缦鹿鄙海瑥亩鴷?huì)刪除x_1
x_1 = 1
x_2 = 2
y_1 = 7
- 生成SSA的初始版本后最爬,將應(yīng)用許多優(yōu)化過(guò)程。這些優(yōu)化應(yīng)用于某些代碼段门岔,這些代碼段可以使處理器執(zhí)行起來(lái)更簡(jiǎn)單或更快速爱致。
- 例如下面的代碼是永遠(yuǎn)不會(huì)執(zhí)行的,因此可以被消除固歪。
if (false) {
fmt.Println(“test”)
}
- 優(yōu)化的另一個(gè)示例是可以刪除某些nil檢查蒜鸡,因?yàn)榫幾g器可以證明這些檢查永遠(yuǎn)不會(huì)出錯(cuò)
- 在對(duì)SSA進(jìn)行優(yōu)化的過(guò)程中使用了S表達(dá)式(S-expressions)進(jìn)行描述, S-expressions 是嵌套列表(樹形結(jié)構(gòu))數(shù)據(jù)的一種表示法胯努,由編程語(yǔ)言Lisp發(fā)明并普及
- SSA優(yōu)化過(guò)程中對(duì)于S表達(dá)式的應(yīng)用如下所示牢裳,將8位的常量乘法組合起來(lái)
(Mul8 (Const8 [c]) (Const8 [d])) -> (Const8 [int64(int8(c*d))])
具體的優(yōu)化包括
常數(shù)傳播(constant propagation)
值域傳播(value range propagation)
稀疏有條件的常數(shù)傳播(sparse conditional constant propagation)
消除無(wú)用的程式碼(dead code elimination)
全域數(shù)值編號(hào)(global value numbering)
消除部分的冗余(partial redundancy elimination)
強(qiáng)度折減(strength reduction)
寄存器分配(register allocation)
SSA優(yōu)化
- 我們可以用下面的簡(jiǎn)單代碼來(lái)查看SSA及其優(yōu)化過(guò)程
- 對(duì)于如下程序
package main
import "fmt"
func main() {
fmt.Println(2)
}
- 我們需要在命令行運(yùn)行如下指令來(lái)查看SSA
- GOSSAFUNC環(huán)境變量代表我們需要查看SSA的函數(shù)并創(chuàng)建ssa.html文件
- GOOS、GOARCH代表編譯為在Linux 64-bit平臺(tái)運(yùn)行的代碼
- go build用-ldflags給go編譯器傳入?yún)?shù)
- -S 標(biāo)識(shí)將打印匯編代碼
$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go
- 下面的命令等價(jià)
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
- 當(dāng)打開(kāi)ssa.html時(shí)叶沛,將顯示許多代碼片段蒲讯,其中一些片段是隱藏的。
- Start片段是從AST生成的SSA灰署。genssa片段是最終生成的Plan9匯編代碼
- Start片段如下
start
b1:-
v1 (?) = InitMem <mem>
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v4 (?) = Addr <*uint8> {type.int} v3
v5 (?) = Addr <*int> {""..stmp_0} v3
v6 (6) = IMake <interface {}> v4 v5 (~arg0[interface {}])
v7 (?) = ConstInterface <interface {}>
v8 (?) = ArrayMake1 <[1]interface {}> v7
v9 (6) = VarDef <mem> {.autotmp_11} v1
v10 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v9
v11 (6) = Store <mem> {[1]interface {}} v10 v8 v9
v12 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v11
v13 (6) = NilCheck <void> v12 v11
v14 (?) = Const64 <int> [0] (fmt..autotmp_3[int], fmt.n[int])
v15 (?) = Const64 <int> [1]
v16 (6) = PtrIndex <*interface {}> v12 v14
v17 (6) = Store <mem> {interface {}} v16 v6 v11
v18 (6) = NilCheck <void> v12 v17
v19 (6) = Copy <*interface {}> v12
v20 (6) = IsSliceInBounds <bool> v14 v15
v25 (?) = ConstInterface <error> (fmt..autotmp_4[error], fmt.err[error])
v28 (?) = OffPtr <*io.Writer> [0] v2
v29 (?) = Addr <*uint8> {go.itab.*os.File,io.Writer} v3
v30 (?) = Addr <**os.File> {os.Stdout} v3
v34 (?) = OffPtr <*[]interface {}> [16] v2
v37 (?) = OffPtr <*int> [40] v2
v39 (?) = OffPtr <*error> [48] v2
If v20 → b2 b3 (likely) (6)
b2: ← b1-
v23 (6) = Sub64 <int> v15 v14
v24 (6) = SliceMake <[]interface {}> v19 v23 v23 (fmt.a[[]interface {}])
v26 (6) = Copy <mem> v17
v27 (+6) = InlMark <void> [0] v26
v31 (274) = Load <*os.File> v30 v26
v32 (274) = IMake <io.Writer> v29 v31
v33 (274) = Store <mem> {io.Writer} v28 v32 v26
v35 (274) = Store <mem> {[]interface {}} v34 v24 v33
v36 (274) = StaticCall <mem> {fmt.Fprintln} [64] v35
v38 (274) = Load <int> v37 v36 (fmt.n[int], fmt..autotmp_3[int])
v40 (274) = Load <error> v39 v36 (fmt.err[error], fmt..autotmp_4[error])
Plain → b4 (+6)
b3: ← b1-
v21 (6) = Copy <mem> v17
v22 (6) = PanicBounds <mem> [6] v14 v15 v21
Exit v22 (6)
b4: ← b2-
v41 (7) = Copy <mem> v36
Ret v41
name ~arg0[interface {}]: v6
name fmt.a[[]interface {}]: v24
name fmt.n[int]: v14 v38
name fmt.err[error]: v25 v40
name fmt..autotmp_3[int]: v14 v38
name fmt..autotmp_4[error]: v25 v40
- 每個(gè)v是一個(gè)新變量判帮,可以單擊以查看使用它的位置局嘁。
- b是代碼塊,本例中我們有3個(gè)代碼塊:b1, b2和 b3
- b1將始終被執(zhí)行晦墙,b2和b3是條件塊悦昵,如b1最后一行所示:
If v20 → b2 b3 (likely) (6)
,只有v20為true會(huì)執(zhí)行b2晌畅,v20為false會(huì)執(zhí)行b3 - 我們可以點(diǎn)擊v20查看其定義但指,其定義是
v20 (6) = IsSliceInBounds v14 v15
-
IsSliceInBounds
會(huì)執(zhí)行如下檢查:0 <= v14 <= v15 是否成立 - 我們可以單擊v14和v15來(lái)查看它們的定義:
v14 = Const64 [0]
,v15 = Const64 [1]
- Const64為64位常量,因此 0 <= 0 <= 1 始終成立抗楔,因此v20始終成立
- 當(dāng)我們?cè)趏pt片段查看v20時(shí)棋凳,會(huì)發(fā)現(xiàn)
v20 (6) = ConstBool [true]
,v20變?yōu)榱耸冀K為true - 因此连躏,我們會(huì)看到在opt deadcode片段中剩岳,b3塊被刪除了
代碼優(yōu)化
- 生成SSA之后,Go編譯器還會(huì)進(jìn)行一系列簡(jiǎn)單的優(yōu)化入热,例如無(wú)效和無(wú)用代碼的刪除
- 我們將用同樣的ssa.html文件拍棕,比較lower 和 lowered deadcode片段
- 在HTML文件中,某些行顯示為灰色勺良,這意味著它們將在下一階段之一中被刪除或更改
- 例如
v15 = MOVQconst [1]
為灰色莫湘,因?yàn)槠湓诤竺娓緵](méi)有被使用。MOVQconst與我們之前看到的指令Const64相同郑气,僅適用于amd64平臺(tái)
機(jī)器碼生成
- 完成以上步驟幅垮,最終還會(huì)生成跨平臺(tái)的plan9匯編指令,并進(jìn)一步根據(jù)目標(biāo)的 CPU 架構(gòu)生成二進(jìn)制機(jī)器代碼
- Go語(yǔ)言源代碼的 cmd/compile/internal 目錄中包含了非常多機(jī)器碼生成相關(guān)的包
- 不同類型的 CPU 分別使用了不同的包進(jìn)行生成 amd64尾组、arm忙芒、arm64、mips讳侨、mips64呵萨、ppc64、s390x跨跨、x86 和 wasm
- Go語(yǔ)言能夠在幾乎全部常見(jiàn)的 CPU 指令集類型上運(yùn)行潮峦。
問(wèn):go的編譯速度相對(duì)于java為什么更快
- 快速編譯是go的設(shè)計(jì)目標(biāo)之一
- go語(yǔ)法緊湊且規(guī)則因此更容易解析
- go具有嚴(yán)格的依賴管理,沒(méi)有循環(huán)依賴問(wèn)題勇婴,計(jì)算依賴樹非常高效
- 語(yǔ)言的設(shè)計(jì)易于分析忱嘹,無(wú)需符號(hào)表即可進(jìn)行解析
- Go語(yǔ)言本身比Java簡(jiǎn)單得多,編輯器本身做的事不多
- Go編譯器較新耕渴,其中的無(wú)用代碼更少
總結(jié)
- 在本文中詳細(xì)介紹了go語(yǔ)言從源代碼編譯為機(jī)器碼的過(guò)程
- 涉及了詞法分析拘悦、語(yǔ)法分析、類型檢查橱脸、SSA生成與代碼優(yōu)化础米、生成機(jī)器碼等過(guò)程
- 以期幫助讀者全面深入的了解go語(yǔ)言的編譯過(guò)程