![Go语言底层原理剖析](https://wfqqreader-1252317822.image.myqcloud.com/cover/131/40795131/b_40795131.jpg)
1.12 SSA生成
遍历函数后,编译器会将抽象语法树转换为下一个重要的中间表示形态,称为SSA(Static Single Assignment,静态单赋值)。SSA被大多数现代的编译器(包括GCC和LLVM)使用,在Go 1.7中被正式引入并替换了之前的编译器后端,用于最终生成更有效的机器码。在SSA生成阶段,每个变量在声明之前都需要被定义,并且,每个变量只会被赋值一次。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_33_1.jpg?sign=1739945978-pnQLGjZxjf6pRGWA5LPdxhVdi6V4sncu-0-cd6e1b32cc4c446daa3c7215f055da05)
例如,在上面的代码中,变量y被赋值了两次,不符合SSA的规则,很容易看出,y:=1这条语句是无效的。可以转化为如下形式:
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_33_2.jpg?sign=1739945978-54apwyo8C047deH6VuAgvhBnW8Tkj1GP-0-39d423e02ae58c85882e5de936d8d7fa)
通过SSA,很容易识别出y1是无效的代码并将其清除。
条件判断等多个分支的情况会稍微复杂一些,如下所示,假如我们将第一个x变为x_1,条件变量括号内的x变为x_2,那么f(x)中的x应该是x_1还是x_2呢?
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_33_3.jpg?sign=1739945978-wQB75cDg9mV4cg7YHdL6opsjBisuYGFG-0-45e1fa7c6e3c856a3591aeaa375bc081)
为了解决以上问题,在SSA生成阶段需要引入额外的函数Φ接收x_1和x_2产生新的变量x_v,x_v的大小取决于代码运行的路径,如图1-8所示。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_33_4.jpg?sign=1739945978-GzCyIVoik8AwpfsQKkyjMskugWg2cNHZ-0-0ccc54d629d270d8f70eba779b2f1706)
图1-8 SSA生成阶段处理多分支下的单一变量名
SSA生成阶段是编译器进行后续优化的保证,例如常量传播(Constant Propagation)、无效代码清除、消除冗余、强度降低(Strength Reduction)等[4]。
大部分与SSA相关的代码位于ssa/文件夹中,但是将抽象语法树转换为SSA的逻辑位于gc/ssa.go文件中。在ssa/README.md文件中,有对SSA生成阶段比较详细的描述。
Go语言提供了强有力的工具查看SSA初始及其后续优化阶段生成的代码片段,可以通过在编译时指定GOSSAFUNC=main实现。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_34_1.jpg?sign=1739945978-eEbYa5MJHx1zEtroE9Iw8IZ76CC41RjR-0-a965faea260e06469541bee8945b19bc)
以上述代码为例,可以通过如下指令生成ssa.html文件。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_34_2.jpg?sign=1739945978-p38GzFqmns3Bqg5y0ze6o0rPFQ0TBFZI-0-b9a2bfa93a80b69fda34be59136301f9)
通过浏览器打开ssa.html文件,将看到图1-9所示的许多代码片段,其中一些片段是隐藏的。这些是SSA的初始阶段、优化阶段、最终阶段的代码片段。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_34_3.jpg?sign=1739945978-veSeGOOgcFG60flrg300jbuPF8XugvYb-0-8f52bebfa51d657516e9e5f1712cf54d)
图1-9 SSA所有优化阶段的代码片段
以如下最初生成SSA代码的初始(start)阶段为例,其中,bN代表不同的执行分支,例如b1、b2、b3。vN代表变量,每个变量只能被分配一次,变量后的Op操作代表不同的语义,与特定的机器无关。例如Addr代表取值操作,Const8代表常量,后接要操作的类型;Store代表赋值是与内存有关的操作。Go语言编译器采取了特殊的方式处理内存操作,例如v11中Store的第三个参数代表内存的状态,用于确定内存的依赖关系,从而避免编译器内存的重排。另外,v8的取值取决于判断语句是否为true,这就是之前介绍的函数Φ。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_35_1.jpg?sign=1739945978-qB0JrANngGGbVzmoTVaSC7MLAoG908A7-0-58f8aa101e5a4e4116c74734801594f2)
初始阶段结束后,编译器将根据生成的SSA进行一系列重写和优化。SSA最终的阶段叫作genssa,在上例的genssa阶段中,编译器清除了无效的代码及不会进入的if分支,并且将常量Op操作变为了amd64下特定的MOVBstoreconst操作。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_35_2.jpg?sign=1739945978-PCG1FR0xILBBQaW9AfDKMYGr6TU2nWvg-0-f5c49efbce70ab811bb60cb22e59d2ba)