LLVM IR代码生成codegen分析

发布时间 2023-10-18 19:47:12作者: 吴建明wujianming

LLVM IR代码生成codegen分析

常用指令

1.从源码生成.ll
clang main.c -emit-llvm -S -c -o main.ll
2.运行.ll
lli main.ll
3.编译汇编
llc main.ll
4.生成dot,得到可视化的DAG
llc -view-dag-combine1-dags main.ll
5.将源文件分别编译为LLVM二进制代码
clang -emit-llvm -c main.c -o main.bc
6.生成目标文件,通过系统链接器链接多个目标文件,生成可执行文件
llc -filetype=obj main.bc -o main.o
llc -filetype=obj sum.bc -o sum.o
clang main.o sum.o -o sum
7.使用优化器优化 IR/BC 代码,如将Store/Load优化为PHYNode
opt t.bc -o t.opt.bc -O3
llvm-dis t.opt.bc -o t.opt.ll

表达式代码生成

为表达式节点生成LLVM代码的过程:连带注释只需区区45行代码便足以搞定全部四种表达式节点。首先是数值常量:

Value *NumberExprAST::Codegen() {
  return ConstantFP::get(getGlobalContext(), APFloat(Val));
}

LLVM IR中的数值常量是由ConstantFP类表示的。在其内部,具体数值由APFloat(Arbitrary Precision Float,可用于存储任意精度的浮点数常量)表示。这段代码就是新建并返回了一个ConstantFP对象。值得注意的是,在LLVM IR内部,常量都只有一份,并且是共享的。因此,API往往会采用”foo:get(...)“的形式而不是“new foo(...)”或“foo::Create(...)”。

Value *VariableExprAST::Codegen() {
  // Look this variable up in the function.
  Value *V = NamedValues[Name];
  return V ? V : ErrorV("Unknown variable name");
}

在LLVM中引用变量也很简单。实际上,位于NamedValues映射表中的变量只可能是函数的调用参数。这段代码首先确认给定的变量名是否存在于映射表中(如果不存在,就说明引用了未定义的变量),然后返回该变量的值。

Value *BinaryExprAST::Codegen() {
  Value *L = LHS->Codegen();
  Value *R = RHS->Codegen();
  if (L == 0 || R == 0) return 0;
 
  switch (Op) {
  case '+': return Builder.CreateFAdd(L, R, "addtmp");
  case '-': return Builder.CreateFSub(L, R, "subtmp");
  case '*': return Builder.CreateFMul(L, R, "multmp");
  case '<':
    L = Builder.CreateFCmpULT(L, R, "cmptmp");
    // Convert bool 0/1 to double 0.0 or 1.0
    return Builder.CreateUIToFP(L, Type::getDoubleTy(getGlobalContext()),
                                "booltmp");
  default: return ErrorV("invalid binary operator");
  }
}

二元运算符的处理就比较有意思了。其基本思想是递归地生成代码,先处理表达式的左侧,再处理表达式的右侧,最后计算整个二元表达式的值。上述代码就opcode的取值用了一个简单的switch语句,从而为各种二元运算符创建出相应的LLVM指令。

在上面的例子中,LLVM的Builder类逐渐开始凸显出自身的价值。2.4只需想清楚该用哪些操作数(即此处的L和R)生成哪条指令(通过调用CreateFAdd等方法)即可,至于新指令该插入到什么位置,交给IRBuilder就可以了。此外,如果需要,2.4还可以给生成的指令指定一个名字。

LLVM的优点之一在于此处的指令名只是一个提示。举个例子,假设上述代码生成了多条“addtmp”指令,LLVM会自动给每条指令的名字追加一个自增的唯一数字后缀。指令的local value name完全是可选的,但它能大大提升dump出来的IR代码的可读性。

LLVM指令遵循严格的约束:例如,add指令的Left、Right操作数必须同属一个类型,结果的类型则必须与操作数的类型相容。由于Kaleidoscope中的值都是双精度浮点数,add、sub和mul指令的代码得以大大简化。

然而,LLVM要求fcmp指令的返回值类型必须是‘i1’(单比特整数)。问题在于Kaleidoscope只能接受0.0或1.0。为了弥合语义上的差异,2.4给fcmp指令配上一条uitofp指令。这条指令会将输入的整数视作无符号数,并将之转换成浮点数。相应地,如果用的是sitofp指令,Kaleidoscope的‘<’运算符将视输入的不同而返回0.0或-1.0。

Value *CallExprAST::Codegen() {
  // Look up the name in the global module table.
  Function *CalleeF = TheModule->getFunction(Callee);
  if (CalleeF == 0)
    return ErrorV("Unknown function referenced");
 
  // If argument mismatch error.
  if (CalleeF->arg_size() != Args.size())
    return ErrorV("Incorrect # arguments passed");
 
  std::vector<Value*> ArgsV;
  for (unsigned i = 0, e = Args.size(); i != e; ++i) {
    ArgsV.push_back(Args[i]->Codegen());
    if (ArgsV.back() == 0) return 0;
  }
 
  return Builder.CreateCall(CalleeF, ArgsV, "calltmp");
}

函数调用的代码生成非常直截了当。上述代码开头的几行是在LLVM Module的符号表中查找函数名。如前文所述,LLVM Module是个容器,待处理的函数全都在里面。只要保证各函数的名字与用户指定的函数名一致,可以利用LLVM的符号表替完成函数名的解析。

拿到待调用的函数之后,就递归地生成传入的各个参数的代码,并创建一条LLVM call指令。注意,LLVM默认采用本地的C调用规范,这样以来,就可以毫不费力地调用标准库中的“sin”、“cos”等函数了。

Kaleidoscope中的四种基本表达式的代码生成就介绍完了。尽情地添枝加叶去吧。去试试LLVM语言参考上的各种千奇百怪的指令,以当前的基本框架为基础,支持这些指令易如反掌。

函数的代码生成

函数原型和函数的代码生成比较繁琐,相关代码不及表达式的代码生成来得优雅,不过却刚好可以用于演示一些重要概念。首先,看看函数原型的代码生成过程:函数定义和外部函数声明都依赖于它。这部分代码一开始是这样的:

Function *PrototypeAST::Codegen() {
  // Make the function type:  double(double,double) etc.
  std::vector<Type*> Doubles(Args.size(),
                             Type::getDoubleTy(getGlobalContext()));
  FunctionType *FT = FunctionType::get(Type::getDoubleTy(getGlobalContext()),
                                       Doubles, false);
 
  Function *F = Function::Create(FT, Function::ExternalLinkage, Name, TheModule);

短短几行暗藏玄机。首先需要注意的是该函数的返回值类型是“Function*”而不是“Value*”。“函数原型”描述的是函数的对外接口(而不是某表达式计算出的值),返回代码生成过程中与之相对应的LLVM Function自然也合情合理。

FunctionType::get调用用于为给定的函数原型创建对应的FunctionType对象。在Kaleidoscope中,函数的参数全部都是double,因此第一行创建了一个包含“N”个LLVM double的vector。随后,FunctionType::get方法以这“N”个double为参数类型、以单个double为返回值类型,创建出一个参数个数不可变(最后一个参数false就是这个意思)的函数类型。注意,和常数一样,LLVM中的类型对象也是单例,应该用“get”而不是“new”来获取。

最后一行实际上创建的是与该函数原型相对应的函数。其中包含了类型、链接方式和函数名等信息,还指定了该函数待插入的模块。“ExternalLinkage”表示该函数可能定义于当前模块之外,且/或可以被当前模块之外的函数调用。Name是用户指定的函数名:如上述代码中的调用所示,既然将函数定义在“TheModule”内,函数名自然也注册在“TheModule”的符号表内。

  // If F conflicted, there was already something named 'Name'.  If it has a
  // body, don't allow redefinition or reextern.
  if (F->getName() != Name) {
    // Delete the one we just made and get the existing one.
    F->eraseFromParent();
    F = TheModule->getFunction(Name);

在处理名称冲突时,Module的符号表与Function的符号表类似:在模块中添加新函数时,如果发现函数名与符号表中现有的名称重复,新函数会被默默地重命名。上述代码用于检测函数有否被定义过。

在两种情况下允许重定义函数:第一,允许对同一个函数进行多次extern声明,前提是所有声明中的函数原型保持一致。第二,允许先对函数进行extern声明,再定义函数体。这样一来,才能定义出相互递归调用的函数。

为了实现这些功能,上述代码首先检查是否存在函数名冲突。如果存在,(调用eraseFunctionParent)将刚刚创建的函数对象删除,然后调用getFunction获取与函数名相对应的函数对象。请注意,LLVM中有很多erase形式和remove形式的API。remove形式的API只会将对象从父对象处摘除并返回。erase形式的API不仅会摘除对象,还会将之删除。

    // If F already has a body, reject this.
    if (!F->empty()) {
      ErrorF("redefinition of function");
      return 0;
    }
 
    // If F took a different number of args, reject.
    if (F->arg_size() != Args.size()) {
      ErrorF("redefinition of function with different # args");
      return 0;
    }

为了在上述代码的基础上进一步进行校验,看看之前定义的函数对象是否为“空”。换言之,也就是看看该函数有没有定义基本块。没有基本块就意味着该函数尚未定义函数体,只是一个前导声明。如果已经定义了函数体,就不能继续下去了,抛出错误予以拒绝。如果之前的函数对象只是个“extern”声明,则检查该函数的参数个数是否与当前的参数个数相符。如果不符,抛出错误。

  // Set names for all arguments.
  unsigned Idx = 0;
  for (Function::arg_iterator AI = F->arg_begin(); Idx != Args.size();
       ++AI, ++Idx) {
    AI->setName(Args[Idx]);
 
    // Add arguments to variable symbol table.
    NamedValues[Args[Idx]] = AI;
  }

最后,遍历函数原型的所有参数,为这些LLVM Argument对象逐一设置参数名,并将这些参数注册倒NamedValues映射表内,以备AST节点类VariableExprAST稍后使用。完事之后,将Function对象返回。注意,此处并不检查参数名冲突与否(说的是“extern foo(a b a”这样的情况)。按照之前的讲解,要加上这一重检查易如反掌。

Function *FunctionAST::Codegen() {
  NamedValues.clear();
 
  Function *TheFunction = Proto->Codegen();
  if (TheFunction == 0)
    return 0;

下面是函数定义的代码生成过程,开场白很简单:生成函数原型(Proto)的代码并进行校验。与此同时,需要清空NamedValues映射表,确保其中不会残留之前代码生成过程中的产生的内容。函数原型的代码生成完毕后,一个现成的LLVM Function对象就到手了。

  // Create a new basic block to start insertion into.
  BasicBlock *BB = BasicBlock::Create(getGlobalContext(), "entry", TheFunction);
  Builder.SetInsertPoint(BB);
 
  if (Value *RetVal = Body->Codegen()) {

现在该开始设置Builder对象了。第一行新建了一个名为“entry”的基本块对象,稍后该对象将被插入TheFunction。第二行告诉Builder,后续的新指令应该插至刚刚新建的基本块的末尾处。LLVM基本块是用于定义控制流图(Control Flow Graph)的重要部件。

  if (Value *RetVal = Body->Codegen()) {
    // Finish off the function.
    Builder.CreateRet(RetVal);
 
    // Validate the generated code, checking for consistency.
    verifyFunction(*TheFunction);
 
    return TheFunction;
  }

选好插入点后,调用函数主表达式的CodeGen()方法。不出意外的话,