[LLVM翻译]创建BOLT COMPILER:第8部分——编程语言创建者的LLVM完全指南。

1,461 阅读22分钟

原文地址:mukulrathi.co.uk/create-your…

原文作者:mukulrathi.co.uk/

发布时间:2020年12月24日-12分钟阅读

系列。创建BOLT编译器

即将推出


更新:这个帖子现在已经在Hacker NewsReddit上起飞了。谢谢大家

我有一个小小的请求。

在Twitter上分享这篇文章

这个教程是给谁看的?

这一系列编译器教程是为那些不只是想创建一个玩具语言的人准备的。你想要的是对象。你想要多态性。你想要并发性。你想要垃圾回收。等等,你不想要GC?好吧,不用担心,我们不会这么做 :P

如果你在这个阶段刚刚加入这个系列,这里有一个快速的回顾。我们正在设计一种Java式的面向对象的并发编程语言Bolt。我们已经完成了编译器前端,在那里我们已经完成了解析、类型检查和数据流分析。我们已经对我们的语言进行了去粗取精,为LLVM做好了准备--主要的收获是,对象已经被去粗取精为结构,它们的方法也被去粗取精为函数。

学习LLVM,你会成为朋友们羡慕的对象。Rust用LLVM做后端,所以它一定很酷。你会在所有的性能基准上打败他们,而不需要手工优化你的代码或编写机器汇编代码。嘘,我不会告诉他们的。

只要把代码给我

所有的代码都可以在Bolt编译器仓库中找到。

我们的desugared表示(我们称之为Bolt IR)的C++类定义可以在deserialise_ir文件夹中找到。本帖的代码(LLVM IR生成)可以在llvm_ir_codegen文件夹中找到。回帖使用了Visitor设计模式和大量使用std::unique_ptr来使内存管理更容易。

要切开模板,找出如何为特定语言表达式生成 LLVM IR,请搜索 IRCodegenVisitor::codegen 方法,该方法会接收相应的 ExprIR 对象,例如,对于 if-else 语句。

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有针对不同大小的ints和float的类型,例如int32、int8、int1等。它也有派生类型:如指针类型、数组类型、结构类型、函数类型。要了解更多,请查看Type文档。

现在,在LLVM中内置了一系列优化,我们可以在LLVM IR上运行,例如消除死码函数内联消除普通子表达式等。这些算法的细节是不相关的。这些算法的细节无关紧要:LLVM为我们实现了它们。

我们这边的交易是,我们以静态单任务(SSA)形式编写LLVM IR,因为SSA形式让优化编写者的生活更轻松。SSA形式听起来很花哨,但它只是意味着我们在使用前定义变量,并且只对变量赋值一次。在SSA形式下,我们不能对一个变量进行重新赋值,例如x = x+1;相反,我们每次都会赋值给一个新的变量(x2 = x1 + 1)。

所以简而言之:LLVM IR看起来像带有类型的汇编,减去了混乱的机器特定细节。LLVM IR必须是SSA形式,这使得它更容易优化。让我们来看一个例子

一个例子。Factorial

让我们看看我们语言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这样的汇编指令 但却用一条调用指令抽象出了机器特有的乱七八糟的函数调用约定的细节。

在控制流图层面。

如果我们退一步,你可以看到IR定义了程序的控制流图。IR指令被归为有标签的基本块,每个基本块的preds标签代表该块的传入边,例如ifcont基本块有前级then和else。

在这一点上,我假设你已经接触过控制流图和基本块。我们在该系列的前一篇文章中介绍了控制流图,我们用它来对程序进行不同的数据流分析。我建议你现在就去查看那篇数据流分析帖子中的CFG部分。我在这里等你:)

phi指令表示条件赋值:根据我们刚从哪个前面的基本块来分配不同的值。它的形式是phi类型[val1,predecessor1],[val2,predecessor2],...。在上面的例子中,如果我们来自then块,我们将%iftmp设为1,如果我们来自else块,则设为%mult。Phi节点必须在一个块的开始,并为每个前辈包含一个条目。

在函数层面。

再退一步说,LLVM IR中函数的整体结构如下:

在模块级。

一个LLVM模块包含了与程序文件相关的所有信息。对于多文件的程序,我们会把它们对应的模块链接在一起)。

我们的factorial函数只是我们模块中的一个函数定义。如果我们想执行程序,例如计算factorial(10),我们需要定义一个main函数,它将是我们程序执行的入口。主函数的签名是C语言的一个宿命(我们返回0表示执行成功)。

// a C main function
int main(){
  factorial(10);
  return 0;
}

我们在模块目标信息中指定要为Intel Macbook Pro编译。

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 API。我们将通过关键概念,然后在进一步探索LLVM IR时介绍更多的API。

LLVM定义了一大堆类,这些类映射到我们已经谈过的概念。

  • 价值
  • 模块
  • 种类
  • 功能
  • BasicBlock
  • 分院...

这些都在命名空间llvm中。在Bolt repo中,我选择了明确这个命名空间,将它们称为llvm::Value、llvm::Module等。)

大部分的LLVM API都是很机械的。现在你已经看到了定义模块、函数和基本块的图,它们在API中对应的类之间的关系就很好地落地了。你可以查询一个模块对象得到它的Function对象列表,查询一个Function得到它的BasicBlocks列表,反过来说:你可以查询一个BasicBlock得到它的父Function对象。

Value是程序计算的任何值的基类。这可能是一个函数(Function子类为Value),一个基本块(BasicBlock也子类为Value),一条指令,或者一个中间计算的结果。

每一个表达式codegen方法都会返回一个Value *:执行该表达式的结果。你可以把这些codegen方法看作是为该表达式生成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;

我们如何为这些表达式生成IR?我们创建一个独特的Context对象,将我们的整个代码生成联系在一起。我们使用这个Context来获得对核心LLVM数据结构的访问,例如LLVM模块和IRBuilder对象。

我们将使用上下文只创建一个模块,我们形象地将其命名为 "模块"。

context = make_unique<LLVMContext>();
builder = std::unique_ptr<IRBuilder<>>(new IRBuilder<>(*context));
module = make_unique<Module>("Module", *context);

IRBuilder

我们使用IRBuilder对象来逐步建立我们的IR。直观地讲,它相当于读/写文件时的文件指针--它携带着隐含的状态,例如,最后添加的指令,该指令的基本块等。就像移动文件指针一样,您可以通过SetInsertPoint(BasicBlock *TheBB)方法将构建器对象设置为在特定基本块的末尾插入指令。同样,你也可以用GetInsertBlock()来获取当前的基本块。

builder对象为每一条IR指令提供了Create___()方法,例如,CreateLoad为加载指令,CreateSub、CreateFSub分别为整数和浮点子指令等。有些Create__()指令会接受一个可选的Twine参数:这个参数用来给结果的寄存器取一个自定义的名字,例如iftmp是下面指令的twine。

%iftmp = phi i32 [ 1, %then ], [ %mult, %else]

使用IRBuilder文档找到与您的指令相对应的方法。

类型和常量

我们不直接构造这些,而是从它们对应的类中获取__()。LLVM会跟踪每个类型/常量类的唯一实例是如何使用的)。

例如,我们getSigned来获取特定类型和值的常量符号整数,getInt32Ty来获取int32类型。

Value *IRCodegenVisitor::codegen(const ExprIntegerIR &expr) {
  return ConstantInt::getSigned((Type::getInt32Ty(*context)),
                                      expr.val);
};

函数类型类似:我们可以使用FunctionType::get。函数类型由返回类型、参数类型的数组和函数是否为变量组成。

   FunctionType::get(returnType, paramTypes, false /* doesn't have variadic args */);

类型声明

我们可以声明自己的自定义结构类型。

例如,一个带有int值的Tree,以及指向左、右子树的指针。

%Tree = type {i32, Tree*, Tree* }

定义一个自定义结构类型是一个两阶段的过程。

首先,我们创建该名称的类型。这将它添加到模块的符号表中。这个类型是不透明的:我们现在可以在其他类型声明中引用,例如函数类型,或者其他结构类型,但是我们不能创建该类型的结构(因为我们不知道其中的内容)。

 StructType *treeType = StructType::create(*context, StringRef("Tree"));

LLVM使用StringRef和ArrayRef对字符串和数组进行装箱。在文档要求使用StringRef的地方,你可以直接传入一个字符串,但我选择在上面明确这个StringRef。

第二步是指定结构体中的类型数组。注意,由于我们已经定义了不透明的Tree类型,我们可以使用Tree类型的getPointerTo()方法获得一个Tree *类型。

treeType->setBody(ArrayRef<Type *>({Type::getInt32Ty(*context);, treeType->getPointerTo(), treeType->getPointerTo()}));

因此,如果你有自定义结构类型在它们的体中引用其他自定义结构类型,最好的方法是声明所有不透明的自定义结构类型,然后填入每个结构的体。

void IRCodegenVisitor::codegenClasses(
    const std::vector<std::unique_ptr<ClassIR>> &classes) {
  // create (opaque) struct types for each of the classes
  for (auto &currClass : classes) {
    StructType::create(*context, StringRef(currClass->className));
  }
  // fill in struct bodies
  for (auto &currClass : classes) {
    std::vector<Type *> bodyTypes;
    for (auto &field : currClass->fields) {
          // add field type
          bodyTypes.push_back(field->codegen(*this));
    }
    // get opaque class struct type from module symbol table
    StructType *classType =
        module->getTypeByName(StringRef(currClass->className));
    classType->setBody(ArrayRef<Type *>(bodyTypes));
  }

职能

函数的操作有类似的两步过程。

  1. 定义函数原型
  2. 填写它们的函数体(如果你要链接一个外部函数,请跳过这一步!)。

函数原型由函数名称、函数类型、"链接 "信息和我们要添加函数到其符号表的模块组成。我们选择外部链接--这意味着函数原型可以从外部查看。这意味着我们可以链接一个外部函数定义(例如,如果使用一个库函数),或者在另一个模块中暴露我们的函数定义。你可以在这里看到完整的链接选项

   Function::Create(functionType, Function::ExternalLinkage,
                           function->functionName, module.get());

要生成函数定义,我们只需要使用API来构建我们在因子实例中讨论过的控制流图。

void IRCodegenVisitor::codegenFunctionDefn(const FunctionIR &function) {
  // look up function in module symbol definition
  Function *llvmFun =
      module->getFunction(function.functionName);
  BasicBlock *entryBasicBlock =
      BasicBlock::Create(*context, "entry", llvmFun);
  builder->SetInsertPoint(entryBasicBlock);
  ...

Kaleidoscope官方教程对如何为if-else语句构造控制流图有很好的解释

更多LLVM IR概念

现在我们已经介绍了LLVM IR和API的基础知识,我们将再看一些LLVM IR的概念,同时介绍相应的API函数调用。

堆栈分配

在LLVM IR中,我们有两种方法可以在局部变量中存储值。我们已经看到了第一种:分配到虚拟寄存器。第二种是使用 alloca 指令将内存动态分配到栈中。虽然我们可以将ints、float和指针存储到栈或虚拟寄存器中,但集合数据类型,如结构和数组,不适合存储在寄存器中,所以必须存储在栈中。

是的,你没看错。与大多数编程语言的内存模型不同,我们使用堆来进行动态内存分配,在LLVM中,我们只有一个堆。

堆不是由LLVM提供的--它们是一个库特性。对于单线程应用来说,堆分配已经足够了。我们将在下一篇文章中讨论在多线程程序中对全局堆的需求(在这里我们扩展Bolt以支持并发)。

我们已经看到了结构类型,例如{i32,i1,i32}。数组类型的形式是[num_elems x elem_type]。注意num_elems是一个常数--你需要在生成IR时提供,而不是在运行时提供。所以 [3 x int32] 是有效的,但 [n x int32] 是无效的。

我们给alloca一个类型,它在堆栈上分配一个内存块,并返回一个指针,我们可以将其存储在一个寄存器中。我们可以使用这个指针从堆栈中加载和存储值。

例如,在栈上存储一个32位的int。

 %p = alloca i32 // store i32* pointer in %p
 store i32 1, i32* %p
 %1 = load i32, i32* %p

相应的构建指令是......你猜对了,CreateAlloca,CreateLoad,CreateStore。CreateAlloca返回Value *的一个特殊子类:AllocaInst *。

AllocaInst *ptr = builder->CreateAlloca(Type::getInt32Ty(*context),
                                         /* Twine */ "p");
// AllocaInst has additional methods e.g. to query type
ptr->getAllocatedType(); // returns i32
builder->CreateLoad(ptr);
builder->CreateStore(someVal, ptr);

全局变量

就像我们在堆栈上分配局部变量一样,我们可以创建全局变量,并从中加载和存储到它们。

全局变量在模块开始时被声明,并且是模块符号表的一部分。

我们可以使用模块对象来创建命名的全局变量,并查询它们。

module->getOrInsertGlobal(globalVarName, globalVarType);
...
GlobalVariable *globalVar = module->getNamedGlobal(globalVarName);

全局变量必须用一个常量值(而不是变量)来初始化。

globalVar->setInitializer(initValue);

另外,我们也可以使用GlobalVariable构造函数在一条命令中完成。

GlobalVariable *globalVar = new GlobalVariable(module, globalVarType, /*isConstant*/ false,
              GlobalValue::ExternalLinkage, initValue, globalVarName)

和以前一样,我们可以加载和存储。

builder->CreateLoad(globalVar);
builder->CreateStore(someVal, globalVar); // not for consts!

GEPs

我们在堆栈或全局内存中得到一个指向集合类型(数组/结构体)的基础指针,但是如果我们想要一个指向某个特定元素的指针呢?我们需要在集合中找到该元素的指针偏移量,然后将其加到基指针上,得到该元素的地址。指针偏移量的计算是机器特有的,例如,取决于数据类型的大小,结构的填充等。

获取元素指针(GEP)指令是将指针偏移量应用到基指针上并返回结果指针的指令。

考虑从p开始的两个数组,按照C语言的惯例,我们可以将该数组的指针表示为char或int

下面我们展示GEP指令,计算每个数组中的指针p+1。

// char = 8 bit integer = i8
%idx1 = getelementptr i8, i8* %p, i64 1 // p + 1 for char*
%idx2 = getelementptr i32, i32* %p, i64 1 // p + 1 for int*

这个GEP指令有点拗口,所以在这里给大家分析一下。

这个i64 1的索引将基类型的倍数加到基指针上。i8的p+1将增加1个字节,而i32的p+1将给p增加4个字节。如果索引是i64 0,我们将返回p本身。

创建GEP的LLVM API指令是......CreateGEP。

Value *ptr = builder->CreateGEP(baseType, basePtr, arrayofIndices);

等等?索引数组?是的,GEP指令可以有多个索引传递给它。我们已经看了一个简单的例子,我们只需要一个索引。

在我们看传递多个索引的情况之前,我想重申一下这个第一个索引的目的。

一个Foo *类型的指针在C语言中可以代表一个Foo类型数组的基本指针。第一个索引增加了这个基数类型Foo的倍数来遍历这个数组。

GEPS与Structs

好了,现在我们来看看结构。所以取一个Foo类型的结构。

  %Foo = type { i32, [4 x i32], i32}

我们要对结构中的特定字段进行索引。自然而然的方法是将它们标记为字段0、1和2。我们可以通过将字段2作为另一个索引传入GEP指令来访问。

  %ThirdFieldPtr = getelementptr  %Foo, %Foo* %ptr, i64 0, i64 2

然后,返回的指针计算为:ptr + 0 *(Foo的大小)+偏移量2 *(Foo的字段)。

对于结构,你很可能总是把第一个索引传为0,GEP最大的困惑就是这个0看起来是多余的,因为我们要的是字段2,那为什么我们要先传一个0的索引呢?希望你能从第一个例子中明白为什么我们需要这个0,就当是把一个大小为1的隐式Foo数组的基指针传给GEP。

为了避免混淆,LLVM有一个特殊的CreateStructGEP指令,它只询问字段索引(这就是为你添加了0的CreateGEP指令)。

 Value *thirdFieldPtr = builder->CreateStructGEP(baseStructType, basePtr, fieldIndex);

我们的聚合结构嵌套得越多,我们可以提供的索引就越多。例如,对于Foo的第二个字段(4个元素的int数组)的元素索引2。

  getelementptr  %Foo, %Foo* %ptr, i64 0, i64 1, i64 2

返回的指针是:ptr + 0 * (Foo的大小) + offset 1 * (Foo的字段) + offset 2 * (数组的elems)。

(在对应的API方面,我们会使用CreateGEP,并传递数组{0,1,2}。)

一篇好的演讲,很好的解释了GEP。

mem2reg

如果你还记得,LLVM IR必须以SSA形式编写。但如果我们试图映射到LLVM IR的Bolt源程序不是SSA形式的,会发生什么?

例如,如果我们要重新分配x:

let x = 1
x = x + 1

一种选择是我们在早期的编译器阶段以SSA形式重写程序。每当我们重新分配一个变量时,我们就必须创建一个新的变量。我们还必须为条件语句引入phi节点。对于我们的例子来说,这是很直接的,但一般来说,这种额外的重写是我们宁愿不处理的痛苦。

// Valid SSA: assign fresh variables
let x1 = 1
x2 = x1 + 1

我们可以使用指针来避免赋值新的变量。注意这里我们并不是重新赋值指针x,只是更新它所指向的值。所以这是有效的SSA。

// valid SSA: use a pointer and update the value it points to
let x = &1;
*x = *x + 1

这种对指针的转换比变量重命名要容易得多。它还有一个非常好的LLVM IR等价物:分配栈内存(并操作栈的指针),而不是从寄存器中读取。

因此,每当我们声明一个局部变量时,我们都会使用 alloca 来获取一个指向新分配的栈空间的指针。我们使用load和store指令来读取和更新指针指向的值。

 %x = alloca i32
store i32 1, i32* %x

%1 = load i32, i32* %x
%2 = add i32 %1, 1
store i32 %2, i32* %x

让我们重新审视一下LLVM IR,如果我们要重写Bolt程序来使用新鲜变量。它只需要2条指令,而如果使用堆栈则需要5条指令。此外,我们还避免了昂贵的加载和存储内存访问指令。

%x1 = 1
%x2 = add i32 %x1, 1   // let x2 = x1 + 1

因此,虽然我们通过避免重写到SSA的传递,让编译器编写者的生活变得更轻松,但这是以牺牲性能为代价的。

可喜的是,LLVM让我们吃到了蛋糕。

LLVM提供了一个mem2reg优化,将堆栈内存访问优化为寄存器访问。我们只需要确保在函数的入口基本块中声明所有局部变量的分配。

如果局部变量的声明发生在函数的中途,在另一个块中,我们该如何做呢?让我们来看一个例子。

// BOLT variable declaration
let x : int = someVal;

// translated to LLVM IR
%x = alloca i32
store i32 someVal, i32* %x

其实我们可以移动 alloca。只要在使用前分配好栈空间,我们在哪里分配并不重要。所以我们把alloca写在这个局部变量声明发生的父函数的最开始。

我们如何在API中做到这一点呢?好了,还记得构建器就像一个文件指针的比喻吗?我们可以有多个文件指针指向文件中的不同地方。同样,我们实例化一个新的IRBuilder来指向父函数的入口基本块的开始,并使用该构建器插入 alloca指令。

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使得添加通证变得非常容易。我们创建一个functionPassManager,添加我们想要的优化通证,然后初始化这个管理器。

  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中找到它们。

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
}

请注意,我们实际上已经去掉了 alloca 和相关的 load 和 store 指令,同时也去掉了当时的基本块!

收拾

最后这个例子向你展示了LLVM的强大功能及其优化。你可以在Bolt仓库的main.cc文件中找到运行LLVM代码生成和优化的顶层代码。

在接下来的几篇文章中,我们将关注一些更高级的语言特性:属、继承、方法重写和并发!当它们出现时,请继续关注。请继续关注它们的发布时间!


通过www.DeepL.com/Translator(免费版)翻译