Java教程

三段式LLVM编译器

本文主要是介绍三段式LLVM编译器,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

三段式LLVM编译器

目录

概述

LLVM技术生态之编译器

一、传统编译器的设计

二、传统编译器模式的实现

三、LLVM的三段式实现

四、LLVM's Code Representation:LLVM IR

1、IR的表现形式

2、IR的格式文件类型

3、IR文件的编译处理流程

4、简单的IR布局

5、Llvm IR 编程

五、LLVM 与 GCC有什么区别

LLVM技术生态之JIT

一、JIT概述

二、为什么要使用JIT

三、JIT生成代码时的代码优化技术

1、语言无关的优化技术之一:公共子表达式消除

2、语言相关的优化技术之一:数组边界检查消除

3、最重要的优化技术之一:方法内联

4、最前沿的优化技术之一:逃逸分析

四、JIT运行的简单原理

四、Llvm JIT与C++ Template模板有什么不同

业界不同领域使用LLVM的方式

一、作为编译器使用

二、作为内存计算引擎使用

结论

参考资料

概述

本文主要从几个方面来讲解Llvm相关内容,Llvm是什么、传统编译器的设计、传统编译器的实现、Llvm的编译器如何实现的、LLVM IR是什么、JIT简单的实现原理、业界多领域是如何使用Llvm的等多个方面来描述LLVM。LLVM命名源自于底层虚拟机(Low Level Virtual Machine)的缩写。不是一个类似于VMware这种虚拟机项目,是类似于GCC一样的编译器框架。说到编译器框架就不得不提一提传统的编译器。

LLVM技术生态之编译器

一、 传统编译器的设计

 

 图1. 传统的三段式设计

二、 传统编译器采用三段式设计:

前端: 前端组件解析程序源代码,检查语法错误,生成一个基于语言特性的AST(Abstract Syntax Tree)表示输入代码。

优化器:优化器组件接收到前端生成的AST,进行优化处理。

后端:把优化器优化后的AST,翻译成机器能识别的语言。

二、传统编译器模式的实现

这种模式的优点在于当编译器决定支持多种语言或者多种目标设备的时候,如果编译器在优化器这里采用普通的代码表示时,前端可以使用任意的语言来进行编译,后端也可以使用任意的目标设备来汇编。如下图:

 

 

 图2. 传统编译器三段式实现

使用这种设计,使编译器支持一种新的语言,需要实现一个新的前端,但是优化器及后端,都可以复用,不用改变。实现支持新的语言,需要从最初的前端设计开始,支持N种设备和M种源代码语言,一共需要N*M种编译方式。

这种三段式设计的另一优点是编译器提供了一个非常宽泛的语法集,即对于开源编译器项目来说,会有更多的人参与,自然就提升了项目的质量。这是为什么一些开源的编译器通常更为流行。

最后一个优点是实现一个编译器前端,相对于优化器与后端是完全不同。将分离开来对于专注于设计前端来提升编译器的多用性(支持多种语言)来说相对容易点。

三、LLVM的三段式实现

在基于LLVM的编译器中,前端的作用是解析、验证和诊断代码错误,将解析后的代码翻译为LLVM IR(通常是这么做,通过生成AST然后将AST转为LLVM IR)。翻译后的IR代码经过一系列的优化过程与分析后,代码得到改善,并将其送到代码生成器去产生原生的机器码。过程如下图所示。这是非常直观的三段式设计的实现过程,但是这简单的描述当然是省去了一些细节的实现。

 

图3. LLVM三段式实现

四、LLVM's Code Representation:LLVM IR

1、IR的表现形式

LLVM中最重要的设计模块:LLVM IR(LLVM Intermediate Representation),在编译器中表示代码的一种形式。设计在编译器的优化模块中,作为主导中间层的分析与转换。经过特殊设计,包括支持轻量级的Runtime优化、过程函数的优化,整个程序的分析和代码完全重构和翻译等。其中最重要的,定义了清晰的语义。参考如下的.ll文件:

define i32 @add1(i32 %a, i32 %b) {

entry:

  %tmp1 = add i32 %a, %b

  ret i32 %tmp1

}

define i32 @add2(i32 %a, i32 %b) {

entry:

  %tmp1 = icmp eq i32 %a, 0

  br i1 %tmp1, label %done, label %recurse

recurse:

  %tmp2 = sub i32 %a, 1

  %tmp3 = add i32 %b, 1

  %tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)

  ret i32 %tmp4

done:

  ret i32 %b

}

上述这段代码对应的是下面这段C代码,提供两种方式返回一个整型变量:

 

unsigned int add1(unsigned int a, unsigned int b) {

  return a+b;

}

// Perhaps not the most efficient way to add two numbers.

unsigned int add2(unsigned int a, unsigned int b) {

  if (a == 0) return b;

  return add2(a-1, b+1);

}

从这个例子中可以看出,LLVM IR 是一种底层的类RISC虚拟指令集。正如真正的RISC指令集一样,提供了一系列线性的简单指令:加、减、比较以及分支结构。这些指令在三种地址形式中:即通过对一些输入的计算,得出的结果存在不同的寄存器中。LLVM IR提供了标签支持,通常看起来像是一种奇怪的汇编语言一样。

和大多数RISC指令集不同的是,LLVM使用一种简单的类型系统来标记强类型(i32表示32位整型,i32**表示指向32位整型的指针),而一些机器层面的细节都被抽象出去了。例如函数调用使用call作标记,而返回使用ret标记。此外还有个不同是LLVM IR不直接像汇编语言那样直接使用寄存器,使用无限的临时存储单元,使用%符号来标记这些临时存储单元。

2、IR的格式文件类型

形式实现一个阶乘,再在main函数中调用中这个阶乘:

// factorial.c

int factorial(int n) {

    if(n>=1) {

        return n * factorial(n-1);

    }

    return 1;

}

// main.cpp

extern "C" int factorial(int);

int main(int argc, char** argv) {

    return factorial(2) * 7 == 42;

}

注:这里的extern "C"是必要的,为了支持C++的函数重载和作用域的可见性的规则,编译器会对函数进行name mangling, 如果不加extern "C",下文中生成的main.ll文件中factorial的函数名会被mangling成类似_Z9factoriali的样子,链接器便找不到要链接的函数。

LLVM IR有两种等价的格式:

一种是.bc(Bitcode)文件

一种是.ll文件,.ll文件是Human-readable的格式。

可以使用下面的命令得到这两种格式的IR文件

$ clang -S -emit-llvm factorial.c # factorial.ll

$ clang -c -emit-llvm factorial.c # factorial.bc

当然LLVM也提供了将代码文本转为二进制文件格式的工具:llvm-as,将.ll文件转为.bc格式文件,llvm-dis将.bc文件转为.ll文件。

 

 

$ llvm-as factorial.ll # factorial.bc

$ llvm-dis factorial.bc # factorial.ll

 对于cpp文件,只需将clang命令换成clang++即可。

$ clang++ -S -emit-llvm main.cpp # main.ll

$ clang++ -c -emit-llvm main.cpp # main.bc

3、IR文件的编译处理流程

llvm编译代码pipeline, 利用不同高级语言对应的前端(这里C/C++的前端都是clang)将transform成LLVM IR,进行优化,链接后,传给不同target的后端transform成target-specific的二进制代码。IR是LLVM的power所在,看下面这条command:

llvm-link factorial.bc main.bc -o linked.bc # lined.bc

llvm-link将两个IR文件链接起来,factorial.bc是C转成的IR,main.bc是C++转成的IR,到了IR这个level,高级语言间的差异消失了!可以相互链接。

进一步可以将链接得到的IR,转成target相关的code。

llc --march=x86-64 linked.bc # linked.s

下图展示了完整的build过程

 

 

 4、简单的IR布局

 

 4.1 Target information

以linked.ll为例进行解析,文件的开头是

; ModuleID = 'linked.bc'

source_filename = "llvm-link"

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"

;后面的注释指明了module的标识,source_filename是表明这个module是从什么文件编译得到的(如果打开main.ll会发现这里的值是main.cpp),如果该modules是通过链接得到的,这里的值就会是llvm-link。

Target information的主要结构如下:

 

 

 4.2 函数定义的主要结构

看一下函数factorial的定义

; Function Attrs: noinline nounwind optnone ssp uwtable

define i32 @factorial(i32 %val) #0 {

  %2 = alloca i32, align 4

  %3 = alloca i32, align 4

  store i32 %0, i32* %3, align 4

  %4 = load i32, i32* %3, align 4

  %5 = icmp sge i32 %4, 1

  br i1 %5, label %6, label %12

; <label>:6:                                      

; preds = %1

  %7 = load i32, i32* %3, align 4

  %8 = load i32, i32* %3, align 4

  %9 = sub nsw i32 %8, 1

  %10 = call i32 @factorial(i32 %9)

  %11 = mul nsw i32 %7, %10

  store i32 %11, i32* %2, align 4

  br label %13

; <label>:12:                                    

; preds = %1

  store i32 1, i32* %2, align 4

  br label %13

; <label>:13:                                    

; preds = %12, %6

  %14 = load i32, i32* %2, align 4

  ret i32 %14

}

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

​!llvm.module.flags = !{!0, !1, !2}

!llvm.ident = !{!3}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 14]}

!1 = !{i32 1, !"wchar_size", i32 4}

!2 = !{i32 7, !"PIC Level", i32 2}

!3 = !{!"Apple LLVM version 10.0.1 (clang-1001.0.46.4)"}

前面已经提到,;表示单行注释的开始。define i32 @factorial(i32 %val) #0指出了该函数的attribute group,其中第一个i32是返回值类型,对应C语言中的int;%factorial是函数名;第二个i32是形参类型,%val是形参名。llvm中的标识符分为两种类型:全局的和局部的。全局的标识符包括函数名和全局变量,会加一个@前缀,局部的标识符会加一个%前缀。一般地,可用标识符对应的正则表达式为[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*。

#0指出了该函数的attribute group。在文件的下面,会找到类似这样的代码

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

因为attribute group可能很包含很多attribute且复用到多个函数,所以IR使用attribute group ID(即#0)的形式指明函数的attribute,这样既简洁又清晰。

在一对花括号里的就是函数体,函数体是由一系列basic blocks(BB)组成的,这些BB形成了函数的控制流图(Control Flow Graph, CFG)。每个BB都有一个label,label使得该BB有一个符号表的入口点,在函数factorial中,这些BB的label就是entry、if.then、if.end,BB总是以terminator instruction(e.g. ret、br、callbr)结尾的。

4.3. IR是一个强类型语言

看一下函数main的定义

; Function Attrs: noinline norecurse optnone ssp uwtable

define i32 @main(i32, i8**) #0 {

  %3 = alloca i32, align 4

  %4 = alloca i32, align 4

  %5 = alloca i8**, align 8

  store i32 0, i32* %3, align 4

  store i32 %0, i32* %4, align 4

  store i8** %1, i8*** %5, align 8

  %6 = call i32 @factorial(i32 2)

  %7 = mul nsw i32 %6, 7

  %8 = icmp eq i32 %7, 42

  %9 = zext i1 %8 to i32

  ret i32 %9

}

LLVM的IR是一个强类型语言,每一条指令都显式地指出了实参的类型,例如mul nsw i32 %6, 7表明要将两个i32的数值相乘,icmp eq i32 %7, 42, icmp eq 表明要将两个i32的数据类型进行相等比较。此外,还很容易推断出返回值的类型,比如i32的数相乘的返回值就是i32类型,比较两个数值的相等关系的返回值就是i1类型。

强类型不但使得IR很human readable,也使得在优化IR时不需要考虑隐式类型转换的影响。在main函数的结尾,zext i1 %8 to i32将%8从1位整数扩展成了32位的整数(即做了一个类型提升)。

如果把最后两行用以下代码替代

ret i32 %8

这段IR就变成illegal的,检查IR是否合法可以使用opt -verify <filename>命令

$ opt -verify linked.ll

opt: linked.ll:45:11: error: '%8' defined with type 'i1' but expected 'i32'

  ret i32 %8

 4.3 terminator instruction介绍

ret指令

语法

ret <type> <value>       ; Return a value from a non-void function

ret void                 ; Return from void function

概述

ret用来将控制流从callee返回给caller

Example

ret i32 5                       ; Return an integer value of 5

ret void                        ; Return from a void function

ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2

br指令

语法

br i1 <cond>, label <iftrue>, label <iffalse>

br label <dest>         

; Unconditional branch

​概述

br用来将控制流转交给当前函数中的另一个BB。

​Example

Test:

  %cond = icmp eq i32 %a, %b

  br i1 %cond, label %IfEqual, label %IfUnequal

IfEqual:

  ret i32 1

IfUnequal:

  ret i32 0

switch指令

​语法

switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]

​概述

switch根据一个整型变量的值,将控制流交给不同的BB。

Example

; Emulate a conditional br instruction

%Val = zext i1 %value to i32

switch i32 %Val, label %truedest [ i32 0, label %falsedest ]

; Emulate an unconditional br instruction

switch i32 0, label %dest [ ]

; Implement a jump table:

switch i32 %val, label %otherwise [ i32 0, label %onzero

                                    i32 1, label %onone

                                    i32 2, label %ontwo ]

 4.4 unreachable 介绍

语法

unreachable

​概述

unreachable告诉optimizer控制流时到不了这块代码,就是说这块代码是dead code。

​Example

在展示unreachable的用法的之前,先看一下undef的用法。undef表示一个未定义的值,只要是常量可以出现的位置,都可以使用undef。(此Example标题下的代码为伪代码)

​%A = or %X, undef

%B = and %X, undef

or指令和and指令分别是执行按位或和按位与的操作,由于undef的值是未定义的,因此编译器可以随意假设值来对代码进行优化,譬如说假设undef的值都是0

​%A = %X

%B = 0

可以假设undef的值是-1

​%A = -1

%B = %X

也可以假设undef的两处值是不同的,譬如第一处是0,第二处是-1

​%A = -1

%B = 0

为什么undef的值可以不同呢?这是因为undef对应的值是没有确定的生存期的,当需要一个undef的值的时候,编译器会从可用的寄存器中随意取一个值拿过来,因此并不能保证其值随时间变化具有一致性。下面可以看unreachable的例子了

​%A = sdiv undef, %X

%B = sdiv %X, undef

sdiv指令是用来进行整数/向量的除法运算的,编译器可以假设undef的值是0,因为一个数除以0是未定义行为,因此编译器可以认为其是dead code,将其优化成

​%A = 0

unreachable

4.5 静态单一赋值介绍

在IR中,每个变量都在使用前都必须先定义,且每个变量只能被赋值一次(如果套用C++的术语,就是说每个变量只能被初始化,不能被赋值),所以称IR是静态单一赋值的。举个例子的,假如想返回a*b+c的值,觉得可能可以这么写

%0 = mul i32 %a, %b

%0 = add i32 %0, %c

ret i32 %0

但是这里%0被赋值了两次,是不合法的,需要把修改成这样

%0 = mul i32 %a, %b

%1 = add i32 %0, %c

ret i32 %1

4.6 SSA

SSA可以简化编译器的优化过程,譬如说,考虑这段代码

d1: y := 1

d2: y := 2

d3: x := y

很容易可以看出第一次对y赋值是不必要的,在对x赋值时使用的y的值时第二次赋值的结果,但是编译器必须要经过一个定义可达性(Reaching definition)分析才能做出判断。编译器是怎么分析呢?首先先介绍几个概念:

变量x的定义是指一个会给x赋值或可能给x赋值的语句,譬如d1就是对y的一个定义

当一个变量x有新的定义后 ,旧的的定义会被新的定义kill掉,譬如d2就kill掉了d1。

一个定义d到达点p是指存在一条d到p路径,在这条路径上,d没有被kill掉

t1是t2的reaching definition是指存在一条t1到t2路径,沿着这条路径走就可以得到t1要赋值的变量的值,而不需要额外的信息。

按照上面的代码写法,编译器是很难判断d3的reaching definition的。因为d3的reaching definition可能是d1,也可能是d2,要搞清楚d1和d2谁kill了谁很麻烦。但是,如果代码是SSA的,则代码就会长成这样

d1: y1 := 1

d2: y2 := 2

d3: x := y2

编译发现x是由y2赋值得到,而y2被赋值了2,且x和y2都只能被赋值一次,显然得到x的值的路径就是唯一确定的,d2就是d3的reaching definition。

SSA带来的问题

假设想用IR写一个用循环实现的factorial函数

int factorial(int val) {

  int temp = 1;

  for (int i = 2; i &lt;= val; ++i)

  temp *= i;

  return temp;

}

5、Llvm IR 编程

使用LLVM IR编程要涉及到Module, Function, BasicBlock, Instruction, ExecutionEngine等概念。下面对这些概念进行一个简单的说明。

 

 

 可以将LLVM中的Module类比为C程序中的源文件。一个C源文件中包含函数和全局变量定义、外部函数和外部函数声明,一个Module中包含的内容也基本上如此,只不过C源文件中是源码来表示,Module中是用IR来表示。

Function是LLVM JIT操作的基本单位。Function被Module所包含。LLVM的Function包含函数名、函数的返回值和参数类型。Function内部则包含BasicBlock。

BasicBlock与编译技术中常见的基本块(basic block)的概念是一致的。BasicBlock必须以跳转指令结尾。

Instruction就是LLVM IR的最基本单位。Instruction被包含在BasicBlock中。

 

 

 ExecutionEngine是用来运行IR的。运行IR有两种方式:解释运行和JIT生成机器码运行。相应的ExecutionEngine就有两种:Interpreter和JIT。ExecutionEngine的类型可以在创建ExecutionEngine时指定。

LLVM IR编程基本流程

创建一个Module

在Module中添加Function

在Function中添加BasicBlock

在BasicBlock中添加指令

创建一个ExecutionEngine

使用ExecutionEngine来运行IR

LLVM IR编程示例与说明

创建Module

Module创建时需要一个context,通常使用global context。在例子中,Module的name被设置为test。

LLVMContext & context = llvm::getGlobalContext();

Module* module = new Module("test", context);

在Module中添加Function

在Module中添加Function的方法比较多,这里介绍一种比较简洁的方法。下面的代码生成了一个函数void foo(void)。

Constant* c = module->getOrInsertFunction("foo",

/*ret type*/                           Type::getVoidTy(context),

/*args*/                               Type::getVoidTy(context),

/*varargs terminated with null*/       NULL);

Function* foo = cast<Function>(c); /* cast is provided by LLVM

foo->setCallingConv(CallingConv::C);

到目前为止,还没有添加BasicBlock,函数foo仅仅是一个函数原型。第6行设置foo遵循C函数调用的规则。LLVM中的函数支持多种调用规则,通常使用C的调用规则即可。更多调用规则可以参考llvm::CallingConv::ID。

在Function中添加BasicBlock

创建BasicBlock可以使用BasicBlock类的静态函数Create。

BasicBlock* block = BasicBlock::Create(context, "entry", foo);

第三个参数foo表示将block插入到Function foo中。

在BasicBlock中添加指令

下面介绍一个在BasicBlock中添加指令的简洁方法。这个方法使用了一个工厂类IRBuilder的实例builder。

首先,初始化builder。

IRBuilder<> builder(block);

这里将block作为参数的指令,插入到block中。

接下来的一段代码开始向block中插入代码。含义包含在注释中。

//Create three constant integer x, y, z.

Value *x = ConstantInt::get(Type::getInt32Ty(context), 3);

Value *y = ConstantInt::get(Type::getInt32Ty(context), 2);

Value *z = ConstantInt::get(Type::getInt32Ty(context), 1);

//addr = &value

/* we will check the value of 'value' and see

** whether the function we construct is running correctly.

*/

long value = 10;

Value * addr = builder.CreateIntToPtr(

    ConstantInt::get(Type::getInt64Ty(context), (uint64_t)&value),

    Type::getInt64PtrTy(context),

    "addr"

);

// mem = [addr]

Value* mem = builder.CreateLoad(addr, "mem");

// tmp = 3*mem

Value* tmp = builder.CreateBinOp(Instruction::Mul,

                                 x, mem, "tmp");

// tmp2 = tmp+2

Value* tmp2 = builder.CreateBinOp(Instruction::Add,

                                  tmp, y, "tmp2");

// tmp3 = tmp2-1

Value* tmp3 = builder.CreateBinOp(Instruction::Sub,

                                  tmp2, z, "tmp3");

// [addr] = mem

builder.CreateStore(tmp3, addr);

// ret

builder.CreateRetVoid();

 

通过LLVM的IR生成一个Module test,这个Module中包含一个Function foo,而foo中包含一个BasicBlock entry。

展示已经生成的IR

可以使用Module的dump方法先展示目前的成果。

module->dump();

输出结果

; ModuleID = 'test'

define void @foo(void) {

entry:

  ; the number '140735314124408' maybe different on your machine.

  %mem = load i64* inttoptr (i64 140735314124408 to i64*)

  %tmp = mul i32 3, i64 %mem

  %tmp2 = add i32 %tmp, 2

  %tmp3 = sub i32 %tmp2, 1

; the number '140735314124408' maybe different on your machine.

  store i32 %tmp3, i64* inttoptr (i64 140735314124408 to i64*)

  ret void

}

创建ExecutionEngine

接下来就要使用ExecutionEngine来生成代码了。

创建一个JIT类型的ExecutionEngine,为了便于观察IR生成的机器码,设置为不优化。

InitializeNativeTarget();

    ExecutionEngine *ee = EngineBuilder(module).setEngineKind(EngineKind::JIT)

        .setOptLevel(CodeGenOpt::None).create();

生成机器指令。

JIT生成机器指令以Function为单位。

void * fooAddr = ee->getPointerToFunction(foo);

std::cout <<"address of function 'foo': " << std::hex << fooAddr << std::endl;

如果用gdb跟踪函数执行,待输出fooAddr后,用x/i命令,可查看foo对应的机器指令。

例如,X86_64机器上输出为:

0x7ffff7f6d010:  movabs $0x7fffffffe2b0,%rax

0x7ffff7f6d01a:  mov    $0x3,%ecx

0x7ffff7f6d01f:  mov    (%rax),%edx

0x7ffff7f6d021:  imul   %ecx,%edx

0x7ffff7f6d024:  add    $0x2,%edx

0x7ffff7f6d02a:  sub    $0x1,%edx

0x7ffff7f6d030:  mov    %edx,(%rax)

0x7ffff7f6d032:  retq

运行机器指令

使用类型转换将fooAddr转换成一个函数fooFunc,然后调用。

//Run the function

std::cout << std::dec << "Before calling foo: value = " << value <<  std::endl;

typedef  (*FuncType)(void);

FuncType fooFunc = (FuncType)fooAddr;

fooFunc();

std::cout << "After calling foo: value = " << value <<  std::endl;

使用value的值来检验foo构造的正确性。运行后的输出

Before calling foo: value = 10

After calling foo: value = 31

经过验算,foo的功能是正确的。

直接生成并运行机器指令

ExecutionEngine还提供一个接口runFunction直接JIT并运行机器指令。具体做法可以参考LLVM::ExecutionEngine::runFunction的文档。

五、LLVM 与 GCC有什么区别

有一种说法,gcc编译器的代码,很难复用到其它项目中。gcc和基于LLVM实现的编译器其实都是分为前端、优化器、后端等模块,为什么gcc就不能被复用呢?

这就是LLVM设计的精髓所在:完全模块化。就拿优化器来说,典型的优化类型(LLVM优化器中称为Pass)有代码重排(expression reassociation)、函数内联(inliner)、循环不变量外移( loop invariant code motion)等。在gcc的优化器中,这些优化类型是全部实现在一起形成一个整体,要么不用,要么都用;或者可以通过配置只使用其中一些优化类型。而LLVM的实现方式是,每个优化类型自己独立成为一个模块,而且每个模块之间尽可能的独立,这样就可以根据需要只选择需要的优化类型编译进入程序中而不是把整个优化器都编译进去。

LLVM实现的方法是用一个类来表示一个优化类型,所有优化类型都直接或者间接继承自一个叫做Pass的基类,并且大多数都是自己占用一个.cpp文件,并且位于一个匿名命名空间中,这样别的.cpp文件中的类便不能直接访问,只提通过一个函数获取到实例,这样pass之间就不会存在耦合,如下面代码所示:

namespace {

  class Hello : public FunctionPass {

  public:

    // Print out the names of functions in the LLVM IR being optimized.

    virtual bool runOnFunction(Function &F) {

      cerr << "Hello: " << F.getName() << "\n";

      return false;

    }

  };

}

​FunctionPass *createHelloPass() { return new Hello(); }

每个.cpp会被编译成一个目标文件.o文件,然后被打包进入一个静态链接库.a文件中。当第三方又需要使用到其中一些优化类型,只需要选择自己需要的。由于这些类型都是自己独立于.a的一个.o中,因此的只有真正被用到的.o会被链接进入目标程序,这就实现了“用多少取多少”的目标,不搞“搭售”。而第三方如果还有自己独特的优化要求,只要按照同样的方法实现一个优化即可。

打个比方,如果将优化器比作卖电脑的,那么gcc的优化器相当于卖笔记本,称为A;而LLVM的优化器相当于卖组装的台式机的,称为B。或许自己有了其它合适的部件,就差一颗强劲的CPU。去A店里要么不买,要么就买一个功能齐全的笔记本,A店不允许只买某台笔记本上的一颗芯片;而去B店里可以做到只买一颗芯片。

到这里,终于可以回答LLVM和gcc的区别了:

LLVM本身只是一堆库,提供的是一种机制(mechanism),一种可以将源代码编译的机制,但是本身不能编译任何代码。也就是说编译什么代码、怎么编译、怎么优化、怎么生成这些策略(strategy)是由用户自己定的。例如clang就使用LLVM提供的这些机制制定了编译C代码的策略,因此前文中说clang可以称之为驱动(driver)。还拿电脑做例子:一堆电脑零件本身并不能做任何事情,这么将组装起来让工作是使用者的事儿。

 

 

参考资料

A Brief History of Just-In-Time

IBM:深入浅出 JIT 编译器

<<Recursive functions of symbolic expressions and their computation by machine>>

基于LLVM的内存计算

llvm.org/docs

 

 

参考链接:

https://blog.csdn.net/Night_ZW/article/details/108338628

这篇关于三段式LLVM编译器的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!