LLVM探究
iOSer都知道LLVM(Low Level Virtual Machine)是Xcode自带的编译器,而在LLVM诞生之前,Apple一直依赖另一个开源编译器GCC。随着Apple收购乔布斯的创业公司NextStep,面向对象语言Objective-C正式成为Apple官方的开发语言。由于当时GCC社区开发者未能及时支持OC的新语言特性,加上GCC开发者与Apple在编译器支持模块化调用方面存在分歧,最终让Apple选择分道扬镳,并转头拥抱了另一个开源项目LLVM。 这场"分手"的深层原因其实更加复杂:GCC的GPL许可证要求任何基于GCC的修改都必须开源,这对于Apple这样重视知识产权的公司来说是难以接受的。而且GCC的代码库经过20多年的发展,已经变成了一个高度耦合、难以扩展的庞然大物。Apple需要的是一个能够快速迭代、支持新语言特性、并且可以深度集成到IDE中的现代化编译器。 LLVM起源于2000年,是由美国UIUC大学的Chris Lattner博士发起,Chris Lattner也是后来的Swift之父。最初LLVM是Chris Lattner的硕士论文项目,旨在构建一个"终身代码优化系统"(Lifelong Program Analysis & Transformation),可以在编译时、链接时、运行时甚至闲置时对代码进行持续优化。这个大胆的想法吸引了Apple的注意。 在Apple的资助下,LLVM得到了飞速的发展。2005年,Apple雇用了Chris Lattner,并组建了专门的LLVM开发团队。同时始于2007开发的Clang,因编译速度快、占内存少、错误诊断信息友好、代码质量高,最终替代笨重的GCC成为LLVM的新前端。Clang的编译速度比GCC快3倍,内存占用减少5倍,这对于大型项目的编译体验提升是巨大的。更重要的是,Clang从设计之初就考虑了IDE集成,提供了丰富的API供开发工具调用,这为Xcode的代码补全、实时错误检测、重构等功能提供了基础。 LLVM和Clang不断完善功能的同时,也在Apple的MacOS和Xcode IDE中得到工业级的应用和推广。2013年,Apple正式宣布废弃GCC,LLVM/Clang成为Xcode的默认编译器。在与Apple的相互成就中,LLVM一跃成为了最领先的开源编译器之一,目前已被Google(用于Android NDK)、Intel、AMD、NVIDIA等科技巨头广泛采用。 LLVM设计理念 与GCC不同,LLVM设计之初就注重模块化和可扩展性,这是一种设计哲学的根本性差异。比如LLVM的优化器,它支持开发者选择Pass的类型和执行顺序,提供基于模块或库的可组装能力。每个Pass都是一个独立的转换单元,可以单独测试、单独优化、单独替换。相对之下,GCC的优化器则是由大量高度耦合的代码组成,很难进行拆分和选择性使用。GCC的优化passes之间存在隐式依赖,修改一个pass可能影响其他多个pass的行为。 模块化的设计理念还体现在LLVM的三段式架构设计中:LLVM通过Libraries collection完美实现了传统编译器想要的编译前端、编译优化器和编译后端三个核心部件,并且通过中间表示(IR)作为各个部件之间的接口。这种设计带来了几个关键优势: 编译前端(Frontend):负责将各种高级语言源代码转换为LLVM中间表示(IR)。不同的语言可以有不同的前端实现,比如Clang处理C/C++/Objective-C,Swift前端处理Swift语言,Rust前端处理Rust语言。前端的主要任务包括词法分析、语法分析、语义分析、类型检查,最终生成抽象语法树(AST)并将其降低(Lower)为LLVM IR。 编译优化器(Optimizer):对中间表示进行各种优化,以提高代码性能。LLVM的优化器采用Pass-based架构,每个Pass负责一种特定的优化。优化可以分为多个层次:函数内优化(Intra-procedural)、过程间优化(Inter-procedural)、全局优化(Whole Program)。LLVM提供了超过100个内置Pass,并且支持用户自定义Pass。 编译后端(Backend):将优化后的中间表示转换为目标机器的代码。后端需要完成指令选择(Instruction Selection)、寄存器分配(Register Allocation)、指令调度(Instruction Scheduling)等任务。LLVM通过TableGen工具和目标描述文件(.td)来描述不同架构的特性,使得添加新的目标平台变得相对容易。 通过三段式的架构设计,LLVM可以通过灵活切换不同编程语言的前端实现,转化成通用的中间表示,并通过编译后端进行本机编译或者交叉编译适配成目标机器代码,从而实现了高可扩展性。LLVM能够快速支持各种新的编程语言,主要得益于三段式架构的高可扩展性。 这种架构的数学模型可以表示为:如果有M种源语言和N种目标架构,传统编译器需要M×N个编译器实现,而LLVM只需要M个前端+N个后端。这种O(M+N)的复杂度远低于O(M×N),大大降低了开发和维护成本。 更重要的是,LLVM IR作为中间层,提供了一个稳定的契约接口。前端开发者不需要了解后端的实现细节,后端开发者也不需要了解各种语言的语法特性。这种解耦使得编译器的各个部分可以并行开发和演进。 LLVM架构 传统编译器的三段式架构: 编译前端(Frontend)通过词法、语法、语义一系列分析,构建抽象语法树(AST),AST可以转换成某种中间表示(IR),作为编译优化器(Optimizer)的输入。编译优化器负责对中间代码进行优化,比如无用代码消除(Dead Code Elimination)、冗余指令合并(Common Subexpression Elimination)、函数内联(Function Inlining)等,以提升代码运行时性能。编译优化器输入IR,最终输出是优化过的IR。经过编译优化器(Optimizer)优化后的IR经过编译后端(Backend)转换成目标平台的机器码。 这个过程中,词法分析(Lexical Analysis)将源代码转换为token流,语法分析(Syntax Analysis)根据语言的文法规则构建语法树,语义分析(Semantic Analysis)进行类型检查、作用域解析等工作。这些步骤在编译原理中被称为编译器的"前端工作"。 通过这种组件化的设计,任何编程语言的编译器只要实现了上述3个部件,就能够把对应语言编写的源代码编译成目标平台可运行的机器代码。 于是支持多语言多平台的新架构被提出: 上述新架构支持不同的编程语言生成统一的中间表示(IR),新语言只用实现一个新的编译前端(Frontend),编译优化器(Optimizer)和编译后端(Backend)则可以复用。这种架构的核心思想是"一次编写,多次复用"(Write Once, Reuse Many Times)。 在现存的编译器中,JVM、.Net虚拟机提供了定义良好的中间表示字节码,理论上任意语言只要实现编译前端,支持把源代码转成字节码就可以使用JVM或者.Net虚拟机。但是运行时强制JIT编译、GC等机制并不适合像C这样的系统级编程语言。JVM和.Net的设计目标是托管环境(Managed Environment),它们假设有一个运行时系统来管理内存、处理异常、提供安全检查。这对于C/C++这种需要手动管理内存、直接操作硬件的语言来说是不合适的。 而另一个新架构的代表GCC,则因为早期设计中存在的耦合问题比如编译后端(Backend)需要遍历编译前端(Frontend)的AST生成调试信息,编译前端(Frontend)生成编译后端(Backend)的数据结构,以及全局变量和数据结构的滥用,导致三大编译组件耦合,代码复用性较差。GCC的这些设计缺陷是历史包袱,很难通过重构来解决,因为数百万行的代码已经基于这些假设编写。 LLVM在实现三段式架构中,汲取了GCC的教训,在设计中采用了严格的模块化设计,整个编译器由一系列可复用的库组成。这些库包括: libLLVMCore:核心IR和基础数据结构 libLLVMAnalysis:各种分析pass(控制流分析、数据流分析、别名分析等) libLLVMTransform:各种优化pass(常量折叠、循环优化、内联等) libLLVMCodeGen:代码生成框架 libLLVMTarget:目标机器描述 libLLVMSupport:通用工具和基础设施 每个库都有清晰的接口定义和职责划分,库之间的依赖关系是单向的、非循环的。这种设计使得开发者可以只链接需要的库,减少最终可执行文件的大小。例如,如果你只需要分析LLVM IR而不需要生成机器码,就可以只链接分析相关的库。 LLVM的架构体现了软件工程中的几个重要原则: 关注点分离(Separation of Concerns):每个组件只关注自己的职责 接口与实现分离:通过IR作为稳定的接口,隔离前端和后端 开放封闭原则:对扩展开放(可以添加新的Pass、新的前端、新的后端),对修改封闭(不需要修改核心代码) LLVM IR LLVM IR(Intermediate Representation)是LLVM整个架构的核心,它是一种强类型的、SSA(Static Single Assignment)形式的低级虚拟指令集。LLVM IR有三种等价的表示形式: ...