SSA:终于知道 Go 编译器偷摸做了哪些事
在go的源码和汇编码之间,其实编译器在你眼皮底下偷偷又做了不少事情,而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]。不过有一些则不是那么容易看出了。
通过中间可以看出过了很多优化步骤才最终生成了汇编码。
有哪些步骤可以参考这里: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
https://en.wikipedia.org/wiki/Static_single_assignment_form
推荐阅读