原文: LLVM - The Architecture of Open Source Application 作者: Chris Lattner
本章讨论了一些LLVM1设计中的决策,LLVM脱胎于一系列底层且联系紧密的工具(比如汇编器,编译器,调试器等),这些工具是为了兼容Unix下的一些经典工具。LLVM曾是一个缩写,现在是这个雨伞项目的代号。虽然它提供了一些独特的功能,并因为一些优秀的工具(例如,Clang 编译器2:一个C/C++/Objective-C的编译器,提供一系列基于GCC编译器的功能)而为人所知,但LLVM区别于其它编译器的地方主要在于内部的架构。
自2000年12月以来,LLVM旨在设计一个可复用,有良好接口的库[LA04]。而那时,开源语言实现常被设计成一个集成可执行程序,是一个特有功能的工具。例如,当时想要复用一个静态编译器(如GCC)来做静态分析和重构是十分困难的。脚本语言常常可以将其运行时和解释器嵌入更大的应用中,而这个运行时只是一个独立的代码块,可以被包含或排除,其零件却不能被复用,各语言实现之间也只能共享很少的代码。
除了编译器本身,一些流行语言实现的社区也经常严重地分化:某个实现通常要么提供传统的静态编译(如GCC, FreePascal, FreeBASIC),要么以解释器和JIT编译器的形式提供运行时编译器。很少有两种都支持的,即使支持,也只能共享很少的代码。 过去十年来,LLVM彻底改变了这个局面。如今LLVM作为主要的技术,实现了很多种语言的静态和运行时编译(一系列语系,如,GCC系的语言,Java, .NET, Python, Ruby, Scheme, Haskell, D, 和一些其它非主流语言)。它也取代了许多特殊用途的编译器,如苹果OpenGL Stack的运行时优化引擎,Adobe After Effects系列的图像处理库。LLVM也被用来开发了各种新产品,其中最著名的数OpenCL GPU编程语言和其运行时。
11.1. 经典编译器设计简介
三段式设计是传统的静态编译器(如大多C编译器)设计中最流行的,分为前端,优化器,后端(见图11.1)。前段解析源代码,检查错误,将输入代码变成特定语言的抽象语法书(AST)。AST可以被转化成新的模式,以便优化,优化器和后端基于这些代码运行。
优化器做出一系列的变换来优化代码的运行时,比如去除那些独立于语言和处理目标的冗余计算。后端(常称为代码生成器)把代码映射到目标指令集上,除了确保代码正确,它还负责一些特定架构之上的特殊优化。后端一般包括指令选择,寄存器的分配,指令计划。
对于解释器和JIT编译器,这个模型也同样使用。Java虚拟机也是这种模型的一个实现,它将Java的字节码用作前端和优化器之间的接口。
11.1.1. 设计的含义
当编译器需要支持多种源代码和目标架构时,这个经典设计的优点就体现出来了。如果编译器在它的优化器中用了共享的代码表示,那么对任何语言都可以写出能编译到这个优化器的前端,也可以对任何平台写出这个优化器的后端,如图11.2所示
有了这个设计,支持一个新语言需要实现一个新的前端,但已有的优化器和后端都可复用。如果这些部件没有分离,那么就要从头开始,因此实现M种语言的N个目标需要N*M个编译器。
三阶段设计的另一个好处是(仅次于多平台)相对于支持单一的语言和目标平台,编译器能为更广大的程序员服务。对开源项目来说,这意味着更多的潜在参与者,自然使编译器得到更多加强和改进。这也是为什么面向很多社区(如GCC)的开源编译器相对于小众编译器(如FreePASCAL),能生成更优化的机器码。这也不像商业编译器那般,性能与预算直接相关。比如,Intel ICC 编译器尽管小众,但以生成高质量代码闻名。
三阶段设计的最后一个优点是实现前端所需的技能和实现优化器及后端的技能非常不同。这样的分离能让前端人员更容易加强和维护编译器的前端部件。虽然这是社交问题而不是技术,但在实际操作中非常重要,尤其对于那些开源项目,他们竭力想减少贡献障碍。
11.2 现有的语言实现
虽然在编译教科书中,三阶段设计的好处很明显,在现实中却从未实现。回顾在LLVM出现以前开源语言实现的历史,您会发现Perl,Python,Ruby和Java之间没有共享的代码。然而,像Glasgow Haskell Compiler(GHC)和FreeBASIC这样的项目却可以移植到多种CPU平台上,但这些实现都非常依赖于他们支持的语言。为了实现各种JIT编译器,如图像处理,正则表达式,显卡驱动,和其他一些CPU密集型的任务,许多特定用途的编辑器技术也被开发出来。
有人说,关于这个模型有三个故事,第一个是关于Java和.NET虚拟机。这些系统拥有JIT编译器,运行时,和一个完善的字节码格式。这意味着任何可以编译成此字节码格式(有很多种这样的格式)的语言3都可以享用到其优化器,JIT和运行时的便利。相对的代价是这些实现在选择 运行时 时缺乏灵活性:他们都强制使用JIT编译,垃圾收集,并使用特别的对象模型。这导致它在编译那些不完全匹配这个模型的语言(如C,用LLJVM)时只能得到次优的结果。
第二个故事可能是最不幸的,却是复用编译技术最流行的方式:将代码转换成C或其他语言,再使用现有的C编译器编译。这样方便了优化器和代码生成器的复用,也较灵活,对运行时有较好的控制,同时也方便了前端的理解,实现和维护。不幸的是,这种做法妨碍了有效的异常处理,调试体验也很差,减慢了编译过程,对那些需要确定尾部调用(或其他C语言不支持的特性)的语言也是个问题。
此模型最成功的一个实现是GCC4。 GCC支持多种前端和后端,有广大的活跃社区和贡献者。作为一个C编译器,GCC历来被广泛移植,对少数其他语言也有支持(需要hack)。随着时间推移,GCC社区进化出了更简洁的设计。至于GCC4.0,它有了一个新的,与前端的分离更明显的优化器。它的Fortran和Ada前端也用了一个简洁的AST。
尽管成功,但这三个实现仍有很强的局限,因为它们是为单片应用而设计的。举个例子,GCC无法被嵌入其他应用,也不能被用作运行时或JIT编译器,也不能从中单独提取和复用它的组件。如果有人想用GCC的C++前端生成文档,索引代码,重构,静态分析,这行不通,他只能得到完整GCC程序产生的XML格式的信息,或通过写一些插件,把代码植入GCC进程中。
GCC不能作为库被复用,有几个原因:包括滥用全局变量,滥用弱强制的常量,糟糕的数据结构,散乱的代码,宏的使用导致其一次编译只能支持一种#前端目标对。最困难的是修复一些内部架构的问题,而这些是在设计的早期就存在的问题。具体来说,GCC存在分层问题和抽象泄露问题:后端需要扫描前端的AST来生成调试信息,前端生成了后端的数据结构,整个编译器依赖于命令行界面来设置全局数据结构。
11.3. LLVM的代码表示:LLVM IR
有了以上的历史背景和说明,让我们继续深入LLVM:LLVM最重要的设计是中间表示(IR),一种用在编译器中的代码格式。在编译器的优化器中,我们用LLVM IR来表示中间层的分析和变换。设计时主要考虑的目标包括,支持轻量级的运行时优化,跨函数跨过程的优化,程序整体分析和激进的?重新构造变换等等。然而,最重要的是要其本身就是一个有完整语义的一级语言。为了更具体的说明,这里有一个.ll文件的例子:
define i32 @add1(i32 %a, i32 %b) {
entry:
%tmp1 = add i32 %a, %b
ret i32 %tmp1
}
define i32 @add2(i32 %a, i32 %b) {
entry:
%tmp1 = icmp eq i32 %a, 0
br i1 %tmp1, label %done, label %recurse
recurse:
%tmp2 = sub i32 %a, 1
%tmp3 = add i32 %b, 1
%tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
ret i32 %tmp4
done:
ret i32 %b
}
这是LLVM IR相对应的C代码,提供了两种整数加法
unsigned add1(unsigned a, unsigned b) {
return a+b;
}
// 可能是两数相加最高效的方法
unsigned add2(unsigned a, unsigned b) {
if (a == 0) return b;
return add2(a-1, b+1);
}
从这个例子可以看到,LLVM IR是一个底层的,类似RISC的虚拟指令集。就像一个真正的RISC指令集一样,它支持简单指令的线性序列,如加法,减法,比较和分支。这些指令有三种地址格式,也就是说取一些输入,在不同的寄存器中生成结果5。LLVM IR支持标记,看上去像是汇编语言的一种奇怪变形。
和大多RISC指令集不同,LLVM使用强类型,构成一个简单类型系统(如i32是一个32位的寄存器,i32**是一个指向32 位寄存器的指针),并且硬件的某些细节被抽离走了。例如,调用约定(调用行为,返回指令和具体的参数)被封装起来。另一个与机器码显著不同的地方是LLVM IR没有固定的寄存器名字,而使用一系列带有%的临时名字。
实际上LLVM IR不仅是一门语言,更有三种同形的形式:以上的文本格式,驻留内存的数据结构(由优化器查看修改),还有一种高效的(驻留在硬盘中)二进制比特码。LLVM项目也提供从文本到二进制码的转换工具:llvm-as,来把文本的.ll文件汇编成包含比特码的.bc文件,llvm-dis则把.bc装换成.ll文件。
编译器的IR对编译器优化来说是完美的天堂,因此十分有趣:优化器不像前端和后端,它不会被某种语言或平台所限制。另一方面,他要同时为这两者很好地服务:既要设计得让前端容易生成代码,也要有强大的表达能力,让重要的优化能在平台上发挥出来。
11.3.1. 编写一个LLVM IR优化
为了理解优化器如何工作,来看一些例子。编译优化器种类繁多,因此要给出万全之策很困难。大多优化器遵从这三个部分:
- 找到待变换的模式
- 验证变换是安全正确的,安全的
- 做变换,更新代码
其中最琐碎的优化是算数相等的模式匹配,比如:任何整数X,X-X是0,X-0是X,(X*2)-X是X。首先看看它们在LLVM IR中如何表示。举例:
⋮ ⋮ ⋮
%example1 = sub i32 %a, %a
⋮ ⋮ ⋮
%example2 = sub i32 %b, 0
⋮ ⋮ ⋮
%tmp = mul i32 %c, 2
%example3 = sub i32 %tmp, %c
⋮ ⋮ ⋮
对于此类“猫眼”转换,LLVM提供了一种指令简化接口,可供更高级的变换使用。这些特殊的变换属于SimplifySubInst函数下,形如:
// X - 0 -> X
if (match(Op1, m_Zero()))
return Op0;
// X - X -> 0
if (Op0 == Op1)
return Constant::getNullValue(Op0->getType());
// (X*2) - X -> X
if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
return Op1;
…
return 0; // 没有匹配,无需变换返回null
Op0和Op1被绑定到整形减法的左右操作数(这些等式对IEEE浮点并不成立)。LLVM是用C++编写,相比于函数式编程如OCaml,C++的模式匹配能力较弱,但其模板系统也可以实现类似的功能。匹配函数和m_ 函数可以对LLVM的IR码进行声明式的模式匹配操作。例如,m_specific谓词只有在乘法左操作数和Op1相同时才会匹配。
这三个例子都匹配成功并且返回相应的匹配,没有匹配则返回null。调用SimplifyInstruction的是一个分发者,在指令码上做一个switch语句,把每个操作码的帮助函数分发出去。各种优化器都会调用它。一个简单的驱动如下:
for (BasicBlock::iterator I = BB->begin(), E = BB->end(); I != E; ++I)
if (Value *V = SimplifyInstruction(I))
I->replaceAllUsesWith(V);
这段代码遍历block中的每条指令,检查是否可以简化。如果可以,就用replaceAllUsesWith方法更新代码中任何可以简化的指令。
11.4. 三阶段设计的LLVM实现
在基于LLVM的编译器中,前端负责解析,验证和诊断输入代码中的错误,将解析过的代码翻译成LLVM IR(经常是通过构建AST,再把AST转成LLVM IR)。IR再被选择性地分析和优化,接着由代码生成器输出本地机器码,如图11.3所示。 这是三阶段设计的一种直观实现,但忽略了LLVM架构从LLVM IR继承的一些强大而灵活的特性。
11.4.1 LLVM IR作为一种完整的代码表示
LLVM IR是优化器的特殊而唯一的接口。也就是说只有知道LLVM IR的原理和一些常量就能写一个LLVM的前端。LLVM是以文本格式为基础的,因此构造一个以文本输出LLVM IR的前端是可能且合理的,然后用Unix的管道将其传至外部的的优化器序列和代码生成器。
听起来有点出人意料,但这是LLVM一个颇为创新的属性,也是能在众多应用中脱颖而出的主要原因之一。广为人知且获得巨大成功的GCC也不具备:它的GIMPLE中间层就不是完备的。例如,当GCC代码生成器输出DWARF debug信息时,它会回溯参考源码级的树结构。GIMPLE本身用一个元组表示代码中的操作,但(至少对GCC 4.5来说)仍然用指向源码树结构的引用表示操作数。
这意味着前端作者需要知道并输出GCC的树形结构和GIMPLE才能写出一个GCC的前端。GCC的后端也有类似问题,作者需要知道RTL后端的细节。最后,GCC无法给出代码的完整表示,或者用文本读写GIMPLE(或相关的组成代码的数据结构)。结果是GCC很难摆弄,前端相对较少。
11.4.2。 LLVM作为库的集合
除了LLVM IR,LLVM的另一重要特性是它由一系列库组成,而不是像GCC般的单一命令行编译器,也不是JVM或.NET那样的封闭虚拟机。LLVM更像一种设施,有一些列的编译器可以被用来解决特定的问题(比如构建一个C编译器,#或一个特殊管道中的优化器)。这是LLVM最强大也是常被忽略的设计。
来看一个优化器设计的例子:读入LLVM IR,稍加处理,输出执行更快的LLVM IR。在LLVM中(和大多编译器一样)优化器是一系列不同优化通路组成的一条管道,每个通路在输入端运行,完成自已的任务。一般的通路包括内联展开,重新组织表达式,循环优化等。不同的优化级别下,对应不同的通路:例如在-O0下Clang不运行任何通路,在-O3下运行67个通路(LLVM 2.8)。
每个LLVM通路是一个C++类,间接继承自Pass类。大多写在单个.cpp文件中,Pass的子类定义在匿名名字空间中(对定义文件保持完全私密)。为了让外部代码使用,类暴露一个生成通路的函数,一个例子6:
namespace {
class Hello : public FunctionPass {
public:
// Print out the names of functions in the LLVM IR being optimized.
virtual bool runOnFunction(Function &F) {
cerr << "Hello: " << F.getName() << "\n";
return false;
}
};
}
FunctionPass *createHelloPass() { return new Hello(); }
上面提到,LLVM优化器提供了一系列通路,每个通路写法类似。这些通路被编译成一个或多个.o文件,然后被先嵌入一系列库文件(Unix中的.a文件)。这些库提供了所有的分析变换能力,其中的通路尽可能的松耦合:独立运行或声明依赖,LLVM的PassManager能得到依赖信息并优化通路执行。
库和抽象能力虽好但不能解决实际问题。真正有用的是新工具的作者能从中得益,比如一个图像处理语言的JIT编译器。作者需要知道一些对图像处理语言敏感的特性,如编译时迟滞,一些必须经过优化的语言特性。
基于库的设计让作者可以控制通路执行的顺序,适合符合图像处理领域的通路:如果代码都在一个大函数中,展开函数就是浪费时间。如果指针不多,命名分析和内存优化就不值得关注。然而,LLVM不能解决所有的优化问题,通路子系统以模块组织,而且PassManager不知道通路的内部细节,所以作者可以自由地创造针对特定语言的通路。图11.4展示了一个假设的XYZ图像处理系统。
一旦选定了一套优化器(以及代码生成器)图像处理编译器就被嵌入可执行程序或动态库。因为引用到的LLVM优化通路只是在.o文件中定义的简单生成函数,优化器则在.a库中,因此,只有实际用到的通路才会被载入应用程序。在上面的例子中,有到PassA和PassB的应用,因此它们被链接进来。PassB用到了PassD来做一些分析任务,PassD也被链接进来。但PassC和其它通路没有用到,因此没有接入。 这就是LLVM基于库设计的强大之处。它包罗万象,但可以灵活应用,有所精通。而传统的编译器高度耦合,难以拆解分析。即使你不知道LLVM的全局也可以灵活运用其中的单个优化器。
这种基于库的设计也易于理解,功能强大,但其本身并没有实际的功能。客户端(比如Clang C编译器)的设计者可以让他们各司其职。LLVM这种注重分层,#架构,子功能的设计使其应用广泛。LLVM提供JIT能力,但并不是所有客户端都必须用到。
11.5 可以重定向的代码生成器
LLVM代码生成器负责将LLVM IR转换成特定的机器码。对于任意平台,要求其能生成最优的机器代码。理想情况下,代码生成器应针对每一平台编写不同的代码,但另一方面,它要解决各平台上相似的问题。例如,目标程序都需要给寄存器肤质,尽管每个平台的寄存器不同,但其中的算法应尽量一致。
与优化器类似,LLVM的代码生成器把问题分成独立的通路,包括指令选择,寄存器分配,调度,代码结构优化,汇编输出。提供默认执行的自带通路。目标程序的作者可以挑选或覆盖默认通路,也可以根据需求创建自定义通路。例如,x86的寄存器较少,其后端使用寄存减压策略来调度。PowerPC寄存器较多,因此使用迟滞优化调度。x86用自定义通路处理x87的浮点栈,ARM的后端使用自定义通路按需在函数里防止常量池。有了这种灵活性,目标程序作者不用重新造轮子就能写出好的代码。
11.5.1 LLVM目标描述文件
基于这种混合匹配策略,作者可以自由选择组件,不同平台间代码可以复用。由此带来的一个问题是:每个组建需要泛化来适应不同平台。例如,寄存器分配需要考虑不同平台下寄存器文件以及指令和操作数之间的限制。LLVM的解决办法是用一种声明式的领域特定语言,给每个平台一个描述,由tblgen来处理。简化的x86编译过程如图11.5所示。
.td文件支持不同的子系统,可以生成目标程序相应的组件。例如,x86后端定义了一个寄存器类,其中一个叫GR32的类包括了所有的32未寄存器(.td中所有平台相关定义用大写)
def GR32 : RegisterClass<[i32], 32,
[EAX, ECX, EDX, ESI, EDI, EBX, EBP, ESP,
R8D, R9D, R10D, R11D, R14D, R15D, R12D, R13D]> { … }
定义说明此类中的寄存器可以存32位的值(最好对齐32位),共16个(.td中另有定义),还有一些分配顺序和其他信息。特定指令可以参考此类定义,把它用作一个操作数。例如32位寄存器的补码操作如下定义:
let Constraints = "$src = $dst" in
def NOT32r : I<0xF7, MRM2r,
(outs GR32:$dst), (ins GR32:$src),
"not{l}\t$dst",
[(set GR32:$dst, (not GR32:$src))]>;
此定义寿命NOT32r是一条指令(使用tblgen中的I类),定义了编码信息(0xF7, MRM2r),定义了输出输入(上文的GR32定义了有效操作数),定义了指令的汇编语法(用{}处理AT&T和Interl风格),最后一行定义了指令的效果和需要匹配的模式。第一行的let限定了输出输入需要在同一个物理寄存器上。
此定义包含了指令的很多信息,通过tblgen,LLVM的代码可以从中提取许多信息。仅此一条定义就足够用来匹配输入的IR代码,处理指令选择。寄存器分配器可以依此执行,也可以编解码相应的机器码,解析打印出文本格式的指令。有了这些特性,目标程序可以生成针对x86的汇编器(替代GNU的gas),或从#目标描述反汇编,编码JIT的指令。
除了这些特性,信息源的一致性还有很多好处。比如汇编和反汇编的语法和二进制编码彼此一致。目标描述也很容易测试:指令编码可以单独做单元测试。
我们已经努力把尽可能多的信息以声明形式装进.td文件,但还是无法面面俱到。开发者还是需要写少许C++代码来实现一些日常任务和特殊的通路(比如处理x87浮点栈的X86FloatingPoint.cpp)。随着LLVM的扩展到新平台,我们需要增加.td的表达能力,我们也在不断努力。用LLVM写目标程序会变得越来越简单。
11.6 模块化设计的有趣特性
模块化不仅优美,还有几种有趣的特性。因为LLVM只提供功能模块,开发者可以自由选择如何使用。
11.6.1 决定每个阶段的运行情况
前面提到,LLVM IR可以高效的线性化成二进制格式(LLVM字节码)。既然LLVM IR是自洽且线性化过程无损,我们就可以执行部分编译,保存进度,以后继续编译。于是我们就有了链接时和安装时优化,也就是把代码生成从编译时剥离出来稍后执行。 链接时优化(LTO)解决了传统编译器视野问题,可以跨文件优化(如内联)而不仅限于一个.c头文件内。Clang中可以设置-flto和-O4,这样就会让LLVM生成bitcode到.ofile文件,替代传统的对象文件,这样就把代码生成推迟到了链接时。
操作系统不同细节也会有所不同,关键是链接器在.o文件中侦测到LLVM字节码,将其载入内存,执行链接,对其优化。优化器此时可以看到代码全局,因此可以执行内联,展开常数,去除无效代码等跨文件的任务。许多现代编译器都支持LTO,但大多(如GCC, Open64, Intel)的线性化过程很低效。但在LLVM中,LTO是和系统设计融为一体,更可以跨语言,因为IR本身就是跨语言的。
安装时优化在链接时之后生成代码,如图11.7所示。软件安装在设备时,你可以知道目标设备的参数,这非常有意思。x86家族内就有各种不同的芯片和特性。把代码生成中的指令选择,规划等延迟到安装时,你可以让程序运行在最适合的环境下。
11.6.2. 优化器的单元测试
编译器是复杂的,因此质量尤为重要,测试必不可少。比如,修复了一个导致优化器崩溃的bug后要进行回归测试,确保其不再发生。测试的传统做法是写一个在编译器上运行的.c文件,保证编译器不会崩溃。GCC的测试集就是用这种方法。
这种方法的问题在于,编译器有众多子系统,优化器也有许多通路,每一个部分都有可能改变在bug出现处的输入。如果问题出现在前端或优化早起,测试就很可能失效。
有了LLVM IR的文本格式和模块化的优化器,LLVM测试集可以在单个优化通路中载入IR代码,用集中的回归测试验证期望的行为。除了崩溃,此法还可以测试优化器的具体行为。这个例子测试了常量传导通路和加法指令。
; RUN: opt < %s -constprop -S | FileCheck %s
define i32 @test() {
%A = add i32 4, 5
ret i32 %A
; CHECK: @test()
; CHECK: ret i32 9
}
RUN这行制定了运行的指令:opt和FileCheck命令行工具。opt是LLVM通路管理器的一个简单包装,它把所有的标准通路(并动态链接其它通路)链接起来暴露给命令行。FileCheck验证标准输入匹配 CHECK文件夹。这里验证了4和5的和转换成9。
虽然测试例子简单,却很难用.c文件写:前端解析时经常会转换常量,所以要在测试中让转换延迟到优化通路非常困难。而这里我们可以直接读取文本格式的LLVM IR,输入到目标通路,dump到一个文本中,无论是回归测试还是特性测试,这种方法都非常直观精确。
11.6.3 用BugPoint自动简化测试
当编译器或其它LLVM库中发现一个bug,第一步是写一个重现bug的测试。一旦有了这个测试,我们就能缩小bug重现的范围并定位bug,比如bug出现的通路。但在你最终学成之前,这个过程无聊又费神,编译器生成错误代码而不崩溃时,更是尤其痛苦。
LLVM BugPoint tool7利用IR线性化和模块化设计实现了这个过程的自动化。例如,给定一个.ll或.bc的输入和导致崩溃的一系列优化通路列表,BugPoint把输入简化为一个小测试,并定位出错的通路,然后输入简化的测试和重现崩溃的opt指令。这里BugPoint利用了Delta debugging的技术过滤了输入和通路列表。BigPoint了解LLVM IR的结构,不会像一般的delta debugging一样输出无用的IR代码。
在更复杂的编译错误中,你可以制定输入,代码生成器的信息和传递给可执行程序的命令行,还有一个引用输出。BugPoint会先判断问题是出自优化器还是代码生成器,然后反复地把测试用例凤城两组:一组发送到完好的代码,一组发到有bug的代码。这样每次都从发送到有bug代码的测试用例中移出测试代码,测试用例变的越来越少。
11.7 回顾 展望
LLVM的模块机制起初并不是为了达到以上描述的目标,而是一种自我保护机制:由于不可能一次做到完美,那么这些模块通道的实现可以方便地替换8。
LLVM另一个敏捷的地方在于(对库用户来说有些争议)可以推翻以前的决定,对API做大面积的修改,而不用担心向后兼容的问题。比如对LLVM IR的大幅修改,需要对所有优化通道和C++ API做大幅的修改。我们做个几次这样的修改,虽然对用户来说很难,但为了保持项目的快速进展必须这样做。为了外部用户(其他语言的绑定支持)的方便,我们提供了许多主要API的C封装(非常稳定),因此LLVM的新版本也支持老的.ll和.bc文件。
在未来,我们会让LLVM变得更模块化,更易于拓展。例如,代码生成器仍然过于集成:现在还不能分功能拓展LLVM。例如,如果你只想使用JIT,而不需要内联汇编,异常处理和调试信息生成,应该可以不用连接这些功能,独立构建一个代码生成器。我们也在持续改进由优化器和代码生成器生成的代码的质量,给IR增加新语言和新编译目标的支持,给高层次的语言优化提供更好地支持。
LLVM项目在各方面持续的成长。我们很高兴地看到,LLVM以各种方式在其他项目中得到应用,这是连设计者都没想到的进展。新的LLDB调试器就是个很好的例子:LLDB使用了Clang的C/C++/Objective-C parser来解析表达式,用了LLVM的JIT将其翻译成目标代码,用到了LLVM的反汇编器,用了LLVM来处理调用约定。有了这些工具,开发者可以专注于调试器的调试逻辑,而不是重新发明一个基本正确的C++ parser。
尽管LLVM是成功地,它仍然有许多改进的空间,而且可能慢慢变得笨重腐化。这些问题没有完美的答案,但只要新问题能不断地暴露出来,以前的决定能被重新审视,代码仍然能被重构,问题就有希望解决。毕竟,我们的目标不是一次做到完美,而是不停地进步。
脚注:
-
http://llvm.org ↩
-
http://clang.llvm.org ↩
-
http://en.wikipedia.org/wiki/List_of_JVM_languages ↩
-
一个backronym(一个已存在缩写的扩写,和原意不同)意思是”GNU Compiler Collection”. ↩
-
相应地 有像x86那样的双地址指令集,分别更新输入寄存器,也有单地址机,取一个操作数并在累加器上操作或堆栈机的栈顶操作。 ↩
-
通道的细节请参照 http://llvm.org/docs/WritingAnLLVMPass.html. ↩
-
http://llvm.org/docs/Bugpoint.html ↩
-
正如我经常说的,LLVM中的子系统至少要重写一次才能称得上好。 ↩