LLVM中的独立工具:
opt
:在IR级对程序进行优化的工具,输入必须是LLVM的bitcode,生成的输出文件必须具有相同的类型。llc
:通过特定后端将LLVM bitcode转换成目标汇编或目标问价的工具。llvm-mc
:能够汇编指令并生成像ELF、MachO、PE等对象格式的目标文件,也可以反汇编相同的对象,从而转存这些指令的相应汇编信息和内部LLVM机器指令数据结构。lli
:LLVM IP的解释器和JIT编译器。llvm-link
:将几个LLVM bitcode链接在一起,产生一个包含你所有输入的LLVM bitcode。llvm-as
:将人工可读的LLVM IR文件转换为LLVM bitcode。llvm-dis
:将LLVM bitcode解码成LLVM汇编码。LLVM中的基本库:
Clang是LLVM编译器的c-family前端,Clang可能意指三种不同的实体:
例如,对于如下size.c示例:
#include
typedef struct _PixelPacket{
char rt, gt, ot;
}PixelPacket;
int main(){
printf("%lu\n",sizeof(PixelPacket));
return 0;
}
可使用clang
命令来生成它的bitcode文件、汇编文件或是前端信息,如下:
clang -emit-llvm -c size.c -o size.bc
、
clang -emit-llvm -S -c size.c -o size.ll
、
clang -mllvm -print-after-all size.c -S -emit-llvm &> log.txt
-emit-llvm
标记会告诉clang
根据是否存在-c
或-S
来生成LLVM bitcode或是LLVM汇编码等信息。
也可使用clang -cc1
工具的一个特殊选项打印Clang的AST,如下(手动调用clang -cc1需要使用-I指定头文件位置):
clang -Xclang -ast-dump (-fsyntax-only) size.c
或
clang -cc1 -ast-dump (-fsyntax-only) size.c -I xx/xx/stdio.h
clang -cc1
工具不仅实现了编译器的前端,还通过LLVM库实例化了其它LLVM组件,以执行LLVM支持的所有编译功能。即clang -cc1几乎实现了完整的编译器。可使用-###
标志来显示由Clang驱动程序调用的程序列表,如下:
clang -### size.c -o size.o
源码转换为LLVM IR bitcode的过程需要经过前端解析,包含:预处理、词法分析、语法分析、语义分析一系列过程。以size.c代码为例,前端的第一个步骤是负责处理源代码的文本输入,它将语言结构拆分成一组单词和记号,并删除注释、空格、制表符等字符。每个记号包含一个SourceLocation类的实例,用于表示程序源代码的位置,可使用clang -cc1
输出词法分析中的所有行号和SourceLocation结果,如下:
clang -cc1 -dump-tokens size.c
C/C++预处理在进行任何语义分析之前使用,负责通过处理以#开头的预处理指令来展开宏、头文件,或者跳过部分代码。预处理器与词法分析器紧密相连,并连续相互作用。
在词法分析把源码解析成记号之后,编译器会进行语法分析,并将记号组合到一起形成表达式、语句和函数体等。这种分析过程也称为解析,它的输入为一个记号流,输出为AST。AST中最顶层的节点是TranslationUnitDeclare
类。它是所有其它AST节点的根,代表整个翻译单元。
以size.c代码为例,打印其AST视图如下(使用Xmanager打开Xshell进行操作):clang -fsyntax-only -Xclang -ast-view size.c
语义分析借助于符号表来确保代码是否违反编程语言的类型系统。该表主要存储了标识符与其各自类型之间的映射。一种直观的类型检查方法是在解析之后执行它,具体做法是从符号表中收集有关类型的信息同时遍历AST。但Clang它在生成AST节点的同时执行类型检查。
LLVM IR有三种等价的表达形式:内存表示(Instruction类等)、被压缩的磁盘表示(bitcode文件)、人工可读的磁盘表示(LLVM汇编码文件)
查看某种pass之后的IR信息:clang -mllvm -print-after=pass名称 size.c -S -emit-llvm
打印CFG(使用Xmanager打开Xshell进行操作):opt -view-cfg size.bc
使用llvm-dis查看bitcode汇编文本:llvm-dis < size.bc | less
LLC编译bitcode为汇编码:llc size.bc -o size.s
.bc和.ll相互转换:llvm-as size.ll -o size.bc
、llvm-dis size.bc -o size.ll
llvm提取工具llvm-extract
提取IR函数、全局变量、删除IR模块中的全局变量:llvm-extract -func=main size.bc -o size-main.bc
优化标志一般包含:-O0
、-O1
、-O2
、-O3
、-O4
、-Os
、-Oz
、-Ofast
。其中-Os
类似于-O2
,但它优化后减少了代码大小,-Oz
类似于-Os
,但它进一步优化使得代码大小更小,-O4
是使用-flto
的-O3
,-Ofast
是在-O3
上开启了更多的激进优化有一些需要严格语法检查的优化。Clang支持-O4
优化,但opt工具不支持,opt工具的处理对象是bitcode文件。
使用opt来优化bitcode文件,示例如下:
opt -O3 size.bc -o size-O3.bc
、
opt -std-compile-opts size.bc -o size-std.bc
opt工具也支持单独的代码优化流程,其中mem2reg
将allocs指令提升为LLVM局部变量。使用mem2reg
并计算模块中每种指令的数量,示例如下:
opt size.bc -mem2reg -instcount -o size-tmp.bc -stats
上述使用了-stats
标志使LLVM打印出每个流程的统计信息,也可使用-time-passes
标志来统计每次优化需要的执行时间,示例如下:
opt size.bc -time-passes -domtree -instcount -o size-tmp.bc
LLVM后端由一组代码生成分析器和变换流程组成。阶段大致分为:指令选择、指令调度(前寄存器分配调度)、寄存器分配、后寄存器分配调度、代码输出。
后端实现分散在LLVM源代码树的不同目录中。代码生成的主要库在lib目录及其子文件夹CodeGen
、MC
、TableGen
、Target
中:
CodeGen
目录包含所有通用代码生成算法的实现文件和头文件:指令选择、指令调度、寄存器分配以及它们所需的辅助分析函数。MC
目录包含汇编器、反汇编器和具体的对象文件(如:ELF、COFF、MachO)等底层功能的实现。TableGen
目录包含TableGen工具的完整实现,该工具用于根据.td
文件中的高层目标描述来生成C++代码。TableGen语言用于生成记录的定义和类组成。定义语句def用于实例化来自关键字class和multiclass的记录。代码生成器广泛使用TableGen记录来表示特定于目标的信息。
def FeatureAVX : SubtargetFeature<"avx", "X86SSELevel", "AVX",
"Enable AVX instructions",
[FeatureSSE42]>;
def FeatureAVX2 : SubtargetFeature<"avx2", "X86SSELevel", "AVX2",
"Enable AVX2 instructions",
[FeatureAVX]>;
def FeatureFMA : SubtargetFeature<"fma", "HasFMA", "true",
"Enable three-operand fused multiple-add",
[FeatureAVX]>;
该文件也可以包括所有其它.td文件,并且作为记录特定目标信息的主要文件。
// 16-bit registers
let SubRegIndices = [sub_8bit, sub_8bit_hi], CoveredBySubRegs = 1 in {
def AX : X86Reg<"ax", 0, [AL,AH]>;
def DX : X86Reg<"dx", 2, [DL,DH]>;
def CX : X86Reg<"cx", 1, [CL,CH]>;
def BX : X86Reg<"bx", 3, [BL,BH]>;
}
let SubRegIndices = [sub_8bit, sub_8bit_hi_phony], CoveredBySubRegs = 1 in {
def SI : X86Reg<"si", 6, [SIL,SIH]>;
def DI : X86Reg<"di", 7, [DIL,DIH]>;
def BP : X86Reg<"bp", 5, [BPL,BPH]>;
def SP : X86Reg<"sp", 4, [SPL,SPH]>;
}
def IP : X86Reg<"ip", 0>;
指令选择是将LLVM IR转换为代表目标指令的SelectionDAG节点(SDNode)的过程。第一步是从LLVM IR指令构建DAG,从而创建一个其节点执行IR操作的SelectionDAG对象。然后对这些节点执行降级、DAG组合器和合法化阶段,使其能够更容易与目标指令相匹配。指令选择流程是后端代码中执行时间最长的流程之一。
SelectionDAG类:SelectionDAG类使用DAG来表示每个基本块的计算,每个SDNode对应一个指令或操作数。SelectionDAG对象有一个表示基本块入口的特殊EntryToken节点,该节点对应一个类型为Other的值,以允许节点链使用它作为起点。SelectionDAG对象同时维护对DAG图的根节点引用,该根节点位于最后一条指令之后,根节点和最后一条指令的关系也被编码为一条Other类型的值链。
降级:TargetLowering类为一个抽象接口,为实现该类的抽象接口,每个编译目标都需要声明一个《Target》TargetLowering的子类,每个目标也需要重载那些将高层次的目标降低到更低层次、更接近目标机器的函数。如图显示指令选择之前的所有处理步骤。
DAG合并以及合法化:从SelectionDAGBuilder产生的输出SelectionDAG还需要经过如上图所示的一系列转换才能进行指令选择。DAG合并在每个合法化阶段后运行,以最小化SelectionDAG的冗余。DAG合法化程序与矢量合法化程序具有相同的作用,但它还负责处理对不支持类型的所有剩余操作。
DAG到DAG指令选择:其目的是通过使用模式匹配将目标无关节点转换成目标相关节点。指令选择算法是一个局部算法,每次在SelectionDAG(基本块)实例上执行。