LLVM 即时编译

2023-10-29 linux database c/cpp

当前大部分的一些三方编译器都会依赖于 LLVM 框架,除了经常使用的 clang 编译器之外,还可以作为一个框架使用。

简介

LLVM 狭义上来说是一个编译器(clang),实际上提供了一套框架以及基于框架的一些编译器实现,尤其是优化器相关内容,是当前很先进的一套编译系统,有如下特点:

  • 支持中间表达式,能够以可读文本方式打印,语法类似汇编,虽然简单,但是相比其它只有内存结构使用起来要方便很多。
  • 模块化设计好。
  • 源自于学术,同时受到 Apple 支持,且开源好用。

注意,不同 LLVM 版本的差异性比较大。

基本概念

Module

LLVM 中的模块 Module 代表了一块比较完整独立的代码,是一个最小的编译单元,包含了很多更小的基本构件,例如全局变量、函数、数据结构等。

#include <llvm/IR/Module.h>
#include <llvm/IR/LLVMContext.h>

using namespace llvm;

int main(int argc, char *argv[]) {
    LLVMContext context;
    Module *module = new Module("Hello", context);
    module->print(outs(), nullptr);
    return 0;
}

可以通过如下方式编译。

----- 如下两种方式相同,只是不同的 Bash 解析
$ clang++ -fuse-ld=lld -g `llvm-config --cxxflags --ldflags --system-libs --libs` \
    -L/opt/clang/lib -o module module.cpp
$ clang++ -fuse-ld=lld -g $(llvm-config --cxxflags --ldflags --system-libs --libs) \
    -L/opt/clang/lib -o module module.cpp
----- 也可以指定不同的链接方式
$ clang++ -fuse-ld=lld -g `llvm-config --cxxflags --ldflags --system-libs --libs core` \
    -L/opt/clang/lib -o module module.cpp

当通过 Bitcode 或者 Textual 加载 IR 时可以选择懒加载,也就是开始只加载函数和全局变量的声明,然后按需加载对应的定义,从而可以避免解析和加载不需要的代码,使用是通过 materialize 或者 materializeAll 加载。

IR

Intermediate Representation, IR 是 LLVM 中的中间语言表示,用作编译器前后端的分水岭,通常有三种表示方式:A) 内存中编译中间语言,通常不便于查看;B) 磁盘上的二进制中间语言,后缀为 .bc;C) 可读语言,通过文本表示,后缀为 .ll

使用如下代码进行测试。

// foobar.c
int foobar(int a, int b) {
    return a + b;
}

int a = 10;
int main(void) {
    int b = 20;
    return foobar(10, 20);
}

可以通过如下命令编译生成中间 IR 文本信息。

clang -emit-llvm -S foobar.c -o foobar.ll -O0

如下是生成的内容。

;----- 标签属性说明
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

;----- 变量声明以及对齐属性,也就是全局变量 a=10
@a = dso_local global i32 10, align 4

;----- 函数声明,通过 define 定义,包含返回值、函数名、参数等
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @foobar(i32 noundef %0, i32 noundef %1) #0 {
  ;----- %开头为虚拟寄存器,可视为临时变量,也被称为临时寄存器
  %3 = alloca i32, align 4       ; 分配类型为i32*的临时空间
  %4 = alloca i32, align 4
  store i32 %0, ptr %3, align 4  ; %0保存在%3
  store i32 %1, ptr %4, align 4
  %5 = load i32, ptr %3, align 4 ; 
  %6 = load i32, ptr %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, ptr %1, align 4
  store i32 20, ptr %2, align 4
  %3 = call i32 @foobar(i32 noundef 10, i32 noundef 20)
  ret i32 %3
}

指令

IR 使用类似汇编语言的三地址码的指令格式,通过 % 表示虚拟寄存器,可以指定名称或者使用数字表示的匿名寄存器,其支持一些常用的指令集。

在 IR 中比较重要的就是跳转,包括了:A) 无条件跳转,类似 br label %7 直接跳转到 %7 位置;B) 有条件跳转,类似 br i1 %10, label %11, label %22 指令,当 %10true 则跳转到 %11 否则 %22

Basic Block

其中 Basic Block 是一段串行执行的指令流,只有最后一条指令是跳转,通常除了第一个之外都包含标签 Label 信息。

Control Flow Graph, CFG 控制流图就是由 Basic Block 以及 Basic Block 之间跳转关系组成的一个图,更进一步的是 Data Flow Graph, DFG 数据流图,很多编译优化算法都是基于 DFG 的。

另外,IR 是基于 Static Single Assignment, SSA 静态单赋值形式实现,也就是每个变量只能被赋值一次且变量在赋值前定义,因为从编译器的角度来看,编译器并不关心变量而是以数据为中心的设计,变量每次写入都会生成一个新的数据版本,编译器的优化就是围绕数据版本展开的。

常用示例

IRBuilder

其作用是为了更方便的创建 IR 指令,不过并没有将所有与创建 IR 有关的 API 都集成进来,如下是一些常见示例。

#include <llvm/Support/TargetSelect.h>

#include <llvm/IR/Module.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/LLVMContext.h>

int main(int argc, char *argv[]) {
    llvm::InitializeNativeTarget();
    llvm::InitializeNativeTargetAsmPrinter();
    llvm::InitializeNativeTargetAsmParser();

    llvm::LLVMContext context;
    llvm::IRBuilder<> builder(context);
    llvm::Module *module = new llvm::Module("IRBuilder Example", context);

    // Add a function
    llvm::FunctionType *funct = llvm::FunctionType::get(builder.getInt32Ty(), false);
    llvm::Function *func = llvm::Function::Create(funct, llvm::GlobalValue::ExternalLinkage, "SimpleFunc", module);

    // Create a block
    llvm::BasicBlock *block = llvm::BasicBlock::Create(builder.getContext(), "entry", func);
    builder.SetInsertPoint(block);

    // Add a return
    llvm::ConstantInt *zero = builder.getInt32(0);
    builder.CreateRet(zero);

    module->print(llvm::outs(), nullptr);
    return 0;
}

这里简单创建一个 SimpleFunc 函数,会返回 0 值。如下是实现一个简单的乘法函数。

#include <llvm/Support/TargetSelect.h>

#include <llvm/IR/Module.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Verifier.h>

int main(int argc, char *argv[]) {
    llvm::InitializeNativeTarget();
    llvm::InitializeNativeTargetAsmPrinter();
    llvm::InitializeNativeTargetAsmParser();

    llvm::LLVMContext context;

    llvm::IRBuilder<> builder(context);
    llvm::Module *module = new llvm::Module("IRBuilder Example", context);

    // Add a global variable
    module->getOrInsertGlobal("simpleGlobalVar", builder.getInt32Ty());
    llvm::GlobalVariable *global = module->getNamedGlobal("simpleGlobalVar");
    global->setLinkage(llvm::GlobalValue::CommonLinkage);
    global->setAlignment(llvm::MaybeAlign(4));

    // Add a function
    std::vector<llvm::Type *> parameters(2, builder.getInt32Ty());
    llvm::FunctionType *funct = llvm::FunctionType::get(builder.getInt32Ty(), parameters, false);
    llvm::Function *func = llvm::Function::Create(funct, llvm::GlobalValue::ExternalLinkage, "SimpleMul", module);
    // Set arguments for the function
    func->getArg(0)->setName("a");
    func->getArg(1)->setName("b");

    // Create a block
    llvm::BasicBlock *block = llvm::BasicBlock::Create(builder.getContext(), "entry", func);
    builder.SetInsertPoint(block);

    // Create an arithmetic statement
    llvm::Value *arga = func->getArg(0);
    llvm::Value *argb = func->getArg(1);
    llvm::Value *result = builder.CreateMul(arga, argb, "multiplyResult");

    // Add a return
    builder.CreateRet(result);

    llvm::verifyFunction(*func);
    module->print(llvm::outs(), nullptr);

    return 0;
}

参考