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

标签: GCC open64 前端和程序分析 后端优化与处理器 编译技术 | 发表时间:2011-08-13 18:52 | 作者:erlv delphij
出处:http://www.lingcc.com

精确的数据流分析是让编译优化能高效进行的基础。 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。

1 SSA是什么?

SSA即静态单赋值,Static Single-Assignment,这是一种中间表示形式。 之所以称之为单赋值,是因为每个名字在SSA中仅被赋值一次.

如下图中的一段程序的控制流图。从这张图中可以看到,最后一个基本块中y值的定义或者来自左侧的分支,或者来自右侧的分支。

SSA_example1.1

SSA_example1.1

将每个赋值语句中的变量赋予一个唯一的名称后,一般新名称采用原变量+版本号(Version)的形式。 对于上面这段控制流图,就变成如下形式:

SSA_example1.2

SSA_example1.2

这张图中有个问题,有分支时,若分支中有对变量的操作,就无法确定使用了哪个版本的变量。 因此,引入了PHI节点。如下图所示:

SSA_example1.3

SSA_example1.3

PHI将分支中的y1和y2连接,并生成一个新的定义y3。有了PHI节点后,最后一个基本块中y3的定义来自之前的PHI节点,PHI节点中的两个操作数y1和y2分别来自左右两个分支。

2 SSA的作用

在SSA中间表示中,可以保证每个被使用的变量都有唯一的定义,即SSA能带来精确的使用–定义关系。 而在图SSA_example1.1中的y值定义却非常模糊。

概括起来,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节点对应的操作。最简单的做法,直接拷贝,如下图所示:

out of ssa

out of ssa

但这样有一个问题,如下图。简单的拷贝算法可能改变代码的语义:

out of ssa problem

out of ssa problem

正确的做法:

  • 对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中间表示

7 相关资料和文献

您可能也喜欢:
Open64 课程–全局标量优化(WOPT II)
Open64 课程–全局标量优化(WOPT I) part II
open64课程–过程间分析优化(IPA)
Open64 课程–全局标量优化(WOPT I) part 1
Open64课程–反馈指导优化(FDO)
无觅

相关文章:

相关 [赋值 ssa static] 推荐:

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

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

C++:浅谈修饰符static

- yu - C++博客-首页原创精华区
本文永久链接:http://www.limou.net/?p=149. static 是C++中很常用的修饰符,它被用来控制变量的存储方式和可见性,下面将从static 修饰符的产生原因、作用谈起,全面分析static 修饰符的实质. static   的两大作用: . static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间.

Static块什么时候运行

- - CSDN博客推荐文章
为了搞清楚这个我们首先要知道一个类想要运行JVM会做哪些事情.       采用双亲委派模式加载类,子类会交给父类的classloader去加载,如果父类加载不到自己才会尝试加载. 最终功能是将java字节码转换为JVM的class对象.       将Java二进制代码合并到JVM的运行时状态中.

SrtEdit 6.3 - 最好用的SRT/SSA/ASS/LRC字幕编辑器

- - 精品绿色便携软件
SrtEdit 2012 是一个非常强大、灵活、实用的文本字幕编辑器,支持编辑 SRT 字幕文件 (*.srt)、SSA 字幕文件 (*.ssa)、ASS 字幕文件 (*.ass)、LRC 歌词文件 (*.lrc),能够满足字幕编辑的各种需求,如自动分割字幕文件、自动合并字幕文件、调整字幕时间轴、制作中英双语同步字幕等等.

JAVA基础-栈与堆,static、final修饰符、内部类和Java内存分配

- - 编程语言 - ITeye博客
栈:后进先出(Last-in/First-Out).      Java的堆是一个运行时数据区,类的对象从中分配空间. 这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放. 堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据.

数的创生(二)赋值完备化

- yunfeng - 科学松鼠会
上篇请见《数的创生(一)方程的解》. 这一节说说从有理数产生新数的另一个途径:从有限到无限. 这个概念我们在小学就已经比较熟悉了,就是从有限小数或者循环小数到无限不循环小数的扩张. 然而,要说清楚这个概念,我们最好还是从更基本的概念开始,即,什么是整数,什么是小数. 人类发明数字之前,整数是通过物件来表示的.

shiro 一个项目多个系统sessionid赋值 (getsession 重载)

- - ITeye博客
Shiro Security是非常不错的Security框架. 最近在我的项目中进行相关整合,shiro不难,难就难在如何对已经成熟的系统进行整合. 作为相关切入点,我也考虑了很久,整体运用上了如张开涛大佬所说. 对于Subject我们一般这么使用:. 1、身份验证(login). 2、授权(hasRole*/isPermitted*或checkRole*/checkPermission*).