LLVM (5) 11.4 三阶段设计在LLVM中的实现

来源:互联网 发布:ios9.0.2软件源 编辑:程序博客网 时间:2024/05/01 22:02
在一个基于LLVM的编译器中,前端负责输入代码的解析、验证和错误诊断,然后将解析后的代码翻译为LLVM IR (通常但不总是,先建立AST,之后通过AST转化为LLVM IR)。IR可选地与一系列的分析和提高代码质量的优化联系起来,然后被送入到代码生成器去生成本地机器码,如图11.3所示。这是非常直接的三阶段设计实现,但是这种简单的描述掩盖了采用LLVM IR的LLVM体系结构的能力和适用性。



11.4.1. LLVM IR是一种完全的代码表示

特别地,LLVM IR是一种具有良好的约定并且只是优化器的接口。这种特性意味着为了写一个LLVM的前端,我们只需要了解LLVM IR是什么、如何工作以及期望的常量。由于LLVM IR具有高级的文本形式,因此建立一个前端去输出LLVM IR文本是可能的和合理的。之后,使用Unix管道将其送给选择的优化器序列和代码生成器。

尽管有些令人惊奇,但是这是一个LLVM的新特性,以及一个主要原因为什么LLVM能够在不同应用中取得广泛的成功。即便已经广泛成功的、具有良好结构的GCC编译器也没有这样的特性: 中级表示GIMPLE不能够自我包含。作为简单例子,当GCC代码生成器抛出DWARF调试信息时,GCC返回并遍历源码级“树”结构。GIMPLE使用“元组(tuple)”表达代码中的操作,但是至少GCC4.5仍然用源码级树结构的索引表达操作。

为了实现GCC前端,开发者需要知道这些实现并且生成类似于GIMPLE的GCC树数据结构。GCC的后端也存在相似的问题,所有开发者需要知道RTL后端如何工作的零碎细节。最终,GCC不能将所有事情表示在代码中,或者不能以文本形式读写GIMPLE(以及形成代码表示的相关数据结构)。结果是很难用GCC做实验,并且因此GCC的前端相对很少。

11.4.2 LLVM是一个库的集合

设计LLVM IR之后,LLVM下一个最重要的事情是它被设计作为一个库集合,而不是一个像GCC或者不透明的、如同JVM或.NET的虚拟机独立的命令行编辑器。LLVM是一个平台,一个有用的、允许特殊问题(如建立C编译器或在一个特别流水线优化器)的编译器技术集合。尽管这是最重要的强大特性的一个,也是一个最少被理解的设计观点。

让我们将优化器设计作为一个例子:读取LLVM IR,一点一点分析,然后生成执行更快的LLVM IR。在LLVM(如同在其它编译器一样)优化器被组织作为明确优化的流水线。Pass的一般例子是inliner、expression reassociation、loop invariant code motion等。依据优化级别,不同的pass被执行: 例如(-O0)(无优化)Clang编译器不执行任何的pass,(-O3)在编译器(LLVM2.8)中执行67个pass。

每个LLVM的pass以C++类的形式编写,继承自Pass类。大多数的pass以一个简单的.cpp文件,并且它们的子类定义在anonymous名字空间(对于定义文件完全私有)。为了使得pass有用,文件外的代码不得不能够得到它,所以一个简单的函数(创造pass)从文件外导出。以下是一个简单的、具体的例子:

namespace {  class Hello : public FunctionPass {  public:    // Print out the names of funcitons in the LLVM IR being optimized.    virtual bool runOnFunction(Function &F) {      cerr << "Hello: " << F.getName() << "\n";      return false;    }  };}FunctionPass *createHelloPass() { return new Hello(); }



如上所述,LLVM优化器提供很多不同的pass,每一个以相似的风格写成。这些pass被编译成一个或多个.o文件,这些.o文件建立一系列的库(unix中的.a文件)。这些库提供所有的分析和转化能力,并且这些pass尽可能松散耦合:它们被期望能够独立运行,或者如果依赖于其它分析,则显示宣称它们的依赖。当给定一系列的pass去运行,LLVM PassManager使用显示的依赖信息区满足这些依赖并且优化pass的执行。

库和抽象的能力是巨大的,但是实际上它们不解决问题。当有人想建立一个能利用编译器技术的新工具,这才变得有趣。特别是为图像处理语言神仙道的JIT编译器。这种JIT编译器的实现在理论中具有一系列的限制:例如,可能图像处理语言对编译时间延迟高度敏感并且具有惯用的语言特性,这对于性能优化很重要。

LLVM采用的基于库的设计允许我们的实现选择pass执行顺序以及哪些负责图像处理领域:如果每一个都被定义为一个单独的大函数,那么没有必要浪费时间再inlining。如果只有很少的指针,假名分析和内存优化不值得考虑。然而,尽管我们做了最好的努力,LLVM不能解决所有的优化问题。由于pass子系统被模块化和PassManager本身不知道任何pass的内部信息,实现者自由地实现与语言相关的pass去去除LLVM编译器中的缺陷或者显示利用语言相关的优化机会。图11.4展示了一个简单的理论的XYZ图像处理系统的例子




一旦优化的集合被选定(相似地选择被用来代码生成)图像处理编译器被建立成为一个可执行文件或者动态库。由于LLVM编译器pass的唯一索引是一个create简单函数,其被定义在每个.o文件,并且由于优化器存在于.a库中,所以只有优化pass被用来连接终端应用,而不是整个LLVM优化器。在我们上面的例子中,由于存在一个索引到PassA和PassB,它们将被链接。由于PassB使用PassD去做分析,PassD不得不被链接。但是,由于PassC没有被使用,它的代码不会被链接到图像处理应用中。

这正是基于库的设计力量的来源。这种直接设计方式允许LLVM提供大量的能力,一些面向特殊应用的能力,而不会给只需要使用简单库的用户带来任何不便。相反,传统编译器的优化是以一种紧致的代码建立,因此很难被分割、推理以及加速。通过LLVM能够理解一个单独的优化器无需知道整个系统如何搭建在一起的。

基于库设计也是为什么如此多的人误解LLVM的一个原因:LLVM库有许多能力,但是实际上它们自己不做任何事情。取决于库用户的设计者,他们决定如何最好地使用这些部分。这种仔细的分层、代管和集中于子集的能力也是为什么LLVM优化器能被使用在不同应用中。同时,仅仅因为LLVM提供JIT编译能力,不意味着每一个用户都使用它。

0 0