SSA:終于知道 Go 編譯器偷摸做了哪些事
在go的源碼和匯編碼之間,其實(shí)編譯器在你眼皮底下偷偷又做了不少事情,而ssa就是查看查看編譯器優(yōu)化行為的利器。
在golang中,我們可以使用go tool compile -S main.go 工具將一個go程序直接轉(zhuǎn)換為匯編代碼。但是你會發(fā)現(xiàn),最終編譯出來的匯編代碼其實(shí)是已經(jīng)被優(yōu)化過了的,編譯器其實(shí)很聰明,甚至將一些函數(shù)合并,取消等。至于這個過程,并不是一蹴而就的,在golang代碼和最終的匯編代碼中,還有一種中間的代碼結(jié)構(gòu),這個結(jié)構(gòu)就叫做SSA (Static Single Assignment) 靜態(tài)單賦值。
這個中間的代碼結(jié)構(gòu)是有必要存在的,go源碼解析后是一個AST樹,是一個樹形結(jié)構(gòu),而最終的匯編是一條一條的線性命令。將樹形結(jié)構(gòu)轉(zhuǎn)化拆分優(yōu)化為匯編命令是比較復(fù)雜的。所以這里將這么一個大的步驟分成兩步走,能大大降低編譯器優(yōu)化的難度。
怎么生成ssa
我們可以使用命令 GOSSAFUNC=Foo go build index.go ?來看我們將一個go源碼,怎么轉(zhuǎn)化為SSA的全過程的。
go代碼
package?array
func?Foo?()?int?{
?a?:=?[3]int{1,3,5}
?i?:=?2
?elem?:=?a[i]
?return?elem
}

生成ssa.html
怎么看ssa

這個html中的ssa中間語言的語法是由 cmd/compile/internal/ssa/gen/genericOps.go 生成的。

每一行和對應(yīng)的SSA代碼都標(biāo)記出來了,有一些即使沒有SSA的經(jīng)驗(yàn),也是能立馬看懂的。比如像v10 是常量1,而v13是代表指針指向a[0], v14 代表將常量1存儲進(jìn)入a[0]。不過有一些則不是那么容易看出了。
通過中間可以看出過了很多優(yōu)化步驟才最終生成了匯編碼。

有哪些步驟可以參考這里:https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/ssa/compile.go#L418
至于每個步驟做了什么事情,這個就很復(fù)雜了。
關(guān)于ssa
關(guān)于ssa,我自己的理解就是,將源碼的AST樹,先演變成像
v1=?xxx
v2=?xxx
v3=?xxx
這種線性執(zhí)行語句。這種語句的特點(diǎn)就是每一行都定義了一個變量。所以叫“靜態(tài)單賦值語句”。然后使用各種之間的賦值規(guī)則,可以很容易看出哪些賦值變量其實(shí)是沒有用到的。對于沒有用到的直接可以刪除。當(dāng)然還有其他各種規(guī)則,最終將v1...vn的賦值變量進(jìn)行預(yù)計算,優(yōu)化,最后優(yōu)化為最簡的幾個賦值變量。這點(diǎn)可以從ssa.html的start到最后的trim就看出了。
最開始的源碼

切換為AST樹

再變成SSA語言

經(jīng)過不斷優(yōu)化,變成三個執(zhí)行語言。(其實(shí)這個foo函數(shù)直接可以在編譯階段將5返回)

最后再變化為匯編碼:

這個編譯器優(yōu)化的過程,我感覺對于語言使用者還是主要適用于純研究。
比如想研究下數(shù)組是在棧上分配內(nèi)存還是在靜態(tài)數(shù)據(jù)區(qū)分配內(nèi)存,可以生成ssa看看。
或者想研究下哪行代碼對應(yīng)哪個內(nèi)部函數(shù)等。
參考:
https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
https://oftime.net/2021/02/14/ssa/
https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-ir-ssa/
https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/README.md
https://en.wikipedia.org/wiki/Static_single_assignment_form
推薦閱讀
