有一本小册子Static Single Assignment Book写的很好,内容又较少,试着翻译一下,意译较多(说人话),不是严肃的翻译,感兴趣的能够看看。频率多是周更。。anyway,stay tuned~web
在平常编程中,名字是一个颇有用的东西。这本书想传递的关键内容是对于每一个不一样的东西给它一个独一无二的名字能够消除不少不肯定性以及不精确性。算法
举个例子,若是你无心中听到一段对话中有'Homer'这个词,没有上下文的状况下你不知道他说的是Homer Simpson(辛普森)仍是古希腊诗人荷马仍是你认识的某个叫Homer的人。可是只要你听到对话说起Springfield (辛普森一家)而不是Smyrna(希腊诗歌),你就能知道他们说的是辛普森一家这个电视剧。不过话又说回来,若是每一个人都有一个独一无二的名字,那么就不可能混淆电视剧角色和古希腊文学人物,这个问题都不会成立。编程
这本书主要讨论静态单赋值(Static Single Assignment Form,SSA)形式,它是一种变量的命名约定。术语static说明SSA与属性和代码分析相关,术语single说明SSA强制变量名具备惟一性。术语assignment表示变量的定义。举个例子,在下面的代码中:后端
x = y + 1;
变量x被赋予表达式(y+1)的值。这是一个定义,或者对于x来讲是赋值语句。编译器工程师会说上面的赋值语句将值(y+1)储存到左值x中。数据结构
关于SSA最简单,限制最少的定义以下:并发
"若是每一个变量在程序中有且只有一个赋值语句,那么该程序是SSA形式"框架
可是实际上SSA还有不少变体,有更多的限制。这些变体可能使变量定义和使用与图论的一些特性有关,或者封装一些特定的控制流/数据流信息。每一个SSA变体都有特设的性质。基本的SSA变体将会在第二章讨论,本书的第三部分还会讨论更多这部分的内容。编程语言
全部SSA变体,包括上面最简单的定义都有一个最基本属性就是引用透明性(referential transparency),所谓引用透明性是指程序中的每一个变量只有一个定义,变量的值和它所在程序的位置无关。咱们可能根据分支的条件完善对于某个变量的认识。举个例子,不用看代码咱们就知道下面if语句后紧跟着的then/else条件块中x的值函数式编程
if (x == 0)
由于x的值在这个if语句中是没有改变的。函数式编程语言写的程序是引用透明的,引用透明性对于形式化方法和数学推理颇有用,由于表达式的值只依赖它的子表达式而不依赖求值的顺序或者表达式的反作用,或者其它表达式。对于一个引用透明的程序,考虑下面的代码片断:函数
x = 1; y = x + 1; x = 2; z = x + 1;
一个naive(并且不正确)的分析器可能认为y和z的值相等,由于他们的定义是同样的,都是(x+1),然而x的值取决于当前代码位置是在第二个赋值的前面仍是后面,即变量的值取决于上下文。当编译器将这个代码段转换为SSA形式时,它会具备引用透明性。转换的过程会为一个变量的屡次定义使用不一样的名字(译注:x1和x2)。使用SSA形式后,只有当x1和x2相等时y和z才相等。
x1 = 1; y = x1 + 1; x2 = 2; z = x2 + 1;
在前一节中,咱们看到了如何经过简单的重命名将代码转换为SSA形式。赋值语句左边被定义的变量叫作target,在SSA中,每一个target都有惟一的名字。反过来,赋值语句右边能够屡次使用target,在这里它们叫作source。贯穿本书,SSA的target名字定义都是变量名再加一个下标这种形式。通常来讲这是不重要的实现细节,虽然它对于编译器debug来讲颇有用。
φ函数是SSA最重要的一个概念,它很特别,又叫作伪赋值函数(pseudo-assignment function)。有些人也叫它notational fiction。ɸ函数的用途是合并来自不一样路径的值,通常出如今控制流的合并点。
考虑下面的代码示例和它对应的控制流图(Control Flow Graph,CFG)表示:
在if不一样分支中,y有不一样的定义。y的不一样定义最终在print那个地方交汇。当编译器将该代码转换为SSA形式时,y的不一样定义被命名为y1和y2。print既可使用y1也可使用y2,这取决于if的条件。在这种状况下,须要用φ函数引入新的变量y3,它的参数是y1和y2。所以SSA版本的上述程序以下:
就放置位置来讲,φ函数通常是放到控制流交汇点,即CFG中有多个前驱基本块的那个基本块头部。若是有n条路径能够进入基本块b,那么在基本块b头部的φ函数有n个参数。φ函数会动态的选择正确的参数。φ函数根据n个参数,建立新的变量名,这个名字是惟一的,由于它要保证SSA的基本性质。所以,在上面的例子中,若是控制流从基本块A流向下面的基本块,那么y3使用φ函数选择y1做为它的值,反之φ函数使用y2做为它的值。注意CFG图φ函数的参数y1和y2前面还加了基本块的标签,这种形式是比较多余的,在本书的后面部分,这个基本块标签能不加就不加,除非没了它会引发歧义。
这里还要强调一下,若是基本块头部有多个φ函数,这些φ函数是并行的,即,它们是同时执行,不须要顺序执行。这一点是很重要的,由于在通过一些优化,好比复写传播(copy propagation)后φ函数的target多是其它φ函数的source。在SSA解构阶段,φ函数会被消除(译注:就是编译器不须要SSA形式,想将它转换为其它IR,这就叫SSA解构),在解构阶段使用常规的复制操做序列化,这点会在17.6小结描述。这个小细节对于寄存器分配后的代码来讲是至关重要的。
严格来讲,φ函数不能被软件直接执行,由于进入φ函数的控制流没有被显式的编码进φ函数的参数。这是能够接受的,由于φ函数一般只用于程序的静态分析。然而,有不少扩展使得φ函数能够执行,如 φif 或者γ函数(参见第12章),它有一个额外的参数,告诉φ函数选择那个值。关于这个会在第12章,第16章和第18章讨论。
接下来咱们再展现一个例子,它说明了一个循环控制流解构的SSA形式。下面是非SSA形式的程序和SSA形式的控制流图:
SSA代码在循环头部新增了两个φ函数。它们合并循环前的值定义和循环中的值的定义。
要注意不要混淆SSA和自动并行化优化中的(动态)静态赋值这两个概念。SSA不会阻止在程序执行的时候对一个变量的屡次定义,好比,上面的SSA代码中,变量y3和x3在循环体内,每次循环都会重定义它们。
SSA构造的详细描述会在第3章给出,如今只须要明白下面的内容:
在将来的第11章咱们会提到,SSA主要的一个优势是它对数据流分析(data-flow analysis)很友好。数据流分析在程序编译的时候收集信息,为将来的代码优化作准备。在程序运行时,这些信息会在变量间流动。静态分析经过在控制流图中传播这些信息,得以捕获关于数据流的一些事实(fact)。这种方式在传统的数据流分析中很常见。
一般,若是程序是一种功能性的(functional)或者稀疏(sparse)的表示,如SSA形式,那么数据流信息能程序中更高效的传播。当程序被转换为SSA形式时,变量在定义点被重命名。对于一个确凿的数据流问题,好比常量传播,它表现为一个程序点的集合,在这些程序点数据流事实可能改变。所以能够直接关联数据流事实和变量名字,而不是在每一个程序点为全部变量维护各自的数据流事实的集合,下图展现了一个非零值分析(non-zero value analysis)
对于程序中的每一个变量,分析的目标是静态肯定哪些变量在运行时包含0值(即null)。在这里0就表示变量为null,0打一把叉表示不为null,T表示可能为null。上图(a)表示传统的数据流分析,咱们会在六个基本块的入口点和出处都计算一次变量x和y信息。而在上图(b)的基于SSA的数据流分析中,咱们只须要在变量定义处计算一下,而后就能得到六个数据流事实。
对于其它的数据流问题,属性也可能在变量定义以外发生改变,这些问题只要插入一些φ函数就能放入稀疏数据流分析的框架中,第11章会有一个例子讨论这个。总的来讲,目前这个例子说明了SSA能给分析算法带来的关键好处是:
这本书的第二部分给出了一个完整的基于SSA数据流分析的描述。
历史背景。在整个20世纪80年代,优化编译器技术愈来愈成熟,各类中间表示被提出,它们包含了数据依赖,使得数据流分析在这些中间表示上很容易进行。在这些中间表示背后的设计理念是显式/隐式包含变量定义和使用的关系,即def-use链条,使得数据流信息能有效的传播。程序依赖图(program dependence graph)和程序依赖网(program dependence web)均属此类IR。第12章还会讨论这些风格的IR的更多细节。
静态单赋值是由IBM Research开发的一种IR,并在20世纪80年代末的几篇研究论文中公开发表。SSA因为其符合直觉的性质和直观的构造算法获得了普遍的应用。SSA给出了一个标准化的变量def-use链,简化了不少数据流分析技术。
当前情况。当前主流的商业编译器和开源编译器,包括GCC,LLVM,HotSpot Java虚拟机,V8 JavaScript引擎都将SSA做为程序分析中的关键表示。因为在SSA执行优化速度快并且高效,那些即时编译器(JIT)会在一些高级地、与平台无关的表示(如Java字节码,CLI字节码,LLVM bitcode)上普遍使用SSA。
SSA最初是为了简化高级程序表示的变形而开发而建立的,由于其良好的特性,可以简化算法和减小计算复杂性。今天,SSA形式甚至被用于最后的代码生成阶段(见第四部分),即后端。好几个工业编译器和学术编译器,既有静态,也有just-in-time,都在它们的后端使用SSA,如LLVM,HotSpot,LAO,libFirm,Mono。不少使用SSA的编译器在编译快要结束时,即寄存器分配前才解构SSA。最近的研究甚至能在寄存器分配期间也使用SSA,SSA形式会保持到很是很是后面的机器代码生成过程才会被解构。
SSA与高级语言。到目前为止,咱们展现了在低级代码上使用SSA形式作分析的优点。有趣的是,在高级代码上若是强制遵循某些准则也可能具备SSA的性质。根据SISAL语言的定义,程序自动具有引用透明性,由于变量不容许屡次赋值。其它语言也能有SSA的性质,好比Java的变量加个final或者C#的变量加个const/readonly。
强制写出具备SSA性质的高级语言程序主要好处是这些程序能具有不变形,这简化了并发编程。猪肚的数据能在多个线程中自由的共享,没有任何数据依赖问题。数据依赖对于多核处理器来讲是一个大问题。
在函数式编程语言中,引用透明是语言的基本特性。所以函数式编程隐式具备SSA性质。第6章会介绍SSA和函数式编程。
本章引入了SSA的符号表示,本书的剩下部分就SSA的各个方面详细讨论。本书的终极目标是:
本节还剩下一些内容,它们与下一章的一些主题相关。
SSA对于变量命名有严格要求,每一个变量的名字都是独一无二的。赋值语言和控制流交汇点会引入新的变量名。这些简化了表达变量def-use关系的数据结构实现和变量存活范围。本书第二部分关注基于SSA的数据流恩熙,使用SSA主要有三个好处:
编译时受益。若是程序是SSA形式,不少编译器优化能够高效的进行,由于引用透明性意味着数据流信息直接与变量关联,而不是每一个程序点的变量。关于这一点咱们已经在1.3的非零值分析中演示过了。
编译器开发受益。SSA使得程序分析和转换能更容易表达。这意味着编译器工程师能更高产,能够写更多的pass,而且能debug更多的pass(译注:smile)。举个例子,基于SSA的GCC4.x的死代码优化比非GCC3.x的非SSA死代码优化实现总代码少了40%。
程序运行时受益。理论上,能基于SSA实现的分析和优化也能基于其它非SSA形式。前一点提到过,基于SSA的实现代码更少,所以不少基于SSA的编译器优化也能更高效进行,关于这一点的示例是一类控制流不敏感分析(control-flow insensitive analysis),具体参见论文Using static single assignment form to improve flowinsensitive pointer analysis
一些人认为SSA很复杂很繁琐,不能高效表达程序。这本书的目的就是让读者免去这些担心。下面的表单展现了关于SSA常见的谬论,以及破除谬论的章节。
谬论 | 破除谬论 |
---|---|
SSA让变量数爆炸 | 第二章会回顾SSA的主要变体,一些变体引入的变量数比原始SSA形式少不少 |
SSA的性质难以维持 | 第三章和第五章讨论了一些修复SSA性质的简单技术(由于一些优化可能重写中间表示,致使SSA性质被破坏) |
SSA的性质难以维持 | 第三章和第十七章展现了高效且效果显著的SSA解构算法的复制操做 |