SSA:終於知道編譯器偷摸做了哪些事
你好,我是軒脈刃。
在golang中,我們可以使用go tool compile -S main.go
工具將一個go程序直接轉換為彙編代碼。但是你會發現,最終編譯出來的彙編代碼其實是已經被優化過了的,編譯器其實很聰明,甚至將一些函數合併,取消等。至於這個過程,並不是一蹴而就的,在golang代碼和最終的彙編代碼中,還有一種中間的代碼結構,這個結構就叫做SSA (Static Single Assignment) 靜態單賦值。
這個中間的代碼結構是有必要存在的,go源碼解析後是一個AST樹,是一個樹形結構,而最終的彙編是一條一條的線性命令。將樹形結構轉化拆分優化為彙編命令是比較複雜的。所以這裡將這麼一個大的步驟分成兩步走,能大大降低編譯器優化的難度。
怎麼生成ssa
我們可以使用命令 GOSSAFUNC=Foo go build index.go 來看我們將一個go源碼,怎麼轉化為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 生成的。
每一行和對應的SSA代碼都標記出來了,有一些即使沒有SSA的經驗,也是能立馬看懂的。比如像v10 是常量1,而v13是代表指針指向a[0], v14 代表將常量1存儲進入a[0]。不過有一些則不是那麼容易看出了。
通過中間可以看出過了很多優化步驟才最終生成了彙編碼。
有哪些步驟可以參考這裡://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/ssa/compile.go#L418
至於每個步驟做了什麼事情,這個就很複雜了。
關於ssa
關於ssa,我自己的理解就是,將源碼的AST樹,先演變成像
v1= xxx
v2= xxx
v3= xxx
這種線性執行語句。這種語句的特點就是每一行都定義了一個變量。所以叫「靜態單賦值語句」。然後使用各種之間的賦值規則,可以很容易看出哪些賦值變量其實是沒有用到的。對於沒有用到的直接可以刪除。當然還有其他各種規則,最終將v1…vn的賦值變量進行預計算,優化,最後優化為最簡的幾個賦值變量。這點可以從ssa.html的start到最後的trim就看出了。
最開始的源碼
切換為AST樹
再變成SSA語言
經過不斷優化,變成三個執行語言。(其實這個foo函數直接可以在編譯階段將5返回)
最後再變化為彙編碼:
這個編譯器優化的過程,我感覺對於語言使用者還是主要適用於純研究。
比如想研究下數組是在棧上分配內存還是在靜態數據區分配內存,可以生成ssa看看。
或者想研究下哪行代碼對應哪個內部函數等。
參考:
//gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
//draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-ir-ssa/
//github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/README.md
//en.wikipedia.org/wiki/Static_single_assignment_form