静态单赋值(SSA,Static Single-Assignment)

精确的数据流分析是让编译优化能高效进行的基础。 SSA就是一种高效的数据流分析技术,目前几乎所有的现代编译器,如GCC、Open64、LLVM都有将SSA技术的支持, 不仅仅是编译器,Jikes RVM, HotSpot JVM, .Net的Mono,Python的Pypy, Andoroid的Dalvik,这些虚拟机/解释器中的Just-in-Time Compiler也有了SSA的支持。 Firefox的下一代JavaScript引擎IonMonkey中,也将为其JIT引入SSA。

是一种中间表示形式。 之所以称之为单赋值,是因为每个名字在SSA中仅被赋值一次.将每个赋值语句中的变量赋予一个唯一的名称后,一般新名称采用原变量+版本号(Version)的形式。



静态单赋值(SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次,在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性,这里以下面的代码举一个简单的例子:



x := 1
x := 2
y := x
根据分析,我们其实能够发现上述的代码其实并不需要第一个将 1 赋值给 x 的表达式,也就是这一表达式在整个代码片段中是没有作用的:



x1 := 1
x2 := 2
y1 := x2
从使用 SSA 的『中间代码』我们就可以非常清晰地看出变量 y1 的值和 x1 是完全没有任何关系的,所以在机器码生成时其实就可以省略第一步,这样就能减少需要执行的指令来优化这一段代码。



SSA带来四大益处:



因为SSA使得每个变量都有唯一的定义,因此数据流分析和优化算法可以更加简单
使用-定义关系链所消耗空间从指数增长降低为线性增长。若一个变量有N个使用和M个定义,若不采用SSA,则存在M×N个使用-定义关系。
SSA中因为使用和定义的关系更加的精确,能简化构建干扰图的算法
源程序中对同一个变量的不相关的若干次使用,在SSA形式中会转变成对不同变量的使用,因此能消除很多不必要的依赖关系。
有了精确的对象使用–定义关系,许多利用使用–定义关系的优化就能更精确、更彻底、更高效。如



常数传播
死代码删除
全局
部分冗余删除
强度削弱
寄存器分配
2.1 SSA与寄存器分配
因为SSA使得依赖分析更加简单、精确,而且PHI节点中的变量不可能同时活跃。因此在SSA形式能协助完成寄存器分配。 实际上,GCC最早的SSA就是GCC 3中RTL阶段。



3 SSA的转换
讲了这么多有关SSA的优点,接下来介绍一下一般编译器构建SSA的方式。



3.1 从普通中间表示到SSA
两步走战略:



插入PHI节点: PHI节点要插在控制流图的汇聚点处(joint point), 只要在汇聚点之前的分支中有针对某个变量的修改, 就需要在该汇聚点插入针对该变量的PHI节点。 PHI节点的操作数是分支路径中重新定义的变量。
变量重命名: 在插入PHI节点后,SSA中所有针对变量的定义就具备了,接下来就依次在定义处重命名变量,并替换对应的变量使用处。
此外,为了节省内存空间,简化SSA上的算法,我们需要将插入的PHI节点数目最小化。 因为PHI节点本身只是一个概念性的节点,若插入过多不必要的PHI节点,算法就需要在控制流图的汇聚点针对每个分支做分析。 可以借用变量的支配边界(dominance frontier)进行PHI节点数目最消化。一般都通过直接计算支配边界的方式插入PHI节点。



3.2 从SSA到普通中间表示
为什么还要从SSA转换回去呢?很简单,处理器不能直接执行PHI节点对应的操作。最简单的做法,直接拷贝
简单的拷贝算法可能改变代码的语义
正确的做法:



对PHI节点的操作数和结果重命名,使其名称相同,即变成同一个变量
再在分支中插入拷贝操作
4 更复杂情况下的SSA
4.1 数组、指针等别名
上面关于SSA的讨论基本都是针对单个简单变量的SSA操作,那么对于复杂的指针、数组之类的访存,SSA应该如何处理呢? 数组和指针使得编译器无法确定define和use的具体变量。



参考资料7给出了一种定义方式,通过引入maydef,mayuse和zero version使得编译器也能对别名(即指针和数组)存在的程序做SSA分析。 若通过指针为其所指区域赋值,就在此处插入maydef,表示可能对变量做了定义。同理,对使用指针所指向区域的值的,就插入一个mayuse。 因为无法确定指针所指向的到底是哪个变量,为了正确性,需要对所有变量都插入maydef动作。同样mayuse也是针对所有变量的。



当指针操作较多时,这种方式就会引入过多的新变量版本。因此就增加了zero version。 zero version的作用就是尽量把maydef所带来的版本数降低。 将那些很可能不会别名的都使用相同的zero version。 比如某个变量通过maydef产生了一个新版本之后,若还会有新的maydef操作,则直接生成zero version,不再生成新的version。



4.2 堆上的存储
在堆上分配的存储空间,一般编译器都将整个堆看作一个对象,来做SSA。



4.3 复合结构–结构体
因为结构体也是由很多元素构成的,所以就存在两种处理方式:把结构体整个看作一个整体做SSA、把结构体的每个元素看作一个对象做SSA。 后者相比前者,因为分的更细,在结构体操作频繁的程序中能带来不错的优化效果。



5 GCC中的SSA
GCC的SSA



tree-ssa.c
tree-into-ssa.c:将函数转换为SSA形式,插入PHI节点,对于未初始化的变量给出警告。
tree-ssa-dce.c:扫描整个函数,标记无副作用且结果并未被使用的语句,所有存储操作都视为有副作用。
tree-ssa-dom.c:支配关系相关优化:复写传播、常数传播、表达式简化、冗余消除、Jump Threading?
tree-ssa-forwprop.c:前向传播单一引用变量,通过将仅使用一次的变量用相应的表达式替代来尝试容易删除。
tree-ssa-copyrename.c:尝试将由拷贝动作产生的SSA变量用原变量替换之,优化符号表。
tree-ssa-phiopt.c: 识别表达条件表达式的phi节点,并将其重写为直线代码。
tree-ssa-alias.c:流敏感基于SSA的指向分析,得到可能-别名,一定-别名和逃逸分析信息。 这些信息将用于将变量从内存中地址可用对象提升为非别名变量,这样这些变量就能使用SSA形式的分析和优化了。
tree-ssa-structalias.c:用于过程间的指向分析。
tree-sra.c:将合适的无别名局部复合变量转换为一个标量集合,并进而转换为SSA形式。
tree-ssa-dse.c:删除那些无用的存储操作
tree-ssa-sink.c:将存储和赋值语句尽量下沉到和它们的使用点接近的位置。
tree-ssa-pre.c:部分冗余删除、load语句移动、完全冗余删除
tree-ssa-loop.c: SSA形式的循环优化
tree-ssa-loop-im.c:循环无关语句移动
tree-ssa-loop-ivcanon.c:循环标准化
tree-ssa-loop-ivopts.c:索引变量优化
tree-ssa-loop-unswitch.c:将循环无关的条件跳转移到循环外
tree-vectorizer.c, tree-vect-analyze.c, tree-vect-transform.c:自动向量化
tree-ssa-ccp.c:条件常数传播
tree-ssa-copy.c:条件复写传播
tree-vrp.c:取值范围传播
tree-outof-ssa.c:从SSA形式转换回普通形式
6 open64 中的SSA
open64中的SSA主要用于循环嵌套优化、过程间优化以及普通的函数内优化。 除了循环变换和内联优化外的所有机器无关优化都基于SSA做。 这部分可以说是Open64的重要卖点,对应的代码在osprey/be/opt下。



Open64在没有过程间优化时,主要以函数为单位进行,基于控制流图和别名分析得到的信息构建SSA。



opt_goto.cxx:goto语句转换,方便做SSA
opt_loop.cxx:循环正规化
opt_sym.cxx:构建相关符号表
opt_alias_class.cxx:别名分类,方便别名分析
opt_cfg.cxx:构建控制流图,包括支配树,不可到达代码识别,if语句转换
opt_tail.cxx:尾递归消除
opt_alias_analysis.cxx:流无关别名分析
opt_ssa.cxx:构建基于WHIRL的SSA
opt_dse.cxx:死store删除
opt_htable.cxx:构建HSSA–基于哈希的全局值编号SSA
opt_ivr.cxx:索引变量标准化
opt_prop.cxx:复写传播
opt_revise_ssa.cxx:将非直接变量展开成直接变量
opt_dce.cxx:死代码删除
opt_cfg_trans.cxx:控制流转换
opt_rename.cxx:SSA变量重命名、更新
opt_du.cxx:构建define-use信息
opt_etable.cxx:基于表达式的部分冗余删除
opt_estr.cxx:强度削弱
opt_ehoist.cxx:代码提升
opt_lftr2.cxx:线性代码测试、替换
opt_vn.cxx:基于值编号的完全冗余删除
opt_ltable.cxx:针对load的部分冗余删除
opt_stable.cxx:store Partial Redundancy Elimination 针对store的部分冗余删除
opt_bdce.cxx: Bitwise dead code elimination–针对结构体
opt_htable_emit.cxx: 从SSA转换回WHIRL中间表示



在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化:



常数传播(constant propagation)
值域传播(value range propagation)
稀疏有条件的常数传播(sparse conditional constant propagation)
消除无用的程式码(dead code elimination)
全域数值编号(global value numbering)
消除部分的冗余(partial redundancy elimination)
强度折减(strength reduction)
寄存器分配(register allocation)
从 SSA 的作用我们就能看出,因为它的主要作用就是代码的优化,所以是编译器后端(主要负责目标代码的优化和生成)的一部分;当然,除了 SSA 之外代码编译领域还有非常多的中间代码优化方法


Category golang