今天咱們一塊兒來研究 Go 1.11 的編譯器,以及它將 Go 程序代碼編譯成可執行文件的過程。以便了解咱們平常使用的工具是如何工做的。
本文還會帶你瞭解 Go 程序爲何這麼快,以及編譯器在這中間起到了什麼做用。
token
,交給 parser
解析。parser
,它將一系列 token
轉換爲 AST(抽象語法樹)
,用於下一步生成代碼。AST
並根據目標機器平臺的不一樣,生成目標機器碼。注意:下面使用的代碼包(go/scanner,go/parser,go/token,go/ast)主要是讓咱們能夠方便地對 Go 代碼進行解析和生成,作出更有趣的事情。可是 Go 自己的編譯器並非用這些代碼包實現的。html
任何編譯器的第一步都是將源代碼文本分解成 token
,由掃描程序(也稱爲詞法分析器)完成。token
能夠是關鍵字,字符串,變量名,函數名等等。每個有效的詞都由 token
表示。
在 Go 中,咱們寫在代碼上的 "package"
,"main"
,"func"
這些都是 token
。linux
token
由代碼中的位置,類型和原始文本組成。咱們可使用 go/scanner 和 go/token 包在 Go 程序中本身執行掃描程序。這意味着咱們能夠像編譯器那樣掃描檢視本身的代碼。
下面,咱們將經過一個打印 Hello World 的示例來展現 token
。git
package main import ( "fmt" "go/scanner" "go/token" ) func main() { src := []byte(` package main import "fmt" func main() { fmt.Println("Hello, world!") } `) var s scanner.Scanner fset := token.NewFileSet() file := fset.AddFile("", fset.Base(), len(src)) s.Init(file, src, nil, 0) for { pos, tok, lit := s.Scan() fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit) if tok == token.EOF { break } } }
首先經過源代碼字符串建立 token
集合並初始化 scan.Scanner
,它將逐行掃描咱們的源代碼。
接下來循環調用 Scan()
並打印每一個 token
的位置,類型和文本字符串,直到遇到文件結束(EOF)標記。github
輸出:golang
2:1 package "package" 2:9 IDENT "main" 2:13 ; "\n" 4:1 import "import" 4:8 STRING "\"fmt\"" 4:13 ; "\n" 6:1 func "func" 6:6 IDENT "main" 6:10 ( "" 6:11 ) "" 6:13 { "" 7:2 IDENT "fmt" 7:5 . "" 7:6 IDENT "Println" 7:13 ( "" 7:14 STRING "\"Hello, world!\"" 7:29 ) "" 7:30 ; "\n" 8:1 } "" 8:2 ; "\n" 8:3 EOF ""
以第一行爲例分析這個輸出,第一列 2:1
表示掃描到了源代碼第二行第一個字符,第二列 package
表示 token
是 package
,第三列 "package"
表示源代碼文本。
咱們能夠看到在 Scanner
執行過程當中將 \n
換行符標記成了 ;
分號,像在 C 語言中是用分號表示一行結束的。這就解釋了爲何 Go 不須要分號:它們是在詞法分析階段由 Scanner
智能地解釋的。express
源代碼掃描完成後,掃描結果將被傳遞給語法分析器。語法分析是編譯的一個階段,它將 token
轉換爲 抽象語法樹(AST)
。 AST
是源代碼的結構化表示。在 AST
中,咱們將可以看到程序結構,好比函數和常量聲明。segmentfault
咱們使用 go/parser 和 go/ast 來打印完整的 AST
:瀏覽器
package main import ( "go/ast" "go/parser" "go/token" "log" ) func main() { src := []byte(` package main import "fmt" func main() { fmt.Println("Hello, world!") } `) fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", src, 0) if err != nil { log.Fatal(err) } ast.Print(fset, file) }
輸出:函數
0 *ast.File { 1 . Package: 2:1 2 . Name: *ast.Ident { 3 . . NamePos: 2:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: 4:1 9 . . . Tok: import 10 . . . Lparen: - 11 . . . Specs: []ast.Spec (len = 1) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: 4: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: 6: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: 6:1 35 . . . . Params: *ast.FieldList { 36 . . . . . Opening: 6:10 37 . . . . . Closing: 6:11 38 . . . . } 39 . . . } 40 . . . Body: *ast.BlockStmt { 41 . . . . Lbrace: 6: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: 7:2 48 . . . . . . . . . Name: "fmt" 49 . . . . . . . . } 50 . . . . . . . . Sel: *ast.Ident { 51 . . . . . . . . . NamePos: 7:6 52 . . . . . . . . . Name: "Println" 53 . . . . . . . . } 54 . . . . . . . } 55 . . . . . . . Lparen: 7:13 56 . . . . . . . Args: []ast.Expr (len = 1) { 57 . . . . . . . . 0: *ast.BasicLit { 58 . . . . . . . . . ValuePos: 7:14 59 . . . . . . . . . Kind: STRING 60 . . . . . . . . . Value: "\"Hello, world!\"" 61 . . . . . . . . } 62 . . . . . . . } 63 . . . . . . . Ellipsis: - 64 . . . . . . . Rparen: 7:29 65 . . . . . . } 66 . . . . . } 67 . . . . } 68 . . . . Rbrace: 8:1 69 . . . } 70 . . } 71 . } 72 . Scope: *ast.Scope { 73 . . Objects: map[string]*ast.Object (len = 1) { 74 . . . "main": *(obj @ 27) 75 . . } 76 . } 77 . Imports: []*ast.ImportSpec (len = 1) { 78 . . 0: *(obj @ 12) 79 . } 80 . Unresolved: []*ast.Ident (len = 1) { 81 . . 0: *(obj @ 46) 82 . } 83 }
分析這個輸出,在 Decls
字段中,包含了代碼中全部的聲明,例如導入、常量、變量和函數。在本例中,咱們只有兩個:導入fmt包 和 主函數。
爲了進一步理解它,咱們能夠看看下面這個圖,它是上述數據的表示,但只包含類型,紅色表明與節點對應的代碼:工具
main函數
由三個部分組成:Name、Type 和 Body。Name 是值爲 main 的標識符。由 Type
字段指定的聲明將包含參數列表和返回類型(若是咱們指定了的話)。正文由一系列語句組成,裏面包含了程序的全部行,在本例中只有一行fmt.Println("Hello, world!")
。
咱們的一條 fmt.Println
語句由 AST
中不少部分組成。
該語句是一個 ExprStmt
表達式語句(expression statement),例如,它能夠像這裏同樣是一個函數調用,它能夠是字面量,能夠是一個二元運算(例如加法和減法),固然也能夠是一元運算(例如自增++,自減--,否認!等)等等。
同時,在函數調用的參數中可使用任何表達式。
而後,ExprStmt
又包含一個 CallExpr
,它是咱們實際的函數調用。裏面又包括幾個部分,其中最重要的部分是 Fun
和 Args
。 Fun
包含對函數調用的引用,在這種狀況下,它是一個 SelectorExpr
,由於咱們從 fmt
包中選擇 Println
標識符。
可是至此,在 AST
中,編譯器還不知道 fmt
是一個包,它也多是 AST
中的一個變量。
Args
包含一個表達式列表,它是函數的參數。這裏,咱們將一個文本字符串傳遞給函數,於是它由一個類型爲 STRING
的 BasicLit
表示。
顯然,AST
包含了許多信息,咱們不只能夠分析出以上結論,還能夠進一步檢查 AST
並查找文件中的全部函數調用。下面,咱們將使用 go/ast 包中的 Inspect
函數來遞歸地遍歷樹,並分析全部節點的信息。
package main import ( "fmt" "go/ast" "go/parser" "go/printer" "go/token" "os" ) func main() { src := []byte(` package main import "fmt" func main() { fmt.Println("Hello, world!") } `) fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", src, 0) if err != nil { fmt.Println(err) } ast.Inspect(file, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } printer.Fprint(os.Stdout, fset, call.Fun) return false }) }
輸出:
fmt.Println
上面代碼的做用是查找全部節點以及它們是否爲 *ast.CallExpr
類型,上面也說過這種類型是函數調用。若是是,則使用 go/printer 包打印 Fun
中存在的函數的名稱。
構建出 AST
後,將使用 GOPATH
或者在 Go 1.11 及更高版本中的 modules 解析全部導入。而後,執行類型檢查,並作一些讓程序運行更快的初級優化。
在解析導入並作了類型檢查以後,咱們能夠確認程序是合法的 Go 代碼,而後就走到將 AST
轉換爲(僞)目標機器碼的過程。
此過程的第一步是將 AST
轉換爲程序的低級表示,特別是轉換爲 靜態單賦值(SSA)表單。這個中間表示不是最終的機器代碼,但它確實表明了最終的機器代碼。 SSA
具備一組屬性,會使應用優化變得更容易,其中最重要的是在使用變量以前老是定義變量,而且每一個變量只分配一次。
在生成 SSA
的初始版本以後,將執行一些優化。這些優化適用於某些代碼,可使處理器執行起來更簡單且更快速。例如,能夠作 死碼消除。還有好比能夠刪除某些 nil 檢查,由於編譯器能夠證實這些檢查永遠不會出錯。
如今經過最簡單的例子來講明 SSA
和一些優化過程:
package main import "fmt" func main() { fmt.Println(2) }
如你所見,此程序只有一個函數和一個導入。它會在運行時打印 2。可是,此例足以讓咱們瞭解SSA。
爲了顯示生成的 SSA
,咱們須要將 GOSSAFUNC
環境變量設置爲咱們想要跟蹤的函數,在本例中爲main
函數。咱們還須要將 -S 標識傳遞給編譯器,這樣它就會打印代碼並建立一個HTML文件。咱們還將編譯Linux 64位的文件,以確保機器代碼與您在這裏看到的相同。
在終端執行下面的命令:
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags -S main.go
會在終端打印出全部的 SSA
,同時也會生成一個交互式的 ssa.html
文件,咱們用瀏覽器打開它。
當你打開 ssa.html
時,將顯示不少階段,其中大部分都已摺疊。start
階段是從 AST
生成的SSA
;lower
階段將非機器特定的 SSA
轉換爲機器特定的 SSA
,最後的 genssa
就是生成的機器代碼。
start
階段的代碼以下:
b1: v1 = InitMem <mem> v2 = SP <uintptr> v3 = SB <uintptr> v4 = ConstInterface <interface {}> v5 = ArrayMake1 <[1]interface {}> v4 v6 = VarDef <mem> {.autotmp_0} v1 v7 = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v6 v8 = Store <mem> {[1]interface {}} v7 v5 v6 v9 = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v8 v10 = Addr <*uint8> {type.int} v3 v11 = Addr <*int> {"".statictmp_0} v3 v12 = IMake <interface {}> v10 v11 v13 = NilCheck <void> v9 v8 v14 = Const64 <int> [0] v15 = Const64 <int> [1] v16 = PtrIndex <*interface {}> v9 v14 v17 = Store <mem> {interface {}} v16 v12 v8 v18 = NilCheck <void> v9 v17 v19 = IsSliceInBounds <bool> v14 v15 v24 = OffPtr <*[]interface {}> [0] v2 v28 = OffPtr <*int> [24] v2 If v19 → b2 b3 (likely) (line 6) b2: ← b1 v22 = Sub64 <int> v15 v14 v23 = SliceMake <[]interface {}> v9 v22 v22 v25 = Copy <mem> v17 v26 = Store <mem> {[]interface {}} v24 v23 v25 v27 = StaticCall <mem> {fmt.Println} [48] v26 v29 = VarKill <mem> {.autotmp_0} v27 Ret v29 (line 7) b3: ← b1 v20 = Copy <mem> v17 v21 = StaticCall <mem> {runtime.panicslice} v20 Exit v21 (line 6)
這個簡單的程序就已經產生了至關多的 SSA
(總共35行)。然而,不少都是引用,能夠消除不少(最終的SSA版本有28行,最終的機器代碼版本有18行)。
每一個 v
都是一個新變量,能夠點擊來查看它被使用的位置。b 是塊,這裏有三塊:b1,b2,b3。b1 始終會執行,b2
和 b3
是條件塊,知足條件才執行。
咱們來看 b1
結尾處的 If v19 → b2 b3 (likely)
。單擊該行中的 v19
能夠查看它定義的位置。能夠看到它定義爲 IsSliceInBounds <bool> v14 v15
,經過 Go 編譯器源代碼,咱們知道 IsSliceInBounds
的做用是檢查 0 <= arg0 <= arg1
。而後單擊 v14
和 v15
看看在哪定義的,咱們會看到 v14 = Const64 <int> [0]
,Const64 是一個常量 64 位整數。 v15 定義同樣,放在 args1 的位置。因此,實際執行的是 0 <= 0 <= 1
,這顯然是正確的。
編譯器也可以證實這一點,當咱們查看 opt
階段(「機器無關優化」)時,咱們能夠看到它已經重寫了 v19
爲 ConstBool <bool> [true]
。結果就是,在 opt deadcode
階段,b3
條件塊被刪除了,由於永遠也不會執行到 b3
。
下面來看一下 Go 編譯器在把 SSA
轉換爲 機器特定的SSA
以後所作的另外一個更簡單的優化,基於amd64體系結構的機器代碼。下面,咱們將比較 lower
和 lowered deadcode
。lower
:
b1: BlockInvalid (6) b2: v2 (?) = SP <uintptr> v3 (?) = SB <uintptr> v10 (?) = LEAQ <*uint8> {type.int} v3 v11 (?) = LEAQ <*int> {"".statictmp_0} v3 v15 (?) = MOVQconst <int> [1] v20 (?) = MOVQconst <uintptr> [0] v25 (?) = MOVQconst <*uint8> [0] v1 (?) = InitMem <mem> v6 (6) = VarDef <mem> {.autotmp_0} v1 v7 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2 v9 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2 v16 (+6) = LEAQ <*interface {}> {.autotmp_0} v2 v18 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2 v21 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2 v30 (6) = LEAQ <*int> [16] v2 v19 (6) = LEAQ <*int> [8] v2 v23 (6) = MOVOconst <int128> [0] v8 (6) = MOVOstore <mem> {.autotmp_0} v2 v23 v6 v22 (6) = MOVQstore <mem> {.autotmp_0} v2 v10 v8 v17 (6) = MOVQstore <mem> {.autotmp_0} [8] v2 v11 v22 v14 (6) = MOVQstore <mem> v2 v9 v17 v28 (6) = MOVQstoreconst <mem> [val=1,off=8] v2 v14 v26 (6) = MOVQstoreconst <mem> [val=1,off=16] v2 v28 v27 (6) = CALLstatic <mem> {fmt.Println} [48] v26 v29 (5) = VarKill <mem> {.autotmp_0} v27 Ret v29 (+7)
在HTML中,某些行是灰色的,這意味着它們將在下一個階段中被刪除或修改。
例如,v15 (?) = MOVQconst <int> [1]
顯示爲灰色。點擊 v15
,咱們看到它在其餘地方都沒有使用,而 MOVQconst
基本上與咱們以前看到的 Const64
相同,只針對amd64的特定機器。咱們把 v15
設置爲1。可是,v15
在其餘地方都沒有使用,因此它是無用的(死的)代碼而且能夠消除。
Go 編譯器應用了不少這類優化。所以,雖然 AST
生成的初始 SSA
可能不是最快的實現,但編譯器將SSA優化爲更快的版本。 HTML 文件中的每一個階段都有可能發生優化。
若是你有興趣瞭解 Go 編譯器中有關 SSA
的更多信息,請查看 Go 編譯器的 SSA 源代碼。
這裏定義了全部的操做以及優化。
Go 是一種很是高效且高性能的語言,由其編譯器及其優化支撐。要了解有關 Go 編譯器的更多信息,源代碼的 README 是不錯的選擇。