0%

即时编译器的中间表达形式

中间表达形式(IR)

通常将编译器分为前端和后端。其中,前端会对所输入的程序进行词法分析、语法分析、语义分析,然后生成中间表达形式,也就是IR(Intermediate Representation)。后端会对IR进行优化,然后生成目标代码。

如果不考虑解释执行的话,从Java源代码到最终的机器码实际上经过了两轮编译:Java编译器将Java源代码编译成Java字节码,而即时编译器则将Java字节码编译成机器码。

对于即时编译器来说,所输入的Java字节码剥离了很多高级的Java语法,而且其采用的基于栈的计算模型非常容易建模。因此,即时编译器并不需要重新进行词法分析、语法分析以及语义分析,而是直接将Java字节码作为一种IR。

静态单赋值(SSA) IR

不过,Java字节码本身并不适合直接作为可供优化的IR。这是因为现代编译器一般采用静态单赋值(Static Single assignment,SSA)IR。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。

在源代码中,对变量的赋值可能是冗余的,传统编译器需要借助数据流分析(reaching definition),从后至前依次确认哪些变量的值被覆盖kill掉。不过如果借助了SSA IR,编译器则可以通过查找赋值了但是没有使用的变量,来识别冗余赋值。

除此之外,SSA IR对其他优化方式也有很大的帮助,例如常量折叠(constant folding)、常量传播(constant propagation)、强度削减(strength reduction)、死代码删除(dead code elimination)等。

SSR IR会带来一个问题,那便是不同执行路径可能会对同一变量设置不同的值,根据不同的执行路径,所读取到的值也很有可能不同。

为了解决这个问题,需要引入一个Phi函数的概念,能够根据不同的执行路径选择不同的值。

即时编译器会将Java字节码转换成SSA IR。更确切的说,是一张包含控制流和数据流的IR图,每个字节码对应其中的若干个结点(有些字节码并没有对应的IR节点)。然后,即时编译器在IR图上进行优化。

可以将每一种优化看成一个独立的图算法,它接收一个IR图,并输出经过转换后的IR图,整个编译器优化过程便是一个个优化串联起来的。

Sea-of-Nodes

HotSpot里的C2采用的是一种名为Sea-of-Nodes的SSA IR。它的最大特点,便是去除了变量的改变,直接采用变量所指向的值,来进行运算。Phi函数本身也是一个值,被其他IR节点所依赖。常量传播在Sea-of—Nodes中变成了一个no-op。

Graal的IR同样也是Sea-of-Nodes类型的,并且可以认为是C2 IR的精简版本。

IR图中,包含控制流、数据流等,被控制流所连接的是固定节点,其他的皆属于浮动节点。若干顺序执行的结点将被包含在同一个基本块之中。(图中没有变量名,取而代之的是一个个的值。)

基本块是仅有一个入口和一个出口的指令序列(IR节点序列)。一个基本块的出口可以和若干个基本块的入口相连,反之亦然。

节点调度

浮动节点的位置并不固定。在编译过程中,编译器需要多次计算浮动节点具体的排布位置。这个过程称之为节点调度(node scheduling)。节点调度是根据节点之间的依赖关系来进行的。

C2没有固定节点这一概念,所有的IR节点都是浮动节点。它将根据各个基本块头尾之间的控制依赖,以及数据依赖内存依赖,来进行节点调度。

内存依赖:假设一段程序往内存中存储了一个值,而后又读取同一内存,那么显然程序希望读取到的是所存储的值。即时编译器不能任意调度对同一内存地址的读写,因为他们之间存在依赖关系。

C2的做法便是将这种时序上的先后记录为内存依赖,并让节点调度算法在进行调度时考虑这些内存依赖关系。Graal则将内存读写转换成固定节点。由于固定节点存在先后关系,因此无需额外记录内存依赖。

Global Value Numbering

GVN是一种发现并消除等价计算的优化技术。例如,如果一段程序中出现了多次操作数相同的乘法,那么即时编译器可以将这些乘法并为一个,从而降低输出机器码的大小。如果这些乘法出现在同一执行路径上,那么GVN还将省下冗余的乘法操作。

在Sea-of-Nodes中,由于只存在值的概念,因此GVN算法将非常简单:如果一个浮动节点本身不存在内存副作用(由于GVN可能影响节点调度,如果有内存副作用的话,那么将引发一些源代码中不可能出现的情况),那么即时编译器只需要判断该浮动节点是否与已存在的浮动节点的类型相同,所输入的IR节点是否一致,便可以将这两个浮动节点归并成一个。

可以将GVN理解为在IR图上的公共子表达式消除(Common Subexpression Elimination, CSE)。这两者的区别在于,GVN直接比较直的相同与否,而SCE则是借助词法分析器来判断两个表达式相同与否。因此,在不少情况下,CSE还需要借助常量传播来达到消除的效果。

总结

即时编译器将所输入的Java字节码转换成SSA IR,以便更好的进行优化。

具体来说C2和Graal采用的是一种名为Sea-of-Nodes的IR,其特点用IR节点来代表程序中的值,并且将原程序中基于变量的计算转换为基于值的计算。

基于IR的优化GVN。