golang快速入門[4]-go語言如何編譯爲機器碼

前文

package main
import "fmt"
func main() {
    fmt.Println("Hello, world")
}
  • 在本文中,咱們將介紹初學者比較關心的話題:go語言如何編譯爲機器碼編程

  • 本文的目標是但願讀者對go語言的編譯過程有一個全面的理解windows

  • 一段程序要運行起來,須要將go代碼生成機器可以識別的二進制代碼閉包

  • go代碼生成機器碼須要編譯器經歷:
    詞法分析 => 語法分析 => 類型檢查 => 中間代碼 => 代碼優化 => 生成機器碼架構

  • Go語言的編譯器入口是 src/cmd/compile/internal/gc 包中的 main.go 文件,此函數會先獲取命令行傳入的參數並更新編譯的選項和配置app

  • 隨後就會開始運行 parseFiles 函數對輸入的全部文件進行詞法與語法分析

func Main(archInit func(*Arch)) {
    // ...

    lines := parseFiles(flag.Args())
  • 接下來咱們將對各個階段作深刻介紹

詞法分析

  • 全部的編譯過程都是從解析代碼的源文件開始的

  • 詞法分析的做用就是解析源代碼文件,它將文件中的字符串序列轉換成Token序列,方便後面的處理和解析

  • 咱們通常會把執行詞法分析的程序稱爲詞法解析器(lexer)

  • Token能夠是關鍵字,字符串,變量名,函數名

  • 有效程序的"單詞"都由Token表示,具體來講,這意味着"package","main","func" 等單詞都爲Token

  • Go語言容許咱們使用go/scanner和go/token包在Go程序中執行解析程序,從而能夠看到相似被編譯器解析後的結構

  • 若是在語法解析的過程當中發生了任何語法錯誤,都會被語法解析器發現並將消息打印到標準輸出上,整個編譯過程也會隨着錯誤的出現而被停止

  • 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     ""
  • 咱們能夠看到,詞法解析器添加了分號,分號經常是在C語言等語言中一條語句後添加的

  • 這解釋了爲何Go不須要分號:詞法解析器能夠智能地加入分號

語法分析

  • 語法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會按照順序被語法分析器進行解析,語法的解析過程就是將詞法分析生成的 Token 按照語言定義好的文法(Grammar)自下而上或者自上而下的進行規約,每個 Go 的源代碼文件最終會被概括成一個 SourceFile 結構:

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
  • 標準的 Golang 語法解析器使用的就是 LALR(1) 的文法,語法解析的結果生成了抽象語法樹(Abstract Syntax Tree,AST)

  • 抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。

  • 之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。好比,嵌套括號被隱含在樹的結構中,並無以節點的形式呈現;而相似於 if-condition-then 這樣的條件跳轉語句,可使用帶有三個分支的節點來表示。

  • 與AST相對應的是CST(Concrete Syntax Trees),讀者能夠在參考資料中拓展閱讀兩者的差異

  • 在AST中,咱們可以看到程序結構,例如函數和常量聲明

  • 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,常量,變量和函數

  • 爲了進一步理解,咱們看一下對其的圖形化抽象表示

golang[4]-1.png

  • 紅色表示與節點相對應的代碼

  • main函數包含了3個部分,名稱, 聲明, 主體

  • 名稱是單詞main的標識

  • 由Type字段指定的聲明將包含參數列表和返回類型

  • 主體由一系列語句組成,其中包含程序的全部行。在本例中,只有一行

  • fmt.Println語句由AST中的不少部分組成,由ExprStmt聲明。

  • ExprStmt表明一個表達式,其能夠是本例中的函數調用,也能夠是二進制運算(例如加法和減法等)

  • 咱們的ExprStmt包含一個CallExpr,這是咱們的實際函數調用。這又包括幾個部分,其中最重要的是FunArgs

  • Fun包含對函數調用的引用,由SelectorExpr聲明。在AST中,編譯器還沒有知道fmt是一個程序包,它也多是AST中的變量

  • Args包含一個表達式列表,這些表達式是該函數的參數。在本例中,咱們已將文字字符串傳遞給函數,所以它由類型爲STRINGBasicLit表示。

類型檢查

  • 構造AST以後,將會對全部import的包進行解析

  • 接着Go語言的編譯器會對語法樹中定義和使用的類型進行檢查,類型檢查分別會按照順序對不一樣類型的節點進行驗證,按照如下的順序進行處理:

    • 常量、類型和函數名及類型

    • 變量的賦值和初始化

    • 函數和閉包的主體

    • 哈希鍵值對的類型

    • 導入函數體

    • 外部的聲明


  • 經過對每一棵抽象節點樹的遍歷,咱們在每個節點上都會對當前子樹的類型進行驗證保證當前節點上不會出現類型錯誤的問題,全部的類型錯誤和不匹配都會在這一個階段被發現和暴露出來。

  • 類型檢查的階段不止會對樹狀結構的節點進行驗證,同時也會對一些內建的函數進行展開和改寫,例如 make 關鍵字在這個階段會根據子樹的結構被替換成 makeslice 或者 makechan 等函數。

  • 類型檢查不止對類型進行了驗證工做,還對 AST 進行了改寫以及處理Go語言內置的關鍵字

生成中間代碼

  • 在上面的步驟完成以後,能夠明確代碼是正確有效的

  • 接着將AST轉換爲程序的低級表示形式,即靜態單一賦值形式(Static Single Assignment Form,SSA)形式,核心代碼位於gc/ssa.go

  • SSA不是程序的最終狀態,其能夠更輕鬆地應用優化,其中最重要的是始終在使用變量以前定義變量,而且每一個變量只分配一次

  • 例以下面的代碼咱們能夠看到第一個x的賦值沒有必要的

x = 1
x = 2
y = 7
  • 編輯器會將上面的代碼變爲以下,從而會刪除x_1

x_1 = 1
x_2 = 2
y_1 = 7
  • 生成SSA的初始版本後,將應用許多優化過程。這些優化應用於某些代碼段,這些代碼段可使處理器執行起來更簡單或更快速。

  • 例以下面的代碼是永遠不會執行的,所以能夠被消除。

 if (false) {
     fmt.Println(「test」)
 }
  • 優化的另外一個示例是能夠刪除某些nil檢查,由於編譯器能夠證實這些檢查永遠不會出錯

  • 在對SSA進行優化的過程當中使用了S表達式(S-expressions)進行描述, S-expressions 是嵌套列表(樹形結構)數據的一種表示法,由編程語言Lisp發明並普及

  • SSA優化過程當中對於S表達式的應用以下所示,將8位的常量乘法組合起來

(Mul8 (Const8 [c]) (Const8 [d])) -> (Const8 [int64(int8(c*d))])
  • 具體的優化包括

    • 常數傳播(constant propagation)

    • 值域傳播(value range propagation)

    • 稀疏有條件的常數傳播(sparse conditional constant propagation)

    • 消除無用的程式碼(dead code elimination)

    • 全域數值編號(global value numbering)

    • 消除部分的冗餘(partial redundancy elimination)

    • 強度折減(strength reduction)

    • 寄存器分配(register allocation)


SSA優化

  • 咱們能夠用下面的簡單代碼來查看SSA及其優化過程

  • 對於以下程序

package main

import "fmt"

func main() {
  fmt.Println(2)
}
  • 咱們須要在命令行運行以下指令來查看SSA

  • GOSSAFUNC環境變量表明咱們須要查看SSA的函數並建立ssa.html文件

  • GOOS、GOARCH表明編譯爲在Linux 64-bit平臺運行的代碼

  • go build用-ldflags給go編譯器傳入參數

  • -S 標識將打印彙編代碼

$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go
  • 下面的命令等價

GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
  • 當打開ssa.html時,將顯示許多代碼片斷,其中一些片斷是隱藏的。

golang[4]-2.png

  • 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
  • 每一個v是一個新變量,能夠單擊以查看使用它的位置。

  • b是代碼塊,本例中咱們有3個代碼塊:b1, b2和 b3

  • b1將始終被執行,b2和b3是條件塊,如b1最後一行所示:If v20 → b2 b3 (likely) (6),只有v20爲true會執行b2,v20爲false會執行b3

  • 咱們能夠點擊v20查看其定義,其定義是v20 (6) = IsSliceInBounds v14 v15

  • IsSliceInBounds 會執行以下檢查:0 <= v14 <= v15 是否成立

  • 咱們能夠單擊v14和v15來查看它們的定義:v14 = Const64 [0] ,v15 = Const64 [1]

  • Const64爲64位常量,所以 0 <= 0 <= 1 始終成立,所以v20始終成立

  • 當咱們在opt片斷查看v20時,會發現v20 (6) = ConstBool [true],v20變爲了始終爲true

  • 所以,咱們會看到在opt deadcode片斷中,b3塊被刪除了

代碼優化

  • 生成SSA以後,Go編譯器還會進行一系列簡單的優化,例如無效和無用代碼的刪除

  • 咱們將用一樣的ssa.html文件,比較lower 和 lowered deadcode片斷

golang[4]-3.png

  • 在HTML文件中,某些行顯示爲灰色,這意味着它們將在下一階段之一中被刪除或更改

  • 例如v15 = MOVQconst [1]爲灰色,由於其在後面根本沒有被使用。MOVQconst與咱們以前看到的指令Const64相同,僅適用於amd64平臺

機器碼生成

  • 完成以上步驟,最終還會生成跨平臺的plan9彙編指令,並進一步根據目標的 CPU 架構生成二進制機器代碼

  • Go語言源代碼的 cmd/compile/internal 目錄中包含了很是多機器碼生成相關的包

  • 不一樣類型的 CPU 分別使用了不一樣的包進行生成 amd6四、arm、arm6四、mips、mips6四、ppc6四、s390x、x86 和 wasm

  • Go語言可以在幾乎所有常見的 CPU 指令集類型上運行。

問:go的編譯速度相對於java爲何更快

  • 快速編譯是go的設計目標之一

  • go語法緊湊且規則所以更容易解析

  • go具備嚴格的依賴管理,沒有循環依賴問題,計算依賴樹很是高效

  • 語言的設計易於分析,無需符號表便可進行解析

  • Go語言自己比Java簡單得多,編輯器自己作的事很少

  • Go編譯器較新,其中的無用代碼更少

總結

  • 在本文中詳細介紹了go語言從源代碼編譯爲機器碼的過程

  • 涉及了詞法分析、語法分析、類型檢查、SSA生成與代碼優化、生成機器碼等過程

  • 以期幫助讀者全面深刻的瞭解go語言的編譯過程

參考資料

相關文章
相關標籤/搜索