漫談Go語言編譯器(01)
開場白
為什么學習編譯器
雖然本系列的定位是科普文,但是我也不準備從最基本的正則表達式,語法樹,有向圖等最基礎(chǔ)的知識講起;因此假設讀者有一定的知識基礎(chǔ)。
在這個前提下,我希望你看過我在Gopher China 2020上的講座,并閱讀過PPT(https://github.com/gopherchina/conference/tree/master/2020)。
除此之外,我希望你閱讀過柴樹杉大神寫的《Go語法樹入門》(https://github.com/chai2010/go-ast-book),這是關(guān)于編譯前端非常優(yōu)秀的入門書籍。在此基礎(chǔ)上,本系列的重點是講解編譯中端和后端。
坦白地說,編譯器確實是最難入門的領(lǐng)域;同時也是最難寫科普文的領(lǐng)域:專業(yè)人士看了覺得淺顯,沒相關(guān)基礎(chǔ)的讀者看了覺得云山霧罩。雖然如此,我還是想挑戰(zhàn)一下。希望能收到讀者更多反饋,我會據(jù)此調(diào)節(jié)講解的內(nèi)容。

Go編譯器在go-1.7之前,采用的是非常老舊的語法樹(AST)覆蓋的編譯技術(shù),從go-1.7開始逐步采用更主流的基于SSA-IR的前中后三階段編譯技術(shù)。雖然Go編譯器無需支持別的高級編程語言,但是上述的第二點和第三點好處仍然適用。
一個額外的好處是,Go編譯器的中端和后端被做成了獨立的庫"golang.org/x/tools/go/ssa"。這就意味著,你可以自己寫一個新的語言前端(甚至不兼容Go語法),并調(diào)用Go的ssa庫,生成可執(zhí)行程序;甚至于你自己定義的編程語言可以無縫地調(diào)用其它Go的代碼;進一步,你可以借助于Go的生態(tài)系統(tǒng)打造自己的編程語言!類似于Scala語言和Java語言的關(guān)系。
SSA-IR(Single Static Assignment)是一種介于高級語言和匯編語言的中間形態(tài)的偽語言,從高級語言角度看,它是(偽)匯編;而從真正的匯編語言角度看,它是(偽)高級語言。
顧名思義,SSA(Single Static Assignment)的兩大要點是:
Static:每個變量只能賦值一次(因此應該叫常量更合適);
Single:每個表達式只能做一個簡單運算,對于復雜的表達式a*b+c*d要拆分成:"t0=a*b; t1=c*d; t2=t0+t1;"三個簡單表達式;
例如有如下Go源代碼:
func foo(a, b int) int {c := 8return a*4 + b*c}
它改寫成SSA形式是:
func foo(a, b int) int {c := 8t0 := a * 4t1 := b * ct2 := t0 + t1return t2}
func foo(a, b int) int {t0 := a << 2t1 := b << 3t2 := t0 + t1return t2}
func foo(a, b int) int {if (a > b) {return a + b} else {return a - b}
func foo(a, b int) int {c := a > bif (c) goto _true; else goto _false;_true:t0 := a + breturn t0_false:t1 := a - breturn t1}
Go編譯器提供了完備的調(diào)試手段,正好我們可以借用過來展示Go編譯器的內(nèi)部工作流程。本系列文章使用go-1.14.15做演示,請讀者安裝此版本。
下面用一個例子test.go:
// test.gopackage mainfunc foo(a, b int) int {c := 8return a*4 + b*c}func main() {println(foo(100, 150))}
使用如下命令編譯,在得到可執(zhí)行目標程序的同時,還會得到一個額外的ssa.html,這個ssa.html就記錄了Go編譯器的工作流程和各階段的中間結(jié)果。其中GOSSAFUNC環(huán)境變量用于指定需要被調(diào)試的函數(shù),本例中是foo。
$ GOSSAFUNC=foo go build a.go# command-line-argumentsdumped SSA to ./ssa.html

其中,source和AST屬于編譯器前端,從start到writebarrier屬于編譯器中端,從lower到genssa屬于編譯器后端。
這其中的start/writebarrier/genssa三道工序的輸出,請讀者認真看一下。
start工序是編譯器前端的最后一步,輸出原始IR序列,請讀者對照源碼仔細體會。v10對應變量c,v11對應第一個乘法運算的乘數(shù)4,v12是第一個乘法的積,v13是第二個乘法的積,v14是加法的和。
startb1:v1 (?) = InitMem <mem>v2 (?) = SP <uintptr>v3 (?) = SB <uintptr>v4 (?) = LocalAddr <*int> {a} v2 v1v5 (?) = LocalAddr <*int> {b} v2 v1v6 (?) = LocalAddr <*int> {~r2} v2 v1v7 (4) = Arg <int> {a} (a[int])v8 (4) = Arg <int> {b} (b[int])v9 (?) = Const64 <int> [0]v10 (?) = Const64 <int> [8] (c[int])v11 (?) = Const64 <int> [4]v12 (6) = Mul64 <int> v7 v11v13 (6) = Mul64 <int> v8 v10v14 (6) = Add64 <int> v12 v13v15 (6) = VarDef <mem> {~r2} v1v16 (6) = Store <mem> {int} v6 v14 v15Ret v16 (+6)
writebarrier是編譯器中端的最后一步,輸出經(jīng)通用優(yōu)化后的IR序列。讀者可以看到,最初的乘法運算(Mul64)被優(yōu)化成了移位運算(Lsh64x64)。
writebarrier [549 ns]b1:v1 (?) = InitMem <mem>v2 (?) = SP <uintptr>v6 (?) = LocalAddr <*int> {~r2} v2 v1v7 (4) = Arg <int> {a} (a[int])v8 (4) = Arg <int> {b} (b[int])v15 (6) = VarDef <mem> {~r2} v1v9 (+6) = Const64 <uint64> [2]v5 (6) = Const64 <uint64> [3]v12 (+6) = Lsh64x64 <int> [false] v7 v9v13 (6) = Lsh64x64 <int> [false] v8 v5v14 (6) = Add64 <int> v12 v13v16 (6) = Store <mem> {int} v6 v14 v15Ret v16 (+6)
本文介紹了理解Go編譯器的所需背景知識,以及Go編譯器的整體工作流程。后續(xù)的文章會逐步針對上面的各道工序展開講解。
