参考:https://www.bilibili.com/video/BV1D84y1y73v/?share_source=copy_web&vd_source=fc187607fc6ec6bbd2c74a3d0d7484cf
对源程序的一次完整扫描或处理。从高级语言到低级语言或者IR会经过几次Pass。比如下图第一个Pass是词法分析、语法分析、语义分析(图片里中间代码生成拼错了)。
可以是数据结构或者定义好的代码,能让编译器或者虚拟机表达源码就行。
利用IR做中转,将前端和优化和后端分离开。不同于GCC的前后端没有解耦,LLVM增加一种新的语言时只用实现一个新的编译前端,优化和后端都能复用。
clang -E -c hello.c -o hello.i
clang -emit-llvm hello.c -S -o hello.ll #这里是导出为IR模式
llc hello.ll -o hello.s #这里导出的是汇编语言了
clang hello.s -o hello #这里得到可执行的二进制文件
;
表示注释
@
全局变量开头
%
局部变量开头
alloca
在函数栈中分配内存
store
写入
load
读取
i32
32位4字节
align
字节对齐(计算机中内存大小的基本单位是字节(byte),理论上来讲,可以从任意地址访问某种基本数据类型,但是实际上,计算机并非逐字节大小读写内存,而是以2,4,或8的 倍数的字节块来读写内存,如此一来就会对基本数据类型的合法地址作出一些限制,即它的地址必须是2,4或8的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。)
操作码OP 第一操作数地址A1 第二操作数地址A2 结果地址A3
。%字符
命名临时寄存器。这三种中间格式是完全等价的:
如果在编译器的优化层对LLVM的IR进行操作,写一个定制的优化pass,就需要了解LLVM IR内存模型。
● LLVM IR文件的基本单位称为module;
● 一个module中可以拥有多个顶层实体,比如function和global variable;
● 一个function define中至少有一个basicblock(就是花括号);
● 每个basicblock中有若干instruction ,并且都以terminator instruction(写作ret,就是return的意思)结尾。
void test( int a, int b){
int c=a*b + 100;
}
1 ; Function Attrs: noinline nounwind optnone ssp uwtable
2 define void @test(i32, i32) #2 { ;有个全局函数@test (a,b)
3 %3 = alloca 132,align 4 ;局部变量C
4 %4 = alloca i32,align 4 ;局部变量d
5 %5 = alloca i32, align 4 ;局部变量e
6 store i32 %0, i32*%3,align 4 ;%0赋值给3C=a
7 store i32%1,i32*%4,align 4 ;%1赋值给%4d=b
8 %6=load i32, i32*%3, align 4 ;读取%3 ,赋值给%6就是函数参数a
9 %7=load i32, i32* %4 , align 4 ;读取%4 ,赋值给%7就是函数参数b
10 %8=mul nsw i32%6, %7 ;a*b
11 %9=add nsw i32%8, 100 ;a*b+100
12 store i32%9, i32*%5, align 4 ;参数%9赋值给%5 e ===>就是转换前函数写的int c变量
ret void
前端的第一个步骤处理源代码的文本输入,将语言结构分解为一组单词和标记,去除注释、空白、制表符等。每个单词或者标记必须属于语言子集,语言的保留字被变换为编译器内部表示。
分组标记以形成表达式、语句、函数体等。检查-组标记是否有意义 ,考虑代码物理布局,未分析代码的意思,就像英语中的语法分析,不关心你说了什么,只考虑句子是否正确,并输出语法树( AST )。
借助符号表检验代码没有违背语言类型系统。符号表存储标识符和其各自的类型之间的映射,以及其它内容。类型检查的一-种直觉的方法是,在解析之后,遍历AST的同时从符号表收集关于类型的信息。
优化通常由分析Pass和转换Pass组成:
在转换Pass和分析Pass之间,有两种主要的依赖类型:
DominatorTree &DT = getAnalysis<DominatorTree>(Func);
Pass类是实现优化的主要资源。然而,我们从不直接使用它,而是通过清楚的子类使用它。当实现一个Pass时,你应该选择适合你的Pass的最佳粒度,适合此粒度的最佳子类,例如基于函数、模块、循环、强联通区域,等等。常见的这些子类如下:
● ModulePass (一个模块)
● FunctionPass(一个函数)
● BasicBlockPass (某几条指令)
也是由多个Pass链接,分为必要Pass和非必要Pass,下面介绍一些必要Pass
第1次指令调度( Instruction Scheduling ) ,也称为前寄存器分配(RA)调度。
●对指令排序,同时尝试发现尽可能多的指令层次的并行;
●然后指令被变换为MachineInstr三地址表示。
LLVM IR寄存器集是无限的,这个性质一直保持着,直到寄存器分配( Register Allocation )
●寄存器分配将无限的虚拟寄存器引用转换为有限的目标特定的寄存器集;
●寄存器不够时挤出( spill )到内存。
第2次指令调度,也称为后寄存器分配(RA)调度。
●此时可获得真实的寄存器信息,某些类型寄存器存在延迟,它们可被用以改进指令顺序。
XLA (加速线性代数)是一种针对特定领域的线性代数编译器。
Julia面向科学计算的高性能动态编程语言,使用LLVM JIT编译。LLVM JIT编译器通常不断地分析正在执行的代码,并且识别代码的一部分 ,使得从编译中获得的性能加速超过编译该代码的性能开销。