面向程序设计语言LLVM杂谈
面向程序设计语言LLVM杂谈
如何为特定语言表达式生成 LLVM IR,请搜索接受相应对象的方法。
例如,对于 if-else 语句:
IRCodegenVisitor::codegenExprIR
Value *IRCodegenVisitor::codegen(const ExprIfElseIR &expr) {
... // this is the LLVM IR generation
}
了解 LLVM IR
LLVM 位于编译器的中间端,取消了语言功能之后,但在面向特定机器架构(x86、ARM 等)的后端之前。
LLVM的IR是相当低级的,它不能包含某些语言中存在的语言特征,而不能包含其他语言中不存在的语言特征(例如,类存在于C++中,但不存在于C中)。如果以前遇到过指令集,LLVM IR 是一个 RISC 指令集。
其结果是,LLVM IR 看起来是一种更具可读性的汇编形式。由于 LLVM IR 是独立于机器的,因此,无需担心寄存器的数量、数据类型的大小、调用约定或其他特定于机器的细节。
因此,在 LLVM IR 中,没有固定数量的物理寄存器,而是一组无限的虚拟寄存器(标记为 %0、%1、%2、%3...可以写作和阅读。后端的工作是从虚拟寄存器映射到物理寄存器。
没有分配特定大小的数据类型,而是在 LLVM IR 中保留类型。同样,后端将获取此类型信息并将其映射到数据类型的大小。LLVM 有不同大小的int和浮点数的类型,例如 int32、int8、int1 等。它还具有派生类型:如指针类型、数组类型、结构类型、函数类型。
现在,LLVM 中内置了一组优化,可以在 LLVM IR 上运行,例如死代码消除、函数内联、公共子表达式消除等。这些算法的细节无关紧要:LLVM 为实现了它们。
这边的讨价还价是,以静态单一赋值 (SSA) 形式编写 LLVM IR,因为 SSA 形式使优化编写者方便。SSA 形式听起来很花哨,但它只是意味着在使用之前定义变量,并且只分配给变量一次。在 SSA 形式中,不能重新赋值给变量,例如x = x+1 ;取而代之的是,每次都会分配给一个新变量 (x2 = x1 + 1)。
简而言之:LLVM IR 看起来像是带有类型的汇编,减去凌乱的机器特定细节。LLVM IR 必须采用 SSA 形式,这样更易于优化。看一个例子!
例如:阶乘
看一下 Bolt 语言中一个简单的阶乘函数:
function int factorial(int n){
if (n==0) {
1
}
else{
n * factorial(n - 1)
}
}
对应的 LLVM IR 如下:
define i32 @factorial(i32) {
entry:
%eq = icmp eq i32 %0, 0 // n == 0
br i1 %eq, label %then, label %else
then: ; preds = %entry
br label %ifcont
else: ; preds = %entry
%sub = sub i32 %0, 1 // n - 1
%2 = call i32 @factorial(i32 %sub) // factorial(n-1)
%mult = mul i32 %0, %2 // n * factorial(n-1)
br label %ifcont
ifcont:
; preds = %else, %then
%iftmp = phi i32 [ 1, %then ], [ %mult, %else ]
ret i32 %iftmp
}
注意,.ll
扩展用于人类可读的 LLVM IR 输出。还有位码.bc
,一种更紧凑的 LLVM IR 机器表示。
可以分 4 个详细级别来介绍此 IR:
请注意,LLVM IR 包含像 br和 icmp这样的汇编指令,但用一条call指令抽象了特定于机器的函数调用约定的混乱细节。
在控制流图级别:
如果退后一步,可以看到 IR 定义了程序的控制流图。IR 指令被分组到标记的基本块中,每个preds
块的标签表示该块的传入边沿。例如,ifcont
基本块有前身then
和else
:
依据控制流图和基本块。控制流图对程序执行不同的数据流分析。
phi指令表示条件赋值:根据刚刚来自的前一个基本块来分配不同的值。它的形式是phi类型[val1,predecessor1],[val2,predecorator2]。。。在上面的例子中,如果来自then块,将%iftmp设置为1;如果来自else块,则将%mult设置为1。Phi节点必须位于块的开头,并且每个前置节点都包含一个条目。
在功能层面:
再退一步,LLVM IR 中函数的整体结构如下:
在模块级别:
LLVM模块包含与程序文件相关联的所有信息。(对于多文件程序,会将它们对应的模块链接在一起。)
阶乘函数只是模块中的一个函数定义。如果想执行程序,例如计算factorial(10),需要定义一个主函数,它将是程序执行的入口点。主函数的签名是C的遗留(返回0表示执行成功):
example_program.c
// C main function
int main(){
factorial(10);
return 0;
}
在模块目标信息中指定要为英特尔Macbook Pro进行编译:
example_module.ll
Copy
source_filename = "Module"
target triple = "x86_64-apple-darwin18.7.0"
...
define i32 @factorial(i32) {
...
}
define i32 @main() {
entry:
%0 = call i32 @factorial(i32 10)
ret i32 0
}
LLVM API:关键概念
现在已经了解了LLVM IR的基本知识,然后在进一步探索LLVM IR时介绍更多API。
LLVM定义了一大堆类。
Value
Module
Type
Function
BasicBlock
BranchInst
...
值
单元
类型
作用
基本块
BranchInst…
这些都在名称空间llvm中。在Bolt repo中,选择通过将它们称为llvm::Value、llvm::Module等来显式设置名称空间。)
大多数LLVM API都是机械的。现在已经看到了定义模块、函数和基本块的关系图,它们在API中对应的类之间的关系很好地显现出来。可以查询Module对象以获得其Function对象的列表,查询Function以获得其BasicBlock的列表,反之亦然:可以查询BasicBlock以获得其父Function对象。
Value是程序计算的任何值的基类。这可以是一个函数(function子类化Value)、一个基本块(BasicBlock也子类化Value)、一条指令或中间计算的结果。
每个表达式codegen方法都返回一个Value*:执行该表达式的结果。您可以将这些代码生成方法视为生成该表达式的IR和表示包含表达式结果的虚拟寄存器的Value*。
virtual Value *codegen(const ExprIntegerIR &expr) override;
virtual Value *codegen(const ExprBooleanIR &expr) override;
virtual Value *codegen(const ExprIdentifierIR &expr) override;
virtual Value *codegen(const ExprConstructorIR &expr) override;
virtual Value *codegen(const ExprLetIR &expr) override;
virtual Value *codegen(const ExprAssignIR &expr) override;
实际上可以移动alloca。只要堆栈空间是在使用前分配的,那么在哪里分配堆栈空间并不重要。因此,在父函数的一开始就编写alloca,这个局部变量声明发生了。
如何在API中做到这一点?还记得构建器类似于文件指针吗?可以有多个指向文件中不同位置的文件指针。同样,实例化一个新的IRBuilder,指向父函数的入口基本块的开始,并使用该生成器插入alloca指令。
Copy
Function *parentFunction = builder->GetInsertBlock()
->getParent();
// create temp builder to point to start of function
IRBuilder<> TmpBuilder(&(parentFunction->getEntryBlock()),
parentFunction->getEntryBlock().begin());
// .begin() inserts this alloca at beginning of block
AllocaInst *var = TmpBuilder.CreateAlloca(boundVal->getType());
// resume our current position by using orig. builder
builder->CreateStore(someVal, var);
LLVM优化
API使添加过程变得非常容易。创建一个函数PassManager,添加想要的优化过程,然后初始化管理器。
std::unique_ptr<legacy::FunctionPassManager> functionPassManager =
make_unique<legacy::FunctionPassManager>(module.get());
// Promote allocas to registers.
functionPassManager->add(createPromoteMemoryToRegisterPass());
// Do simple "peephole" optimizations
functionPassManager->add(createInstructionCombiningPass());
// Reassociate expressions.
functionPassManager->add(createReassociatePass());
// Eliminate Common SubExpressions.
functionPassManager->add(createGVNPass());
// Simplify the control flow graph (deleting unreachable blocks etc).
functionPassManager->add(createCFGSimplificationPass());
functionPassManager->doInitialization();
在程序的每个功能上运行它:
for (auto &function : functions) {
Function *llvmFun =
module->getFunction(StringRef(function->functionName));
functionPassManager->run(*llvmFun);
}
Function *llvmMainFun = module->getFunction(StringRef("main"));
functionPassManager->run(*llvmMainFun);
具体而言,看一下 Bolt 编译器之前和之后的 LLVM IR 输出。可以在 repo 中找到它们:factorial
define i32 @factorial(i32) {
entry:
%n = alloca i32
store i32 %0, i32* %n
%1 = load i32, i32* %n
%eq = icmp eq i32 %1, 0
br i1 %eq, label %then, label %else
then: ; preds = %entry
br label %ifcont
else: ; preds = %entry
%2 = load i32, i32* %n
%3 = load i32, i32* %n
%sub = sub i32 %3, 1
%4 = call i32 @factorial(i32 %sub)
%mult = mul i32 %2, %4
br label %ifcont
ifcont: ; preds = %else, %then
%iftmp = phi i32 [ 1, %then ], [ %mult, %else ]
ret i32 %iftmp
}
优化版本:
define i32 @factorial(i32) {
entry:
%eq = icmp eq i32 %0, 0
br i1 %eq, label %ifcont, label %else
else: ; preds = %entry
%sub = add i32 %0, -1
%1 = call i32 @factorial(i32 %sub)
%mult = mul i32 %1, %0
br label %ifcont
ifcont: ; preds = %entry, %else
%iftmp = phi i32 [ %mult, %else ], [ 1, %entry ]
ret i32 %iftmp
}