Go语言底层原理剖析
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.12 SSA生成

遍历函数后,编译器会将抽象语法树转换为下一个重要的中间表示形态,称为SSA(Static Single Assignment,静态单赋值)。SSA被大多数现代的编译器(包括GCC和LLVM)使用,在Go 1.7中被正式引入并替换了之前的编译器后端,用于最终生成更有效的机器码。在SSA生成阶段,每个变量在声明之前都需要被定义,并且,每个变量只会被赋值一次。

例如,在上面的代码中,变量y被赋值了两次,不符合SSA的规则,很容易看出,y:=1这条语句是无效的。可以转化为如下形式:

通过SSA,很容易识别出y1是无效的代码并将其清除。

条件判断等多个分支的情况会稍微复杂一些,如下所示,假如我们将第一个x变为x_1,条件变量括号内的x变为x_2,那么f(x)中的x应该是x_1还是x_2呢?

为了解决以上问题,在SSA生成阶段需要引入额外的函数Φ接收x_1和x_2产生新的变量x_v,x_v的大小取决于代码运行的路径,如图1-8所示。

图1-8 SSA生成阶段处理多分支下的单一变量名

SSA生成阶段是编译器进行后续优化的保证,例如常量传播(Constant Propagation)、无效代码清除、消除冗余、强度降低(Strength Reduction)等[4]

大部分与SSA相关的代码位于ssa/文件夹中,但是将抽象语法树转换为SSA的逻辑位于gc/ssa.go文件中。在ssa/README.md文件中,有对SSA生成阶段比较详细的描述。

Go语言提供了强有力的工具查看SSA初始及其后续优化阶段生成的代码片段,可以通过在编译时指定GOSSAFUNC=main实现。

以上述代码为例,可以通过如下指令生成ssa.html文件。

通过浏览器打开ssa.html文件,将看到图1-9所示的许多代码片段,其中一些片段是隐藏的。这些是SSA的初始阶段、优化阶段、最终阶段的代码片段。

图1-9 SSA所有优化阶段的代码片段

以如下最初生成SSA代码的初始(start)阶段为例,其中,bN代表不同的执行分支,例如b1、b2、b3。vN代表变量,每个变量只能被分配一次,变量后的Op操作代表不同的语义,与特定的机器无关。例如Addr代表取值操作,Const8代表常量,后接要操作的类型;Store代表赋值是与内存有关的操作。Go语言编译器采取了特殊的方式处理内存操作,例如v11中Store的第三个参数代表内存的状态,用于确定内存的依赖关系,从而避免编译器内存的重排。另外,v8的取值取决于判断语句是否为true,这就是之前介绍的函数Φ。

初始阶段结束后,编译器将根据生成的SSA进行一系列重写和优化。SSA最终的阶段叫作genssa,在上例的genssa阶段中,编译器清除了无效的代码及不会进入的if分支,并且将常量Op操作变为了amd64下特定的MOVBstoreconst操作。