转载

[译] Go 1.7 中程序结构的解析

Go 1.7终于发布了,这个版本的一大特色是引入了一个新的基于SSA的编译器! SSA是一个描述低层次的操作集合的方法,比如隐射到系统底层操作的加载和存储操作。而SSA的特殊之处在于它有无限数量的寄存器。它本身不是特别有趣,除了它启用了一类很容易理解的优化传值这一点以外,因为这个特性使得编译速度更快,编译出的二进制文件更小。Go的新版本发布说明实现正在成熟,并且开始利用llvm(wider world of compiler technology)中的技术和实践。

新的基于SSA后端除了带来的性能提升,还提供了一系列的新工具允许开发者与SSA机制交互。这个工具输出了SSA过程语句,优化传值,和Go特有的汇编。当使用go工具来反汇编时,可以通过设置GOSSAFUNC环境变量为函数名,比如:

$ GOSSAFUNC=main go build

这个调用会输出到终端,但是更有趣的输出是一个HTML格式的文件,名为 ssa.html ,输出到了当前目录。在浏览器打开该文件,你会看到类似这样的内容:

[译] Go 1.7 中程序结构的解析 你所看到的是一个表格,这个表格的很多列在右侧,除了第一列和最后一列之外的列代表了通过前述的SSA表单的优化(我数过了,共37个单独的传值)。第一列是编译器的初始化,未优化的SSA输出,最后一列是Go特有的汇编,然后会被转换为机器码,生成最终的二进制文件或者共享库。

[译] Go 1.7 中程序结构的解析

虽然这对于新手可以令人生畏,但是SSA是相对简单的设计---每一行代表了赋给一个指令的结果的值(比如一个无限数量的寄存器)或者一个基本块的标签(语句集合,嗯,在花括号之间的源代码),或者基本块的退出,它会跳转到不同的基本块执行(比如if语句控制流或者从一个函数调用返回)。

比如:

v4 = Const64 <int> [42]

意思是赋值给64位的整形常量42给标签为V4的寄存器。

b5: ← b4  v15 = Copy <mem> v14  v16 = StaticCall <mem> {runtime.printnl} v15 Call v16 → b6

意思是b5是有两个语句的基本块的标签。它包含退出Call指令,使得程序执行到另外的一个基本块,b6,从函数中返回时产生了v16值。

像Const64、Copy和StatiCall这样的符号与汇编指令MOV和LEA类似。

有一个特殊的操作是Phi,或者“Phi code”。注意到一个Phi节点带有2个参数,也就是2个值。而且注意到一个带有Phi节点的基本块有2个基本块标签,靠近它自己的标签,这点和其他的基本块不同:

b3: ← b1 b2   v20 = Phi <int> v4 v6   ...

这是个有趣的构造,它和程序控制流相关。一个基本块是通过一个单一的entry和一个单一的退出点来定义,并且有一个可以按照顺序执行的语句集合(比如,没有分支逻辑)。SSA代表“单一的静态赋值”,意味着每个值被赋给一个,而且每次只有一个。但是如果你有一个变量的引用,依赖于if语句,可能有不同的值呢?Phi节点是解决这种明显冲突的一种方式。因为每个if语句的分钟可以定义为赋给唯一的值,所以一个Phi节点可以依赖于最终采用的哪个分支,把它们合并为一个最终的值。所以,你可以它想象在运行时基于一些条件会重新获取值。这也是为什么块在顶部有2个依赖,而不是一个的原因。

让我们写一个无聊的程序来激发更多的例子:

package main func main() {    x := 5    if 1 < 0 {        x = -42    }    println(x) }

我们以最初的基本块b1开始:

b1:  v1 = InitMem <mem>  v2 = SP <uintptr>  v3 = SB <uintptr>  v4 = Const64 <int> [5]  v5 = ConstBool <bool> [false]  v6 = Const64 <int> [-42]  v11 = OffPtr <*int64> [0] v2 If v5 → b2 b3

在一些程序初始化后,v4是给本地变量x设置为常量5的赋值语句。 Go在编译阶段就知道1<0肯定是false,所以它直接给v5赋值给false。v6是给x赋值-42,这发生在程序执行时。

最终,我们的基本块退出,If v5 → b2 b3。这个测试了v5的真实值,来决定是执行b2(如果为true)还是b3(如果为false)。这和下面的汇编块类似:

JNZ b2 b3:  ... b2:  ...

一个关于Go SSA HTML 界面的好事情是,你可以在SSA表单的任何符号上点击,它会高亮引用到该元素或者从该元素得到的相关代码。

[译] Go 1.7 中程序结构的解析

我们可以从不同的颜色看到控制流是如何走的。你可以直观地连接那些代码块,这些代码块可以执行、赋值、函数调用,和其他结果分支。

在Phi代码和它的依赖上点击,你可以看到值是来自于前面的控制流的哪个位置。

[译] Go 1.7 中程序结构的解析 继续往下,在下面的基本块中函数调用输出了整型值的内容:

b4: ← b3  v9 = Copy <int> v20  v10 = Copy <int64> v9  v12 = Copy <mem> v8  v13 = Store <mem> [8] v11 v10 v12  v14 = StaticCall <mem> {runtime.printint} [8] v13 Call v14 → b5

StaticCall指令从Go 运行时调用了格式化整型的函数,然后在终端打印输出。一个有趣的事情是,注意前面的调用提前设置好了一些事情,也就是在那里产生了printint函数。如果你注意到,v11指回到了在b1设置的值,也就是v2的指针偏移量。而v2是在栈指针SP中设置的,SP在程序初始化的顶部。这样做很有意义,因为当调用需要指针的函数时,产生的汇编语言需要连接内存位置到地址。

还有更多的需要继续深究,包括特殊的优化传值,跟踪独立指令是如何走通到最终的汇编或者被消除。但是,希望这篇文章给你介绍了SSA以及在你的应用程序中的映射机制。

原文  https://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&mid=2653369746&idx=1&sn=c190bd4c3591db7152ebac3a84a59f20&scene=1&srcid=0820lKcAWPZNcachmb99dM9a&pass_ticket=XWpSJqoKagI1wW2OAUYcTR4JLcogoZTBtss/IpoOHKZPWhZvHawFYFy3HPh56uNA
正文到此结束
Loading...