面向程序设计语言LLVM杂谈

发布时间 2023-11-01 05:31:24作者: 吴建明wujianming

面向程序设计语言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基本块有前身thenelse

依据控制流图和基本块。控制流图对程序执行不同的数据流分析。

 

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*。

ir_codegen_visitor.h

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指令。

expr_codegen.cc

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,添加想要的优化过程,然后初始化管理器。

ir_codegen_visitor.cc

 

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();

在程序的每个功能上运行它:

ir_codegen_visitor.cc

 

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

阶乘未优化.ll

 

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

}

优化版本:

阶乘优化.ll

 

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

}