[OMR/TR] Testarossa的IL设计(持续更新)

边读代码边更新的笔记。一开始肯定会有不准确的地方,慢慢逐步修正。欢迎吐槽和补充~

先写点我原本就知道的内容,后面再更新具体到OMR TR代码的知识点。本文涉及的OMR TR代码基于最初开源的版本:Initial contribution of compiler technology consisting of: · eclipse/omr@03874a4 · GitHub

上一篇文章 [新闻] IBM/Eclipse OMR的编译器部分也已开源,以及IBM即将开源J9 VM - 编程语言与高级语言虚拟机杂谈(仮) - 知乎专栏 提到了OMR中的编译器组件——Testarossa(以下简称OMR TR)——也终于开源的新闻。这里就来看看OMR TR的中间表现形式(Intermediate Representation,IR)的设计是怎样的。

OMR中的相关文档:omr/IntroToTrees.md at master · eclipse/omr · GitHub

单一IR贯穿编译流程

OMR TR的IR叫做Testarossa IL(Intermediate Language),下面简称TR IL。

Testarossa的整个编译流程中基本上都是使用同样结构的树形(DAG形)TR IL。包括从平台无关代码向平台相关代码lower,也是从平台无关TR IL转换为平台相关TR IL。大部分平台相关优化也在TR IL上做。有一个全局寄存器分配器也是在TR IL上做的。

然后指令选择(instruction selection)会从TR IL生成出平台相关的Instruction对象,然后这层由Instruction对象构成的底层IR主要用于局部寄存器分配(local register allocation/assignment)、计算栈帧布局与最终的代码生成(codegen);在z上还会做一次scheduling,而其它平台上基本上除了窥孔优化(peephole optimization)之外就不在Instruction层面上做什么别的优化了。

引用Testarossa编译器的创始人Kevin Stoodley大大以前一个演讲里对TR编译流程的介绍:

这张图里的“Trees & CFG”就是本文所说的TR IL。


树形IR(Tree IR) / DAG IR

TR IL是一种颇为传统的树形IR(Tree IR),比一般编译器前端用的抽象语法树(AST)底层一些,而比完全拉直的线性IR要高层一些。

  • 相信熟悉虎书系列的同学会对这种IR比较熟悉(对应虎书第7、8两章内容)。
  • 熟悉GCC IR的同学,TR IL跟GCC的GENERIC已经lower了控制流但尚未lower表达式树的GIMPLE比较相似。
  • 熟悉LCC的同学,TR IL跟LCC中的DAG IR几乎是一样的东西。
  • 熟悉HotSpot Client Compiler(C1)的同学,TR IL与早期的C1的IR设计相当相似。早期C1的设计可以参考2000年的论文:A Compiler for the Java HotSpotTM Virtual Machine
  • 熟悉RyuJIT的同学,TR IL跟RyuJIT的Tree IR颇有相似之处。

TR IL比AST底层的地方在于:一般的AST设计会比较贴近源语言的语法结构,所以如果源语言有结构化控制流结构(if-then-else、while/for-loop等)的话,在AST里也会有直接对应的节点。而在一个典型的编译器后端用的树形IR中,控制流会被拆解为条件跳转与无条件跳转,树的形状会更贴近于底层控制流,而不再维持原本AST那种贴近源语言语法的层次结构。

TR IL比线性代码高层的地方在于:一般的线性代码跟机器语言比较贴近,就是一串线性执行的代码,没有所谓“语句”与“表达式”之分了——所有复杂(嵌套)表达式都被lower为一串使用临时变量的简单表达式。线性IR中的表达式大都可以用三地址代码直观地表示:

dest = src1 op src2

TR IL所使用的树形IR则仍然保持“语句”与“表达式”的区别。其中“语句”指的是可能有副作用的操作(例如变量赋值、函数调用等)或者是控制流跳转。语句不能嵌套,只能按顺序线性执行;换句话说,语句用于确定程序的执行顺序。而“表达式”则是纯粹的运算,没有副作用,没有控制流,可以嵌套。

常见的树形IR中“表达式”的若干重要特征是:

  • 由于没有副作用与控制流,嵌套表达式的求值顺序可以任意决定,这给优化留下了自由度;
  • 一个语句中的嵌套表达式的中间结果不会被别的语句中的表达式所看到。也就是说,一个语句中的表达式中间结果的生命周期仅限于该语句内,不会“泄漏”到别的语句中。这个特征对于某些非常简易的局部优化挺有帮助,例如这里提到的超简易寄存器分配算法。

不过就跟很多叫做“树形IR”但实际上却用的是有向无环图(DAG)的IR一样,TR IL的表达式树实际上也是DAG而不总是真的树——表达式的中间运算结果在一个语句内可以被多个子表达式共享。像LINQ的Expression Tree虽然叫做树但骨子里也是DAG

TR IL的DAG节点共享并不仅限制在一个“语句”中,而是在一个基本块或者扩展块中的语句都可以共享。这是DAG IR一种比较典型的做法。注意这与上面说的一般树形IR的表达式的“中间结果不会泄漏到别的语句”不一样。

TR IL的“语句”由TreeTop表示。TreeTop是一个三元组:(TreeTop* prev, TreeTop* next, Node* node),基本块里的一串TreeTop构成了指定程序执行顺序语义的双向链表。一个TreeTop节点下面挂的Node是这个语句里唯一一个可以有副作用的节点。这跟虎书第8章提到的canonical tree的设计非常相似。


每个节点只能生成一个值

TR IL的DAG节点,每个节点只能生成一个值。这对于大多数场景都够用。但有些特殊的平台支持,例如说一条div机器指令可能可以同时计算出商与余两个值,又比如说一条加法指令我们可能不但需要它的和,还想要它的进位(carry)值。要针对这些平台支持做优化,有些编译器会选择让IR支持某些节点生成多个值(或者说生成一个tuple然后用projection节点来提取tuple里的各个值)。

TR IL的设计则是坚守每个节点只生成一个值的设计。在遇到divmod、addwithcarry这样的需求时也还是用多个节点来分别表示那些原始操作,只是让优化器想办法识别出这些模式而把它们打包安排在相邻位置上。这个设计取舍是为了更容易保证构造IR时的正确性,而让优化器去解决性能问题。

参考文档:Extending Trees - Simplicity


双层IR——控制流图与数据操作

TR IL是一种典型的双层IR,有单独一层控制流图(CFG)结构,由基本块(basic block)与控制流边构成;每个基本块代表一个最长的可线性执行的代码。基本块里是数据操作,也就是上一节提到的由“语句”和“表达式”构成的树形IR。

OMR TR所使用的基本块是“扩展基本块”(extended basic block)——函数调用以及其它潜在会抛出异常(有异常控制流)的IL指令并不结束一个基本块。

不仅如此,OMR TR在优化过程中还会进一步尝试把多个基本块粘合在一起,构成所谓的“扩展块”(extended block)——由多个原始基本块构成的单入口多出口(single-entry, multiple-exit)结构。这种扩展块结构也叫做superblock

参考文档:Intro To Trees - Basic Block


传统的IR——没有显式使用SSA形式

TR IL相对于现代流行的编译器IR,有一个很有趣的“特点”,就是虽然它支持各种控制流/数据流分析和优化,但却没有使用直接嵌入在IR中的SSA形式

“取而代之”的是,TR IL里有TR_UseDefInfo这个辅助数据结构,用于记录IR中变量的use-def与def-use信息。本质上说它足以实现SSA形式所能实现的功能,只是没有把这个信息嵌入到IR里而已。

这个设计与Testarossa的血缘或许有很深的关系。正如前文所述,Testarossa最初的正式用途是作为J9 JVM配套的JIT编译器。而当时J9主要是针对嵌入式市场(Java ME)而开发的,配套的J9/TR自然必须严格考虑编译成本,不能做太重量级的分析与优化。后来J9逐步发展成熟,替代了IBM原本的桌面/服务器端JVM——Sovereign JVM——成为IBM JDK的唯一JVM,配套的J9/TR的功能也比以前大大丰富,单个编译器可进行不同优化级别的编译,以便支持J9的多层编译系统。

在最低优化级别的编译模式下,TR最主要的任务就是以最快速度从Java字节码生成出机器码,要构造SSA形式显然是太重量级了。而如果在高优化级别中为了深入做数据流分析而把IR转进一个单独的SSA形式版的TR IL,又会让IR的设计变得复杂:要么要支持两套IR,一套传统形式,一套SSA形式;要么要在同一套IR里支持传统模式与SSA模式。这两种用SSA形式的做法都有别的编译器使用,但TR选择了第三种也算常见的取舍:主IR里不直接携带use-def / def-use信息,而是在主IR之外用辅助数据结构来记录并跟踪这个信息。

TR IL里,TR_UseDefInfo是可选的辅助数据结构,只在需要做数据流分析/优化时才计算,可以很好地应对不同优化级别的不同需求。对应地,TR在做数据流分析时主要用的是传统的bitvector形式的分析。

说来,《High-Performance Compilers for Parallel Computing》一书的第6章,"Scalar Analysis with Factored Use-Def Chains",把带有Factored Use-Def (FUD) Chain的IR与SSA形式的等价性介绍了一遍。此书作者Michael Wolfe对“SSA形式”这个叫法似乎没啥特别大的好感 >_<


(未完待续)

文章被以下专栏收录
12 条评论
推荐阅读