你好,我是軒脈刃。
在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]。不過有一些則不是那麼容易看出了。
通過中間可以看出過了很多優化步驟才最終生成了彙編碼。
有哪些步驟可以參考這裡:https://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看看。
或者想研究下哪行程式碼對應哪個內部函式等。
參考:
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