LLVM 简介 (一)
LLVM项目
LLVM是一个开源的项目,是一个编译器框架,是一系列模块化、可重用的编译器以及工具链技术的集合。
LLVM的核心是LLVM库。同时LLVM还实现了一些周边工具。
LLVM的一个设计思想是优化可以渗透在整个编译流程中各个阶段,比如编译时、链接时、运行时等。
你可以基于LLVM提供的功能开发自己的模块,并集成在LLVM系统上,增加它的功能;或者利用LLVM来支撑底层实现,开发自己的工具。它可以很容易和IDE集成(因为IDE软件可以直接调用库来实现一些如静态检查这些功能),也很容易构建生成各种功能的工具(因为新的工具只需要调用需要的库就行)。
可见 https://github.com/llvm/llvm-project https://llvm.org/docs/GettingStarted.html
LLVM最初代表Low Level Virtual Machine, 但是随着LLVM家族越庞大,这个最初的意思已经不适用。llvm.org 上的介绍很明确, LLVM就是这个项目的全称。
LLVM项目最初由UIUC的Vikram 和 Chris Lattner于2000年开始开发。他们的最初的目的是为了静态和动态编程语言开发一个动态编译技术。2005年,Chris进入苹果公司,继续进行LLVM的开发。2013年,索尼公司也开始使用Clang开发PS4。
在这之前,Apple
公司一直使用gcc
作为编译器,后来GCC对Objective-C
的语言特性支持一直不够,Apple自己开发的GCC模块又很难得到GCC委员会的合并。等到Chris Lattner毕业时,Apple就把他招入靡下,去开发自己的编译器,所以LLVM最初受到了Apple的大力支持。
广义的LLVM是指整个LLVM架构,狭义的LLVM指的是LLVM后端(包含代码优化和目标代码生成)。
Clang
LLVM只是一个编译器框架,所以还需要一个前端来支撑整个系统,所以Apple研发了Clang,作为整个编译器的前端,Clang用来编译C、C++和Objective-C。可以用Clang/LLVM来和gcc做对比
Clang与LLVM的关系如图
相比于GCC,Clang具有如下优点
- 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
- 占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右
- 模块化设计:Clang采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
- 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
当然除了Clang,还可以使用其他前端,见后续介绍。
LLVM 架构
参考了:http://www.aosabook.org/en/llvm.html https://www.jianshu.com/p/1367dad95445
传统编译器架构
传统的编译器框架如下,三个阶段:
Frontend:前端,进行词法分析、语法分析、语义分析、生成中间代码。 解析源代码,检查错误,并且把它翻译为特定语言语法抽象树(language-specific Abstract Syntax Tree)。AST树也可以被进一步为了优化转换为中间表示(比如,LLVM Intermedaite Representaion), 后面的2个阶段都使用这个中间表示。
Optimizer:优化器,中间代码优化,比如去除无用的变量或者无用的计算,来提高代码运行效率。
Backend:后端。Code Generation,生成目标机器码(target instruction set)。Backend目标是生成充分可以利用目标机器体系结构的native code。
这种设计最大的优势就是解耦合,让每部分专注于自己的功能。比如如果所有的语言都使用了同一种中间表示,那么意味着可以使用相同的Optimizer。这就大大提高了代码重复利用率以及Optimizer优化。
LLVM设计思路
传统架构的缺点
经典的编译器如gcc
在设计上都是提供一条龙服务的: 你不需要知道它使用的IR是什么样的,它也不会暴露中间接口来给你操作它的IR。 换句话说,从前端到后端,这些编译器的大量代码都是强耦合的。
这样做有好处也有坏处。好处是,因为不需要暴露中间过程的接口,它可以在内部做任何想做的平台相关的优化。
而坏处是,每当一个新的平台出现,这些编译器都要各自为政实现一个从自己的IR到新平台的后端。 甚至如果当一种新语言出现,且需要实现一个新的编译器,那么可能需要设计一个新的IR,以及针对大部分平台实现这个IR的后端。 不妨想一下,如果有M种语言、N种目标平台,那么最坏情况下要实现 M*N 个前后端。这是很低效的。
因此,我们很自然地会想,如果大家都共用一种IR呢? 那么每当新增加一种语言,我们就只要添加一个这个语言到IR的前端; 每当新增加一种目标平台,我们就只要添加一个IR到这个目标平台的后端。 如果有M种语言、N种目标平台,那么最优情况下我们只要实现 M+N 个前后端。
LLVM就是这样一个项目。LLVM的核心设计了一个叫 LLVM IR 的中间表示, 并以库(Library) 的方式提供一系列接口, 为你提供诸如操作IR、生成目标平台代码等等后端的功能。
LLVM架构
特点可以概括为:
- 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
- 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改
- 相比之下,GCC的前端和后端没分得太开,前端后端耦合在了一起。所以GCC为了支持一门新的语言,或者为了支持一个新的目标平台,就 变得特别困难
- LLVM现在被作为实现各种静态和运行时编译语言的通用基础架构(C家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
基于IR的LLVM编译器是一个强有力和灵活的工具。LLVM编译器三个阶段之间使用IR进行数据交流。
前端
在LLVM编译器里,前端(front end)会对源代码进行解析,验证,诊断错误。通常LLVM的前端,会先构建一个语法抽象树(Abstract Syntax Tree), 然后把AST转化为LLVM 的中间表示(LLVM Intermedaite Representation)。
这个LLVM IR 中间表示会作为优化分析的pipeline的输入。
注意有一个LLVM IR linker,这个是IR的链接器,而不是gcc中的链接器。为了实现链接时优化,LLVM在前端(Clang)生成单个代码单元的IR后,将整个工程的IR都链接起来,同时做链接时优化。
优化
一个LLVM 优化器是由很多pass组成的流水线(pipeline)。
详见下面 LLVM PASS 中 LLVM PASS与LLVM Optimizer
后端
LLVM backend产生目标机器码。
LLVM backend就是LLVM真正的后端,也被称为LLVM核心,包括编译、汇编、链接,最后生成汇编文件或者目标码。这里的LLVM compiler和gcc中的compiler不一样,这里的LLVM compiler只是编译LLVM IR。
详见下面的 LLVM PASS与LLVM Code Genarator
LLVM各部分与gcc的对应关系
gcc的编译器,输入是源代码,输出是汇编代码,相当于是LLVM中Clang一级加上IR linker再加上LLVM compiler中的生成汇编代码部分(Clang输出可执行文件的一条龙过程,不会生成汇编文件,内部全部走中间表示,生成汇编码和生成目标文件是并列的)。
gcc的汇编器,输入是汇编代码,输出是目标文件,相当于是LLVM中的llvm-mc
(这是另一个工具,Clang一条龙默认不走这个工具,但会调用相同的库来做汇编指令的下降和发射)。
gcc的链接器,输入是目标文件,输出是最终可执行文件,相当于LLVM中的Linker,现在LLVM Linker还在开发中(已释出,叫lld,但仍然不成熟),所以Clang驱动程序调起来的链接器还是系统链接器,可以选择使用gcc的ld(这块会很快变,LLVM社区必然会在lld成熟后默认换上去,大家可以自行验证)。
参考:https://zhuanlan.zhihu.com/p/140462815
LLVM PASS
pass的概念
https://zhuanlan.zhihu.com/p/122522485 https://zhuanlan.zhihu.com/p/77393146
Pass就是“遍历一遍IR,可以同时对它做一些操作”的意思。翻译成中文应该叫“趟”。LLVM Pass 是LLVM最重要的一个概念。Pass代表了一个处理步骤比如内联函数的替换, 表达式重写,循环优化。
在实现上,LLVM的核心库中会给你一些 Pass类 去继承。
所有的LLVM pass都有一个父类, Pass class。很多LLVM pass是被实现在一个单独的.cpp文件里。下面的例子是个一个简单的LLVM Pass例子。LLVM pass需要被定义在一个匿名的命名空间,保持对定义文件的私有可见性。最后一行代码,用于让外部可以创建使用这个pass。
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 Pass的作用
- 显然它的一个用处就是插桩: 在Pass遍历LLVM IR的同时,自然就可以往里面插入新的代码。
- 机器无关的代码优化:IR在被翻译成机器码前会做一些机器无关的优化。 但是不同的优化方法之间需要解耦,所以自然要各自遍历一遍IR,实现成了一个个LLVM Pass。 最终,基于LLVM的编译器会在前端生成LLVM IR后调用多个LLVM Pass做机器无关优化, 然后再调用LLVM后端生成目标平台代码。
- 静态分析: 像VSCode的C/C++插件就会用LLVM Pass来分析代码,提示可能的错误 (无用的变量、无法到达的代码等等)。
LLVM PASS与LLVM Optimizer
一个LLVM 优化器是由很多pass组成的流水线(pipeline)
LLVM编译器优化级别可以用参数指定, -O0 表示不使用任何pass没有优化,-O3 使用67passes进行优化(LLVM 2.8)。
LLVM优化器提供了很多passes。它们的实现方式类似。所有的passes被编译到一个或者很多.o文件,然后再被编译为archive libraries (Linux 系统使用后缀 .a 表示 )。LLVM编译器使用这些库文件进行优化。
这些passes之间是松耦合的,尽量不依赖其他的pass。如果有的pass依赖于其他的pass,那么这个pass就要显示的声明这些依赖关系。
当我们要使用具有依赖性的pass,LLVM PassManager会使用这些依赖声明,加载依赖库,然后执行这个pass。
LLVM最强大的地方在于,虽然LLVM提供很多pass,但是如何使用他们取决于用户。比如下图一个用户使用LLVM实现一个新的编译器XYZoptimizer。这个新的编译器只使用3 LLVM Pass (A, B, D)。最终的XYZoptimizer.o 不变包含整个LLVM, 它只链接它使用的pass。
LLVM PASS 与 LLVM Code Generator
Code Generator把LLVM IR编译成为目标机器码。
每一个不同硬件平台的Code Generator都不会一样,但是它们也有很多相似的操作。比如寄存器分配, 尽管每一个硬件平台不一样,但是分配算法是通用的。
所以Code Generator因此也可以被分成不同的独立的passes, 指令选择,寄存器分配,调度,代码布局优化和汇编语言生成。LLVM为Code Generator提供了很多passes。
用户可以自己选择不同的pass,重写默认实现或者增加自己的pass。这种组装的灵活性让用户不必从头实现Code Generator。
对于不同平台不同的地方, LLVM提供了目标表述文件(.td 文件)。LLVM使用tblgen工具来处理这些描述文件。
LLVM IR
LLVM IR之所以流行, 其原因之一是,它是一个完备强大的代码中间表示。每一阶段基于LLVM IR做自己的事情,无需去关注前阶段,或者后阶段的事情。作为一个反例, 比较成功的GCC编译器无法做到这一点。GCC编译器代码的中间表示是不完备的。比如它的优化过程,在某些情况下(提供DWARF debug信息),需要回溯到上一阶段。
这部分可见 http://www.aosabook.org/en/llvm.html https://llvm.org/docs/LangRef.html
在LLVM中,IR有三种表示,一种是可读的IR,类似于汇编代码,但其实它介于高等语言和汇编之间,这种表示就是给人看的,磁盘文件后缀为.ll
;第二种是不可读的二进制IR,被称作位码(bitcode),磁盘文件后缀为.bc
;第三种表示是一种内存格式,只保存在内存中,这种格式是LLVM之所以编译快的一个原因,它不像gcc,每个阶段结束会生成一些中间过程文件,它编译的中间数据都是这第三种表示的IR。三种格式是完全等价的,我们可以在Clang/LLVM工具的参数中指定生成这些文件(默认不生成,对于非编译器开发人员来说,也没必要生成),可以通过llvm-as
和llvm-dis
来在前两种文件之间做转换。
LLVM模块化设计的好处
LLVM IR是可以被序列化的形式存储为bitcode文件。并且因为LLVM IR不需要依赖其他东西(self-contained), 所以这个序列化的存储,是耗费很小的。
我们可以存储优化过程中的LLVM IR中间结果,在将来的某个点继续加载进行处理。这个特征就让LLVM支持Link-Time Optimization(LTO)和 Install-Time optimization。
LTO解决了传统编译器只能扫描一遍要翻译的单元,不能做跨文件的优化的问题。
LLVM编译器(比如Clang)可以把LLVM IR 的bitcode表示,写到.o文件里,而不是本地机器码。把本地机器码的产生推迟到link time。如下图所示,在link time,可以做cross-boundaries的优化。
Intall-Time 把LLVM backend从link time 分离出来。
这样做的好处是,比如使用不同平台的用户可以下载相同的link time的输出,在用户本地翻译长目标机器码,如下图所示。
对于编译的测试,LLVM也具有很大的优势。
编译器非常复杂,GCC测试的时候,很难确定是什么引起测试失败。但是LLVM编译器可以基于LLVM IR进行单元测试。
在测试的时候,一旦发现一个测试失败。修复这个bug的第一步是,在得到一个可以重现这个bug的测试用例。一旦,找到这个测试用例,就要逐渐减小的它的范围,从而知道哪一部分包含了这个bug。
人工完成这个过程非常繁琐,LLVM提供一个BugPoint的工具来做这个事情。
LLVM工具
LLVM的核心是一些库,而不是一个具体的二进制程序。 不过,LLVM这个项目本身也基于这个库实现了周边的工具
LLVM工具用来调用LLVM的一部分库,实现库的功能,通常使用编译器的人会调用到这些工具。 注意区分库和工具的概念,工具通过调用库实现功能。
主要可以参考https://llvm.org/docs/GettingStarted.html#llvm-tools 。以及https://llvm.org/docs/CommandGuide/、、https://llvm.org/
bugpoint bugpoint is used to debug optimization passes or code generation backends by narrowing down the given test case to the minimum number of passes and/or instructions that still cause a problem, whether it is a crash or miscompilation. See HowToSubmitABug.html for more information on using bugpoint. llvm-ar The archiver produces an archive containing the given LLVM bitcode files, optionally with an index for faster lookup. llvm-as The assembler transforms the human readable LLVM assembly to LLVM bitcode. llvm-dis The disassembler transforms the LLVM bitcode to human readable LLVM assembly. llvm-link llvm-link, not surprisingly, links multiple LLVM modules into a single program. lli lli is the LLVM interpreter, which can directly execute LLVM bitcode (although very slowly…). For architectures that support it (currently x86, Sparc, and PowerPC), by default, lli will function as a Just-In-Time compiler (if the functionality was compiled in), and will execute the code much faster than the interpreter. llc llc is the LLVM backend compiler, which translates LLVM bitcode to a native code assembly file. opt opt reads LLVM bitcode, applies a series of LLVM to LLVM transformations (which are specified on the command line), and outputs the resultant bitcode. ‘opt -help’ is a good way to get a list of the program transformations available in LLVM. opt can also run a specific analysis on an input LLVM bitcode file and print the results. Primarily useful for debugging analyses, or familiarizing yourself with what an analysis does.
以及Clang
Clang是现在LLVM项目中一个很重要的前端工具。clang能够调用起来整个编译器的流程,也就是上边其他工具调用的库,它很多都同样会调用。clang通过指定-emit-llvm
参数,可以配合-S
或-c
生成.ll
或.bc
文件,这样我们就能把Clang的部分和LLVM的后端分离开来独立运行,对于观察编译器流程来说,很实用。
clang -emit-llvm -c main.c -o main.bc
clang -emit-llvm -S main.c -o main.ll
概念上的一词多义
https://zhuanlan.zhihu.com/p/140462815
LLVM
- LLVM项目或基础架构:这是对整个LLVM编译器框架的程序,包括了前端、优化器、后端、汇编器、链接器,以及libc++、JIT等。上下文如:“LLVM项目由以下几个模块组成”。
- 基于LLVM开发的编译器:这是指一部分或全部基于LLVM项目开发的编译器软件,软件可能基于LLVM的前端或后端来实现。上下文如:“我用LLVM将C语言编译到MIPS平台”。
- LLVM库:LLVM项目由库代码和一些工具组成,有时会指代LLVM库内容。上下文如:“我的项目使用了LLVM的即时编译框架”(JIT是其中一个库)。
- LLVM核心:在IR和后端算法上的内容,就是LLVM核心,也就是通常Clang/LLVM中的LLVM。上下文如:“LLVM和Clang是两个项目”。
- LLVM IR:有些时候也会指代其中间表示。上下文如:“Clang是一个前端,能将源代码翻译成LLVM”。
Clang
通常我们在命令行上调用的clang工具,是Clang驱动程序,因为LLVM本质上只是一个编译器框架,所以需要一个驱动程序把整个编译器的功能串起来,clang能够监控整个编译器的流程,即能够调用到Clang和LLVM的各种库,最终实现编译的功能。
参考:
官方文档:
http://www.aosabook.org/en/llvm.html
https://llvm.org/docs/index.html
https://llvm.org/docs/GettingStarted.html
https://llvm.org/pubs/2008-10-04-ACAT-LLVM-Intro.pdf
https://llvm.org/docs/LangRef.html
https://github.com/llvm/llvm-project
博客:
https://zhuanlan.zhihu.com/p/75723370
https://zhuanlan.zhihu.com/p/77393146
https://www.jianshu.com/p/1367dad95445
https://zhuanlan.zhihu.com/p/140462815
https://zhuanlan.zhihu.com/p/122522485
https://bbs.pediy.com/thread-251326.htm
后续学习资料:
http://www.cs.cmu.edu/afs/cs.cmu.edu/academic/class/15745-s14/public/lectures/
https://www.zhihu.com/column/c_1250484713606819840