- Go语言底层原理剖析
- 郑建勋
- 942字
- 2021-10-15 18:09:00
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=1739393625-GEbreigJxMsgrwE9ep9twpwVUmi2GvgT-0-3ce209d07b9a22190565f229c9ef9aeb)
例如,在上面的代码中,变量y被赋值了两次,不符合SSA的规则,很容易看出,y:=1这条语句是无效的。可以转化为如下形式:
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_33_2.jpg?sign=1739393625-BBtcZXuNU52dqzIfB4k4ogGGWvueYPtV-0-3a123b58c0f1f0ff4d1c7798ca74421f)
通过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=1739393625-ntmGKPj9XpBzblKbRx1TDitbosHdaAqm-0-54be3db61499954529d46ef4d7f64b32)
为了解决以上问题,在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=1739393625-Vce8PzTHOLvyrcYidhNueZSqKrwDwUme-0-8a02094a39e1fc2919622a140ff56ba7)
图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=1739393625-2NhkjA72mziCO0OVcuvYCv2qOijauNUU-0-636e413e976b49982f02bc0b9f8a706b)
以上述代码为例,可以通过如下指令生成ssa.html文件。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_34_2.jpg?sign=1739393625-moLYRvPvTU1teO8QCHIq056FNxREmlC2-0-911d769f5c29e8525a2ad6c31565af77)
通过浏览器打开ssa.html文件,将看到图1-9所示的许多代码片段,其中一些片段是隐藏的。这些是SSA的初始阶段、优化阶段、最终阶段的代码片段。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_34_3.jpg?sign=1739393625-AbmVRXmLMszQvzmhAHlwmkSAU1paf32p-0-c31431c55ed6f2af33e7f947082d783d)
图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=1739393625-R6jjD6fRoue8pOmF20wIwpzSUlBVT8SY-0-86c8976835f38232d2ab1f8ce619aa2c)
初始阶段结束后,编译器将根据生成的SSA进行一系列重写和优化。SSA最终的阶段叫作genssa,在上例的genssa阶段中,编译器清除了无效的代码及不会进入的if分支,并且将常量Op操作变为了amd64下特定的MOVBstoreconst操作。
![](https://epubservercos.yuewen.com/88BA42/21190707608528606/epubprivate/OEBPS/Images/41662_35_2.jpg?sign=1739393625-g4VhMN67kMCyow0OKWcqat5IyeGv50Cn-0-a161781f409b008b6877ffc208e6390c)