轩脉刃de刀光剑影

轩脉刃de刀光剑影

你好,我是轩脉刃。

在golang中,我们可以使用go tool compile -S main.go 工具将一个go程序直接转换为汇编代码。但是你会发现,最终编译出来的汇编代码其实是已经被优化过了的,编译器其实很聪明,甚至将一些函数合并,取消等。至于这个过程,并不是一蹴而就的,在golang代码和最终的汇编代码中,还有一种中间的代码结构,这个结构就叫做SSA (Static Single Assignment) 静态单赋值。

这个中间的代码结构是有必要存在的,go源码解析后是一个AST树,是一个树形结构,而最终的汇编是一条一条的线性命令。将树形结构转化拆分优化为汇编命令是比较复杂的。所以这里将这么一个大的步骤分成两步走,能大大降低编译器优化的难度。

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

怎么生成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:终于知道编译器偷摸做了哪些事-LMLPHP

生成ssa.html

怎么看ssa

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

这个html中的ssa中间语言的语法是由 cmd/compile/internal/ssa/gen/genericOps.go 生成的。

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

每一行和对应的SSA代码都标记出来了,有一些即使没有SSA的经验,也是能立马看懂的。比如像v10 是常量1,而v13是代表指针指向a[0], v14 代表将常量1存储进入a[0]。不过有一些则不是那么容易看出了。

通过中间可以看出过了很多优化步骤才最终生成了汇编码。

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

有哪些步骤可以参考这里: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就看出了。

最开始的源码

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

切换为AST树

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

再变成SSA语言

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

经过不断优化,变成三个执行语言。(其实这个foo函数直接可以在编译阶段将5返回)

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

最后再变化为汇编码:

SSA:终于知道编译器偷摸做了哪些事-LMLPHP

这个编译器优化的过程,我感觉对于语言使用者还是主要适用于纯研究。

比如想研究下数组是在栈上分配内存还是在静态数据区分配内存,可以生成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

12-22 23:42