2312llvm,06即时编译

即时编译器

LLVM即时(即时)编译器是基于函数动态翻译引擎.

术语来自即时制造,即工厂按需制造或购买物资,而不放入仓库.在编译过程中,该比喻也很合适,因为即时编译器不会存储二进制程序到磁盘,而是在需要时才开始编译.

即时策略的优势,在于知道运行程序的精确的机器和微架构.这让即时系统可对特定处理器微调代码.
而且,有的编译器只有在运行时才知道其输入,因而只能实现即时系统.

如,GPU驱动即时编译着色语言,互联网浏览器处理js等.

了解LLVM即时引擎基础

LLVM即时编译器是基于函数的,因为它一次可编译单个函数.这定义了编译器的工作粒度,对即时系统来说这是个重要决定.

通过按需编译函数,编译器只会处理当前程序调用中实际用到的函数.如,你的程序有多个函数,但启动时设置了错误的命令行参数,基于函数的即时系统只会编译那个打印帮助消息的函数,而不是该程序.

即时引擎,在运行时编译且执行LLVMIR函数.在编译阶段,即时引擎会用LLVM生成目标相关二进制指令组成的二进制数据块.

它返回编译后的可执行的函数指针.

一篇有趣的博客文章对比了即时编译的开源方法.

LLVM作为静态编译器比即时系统更加有名,因为在即时编译过程中,每趟消耗的时间是很重要的,这都算程序执行成本.

GCC相似,LLVM基础架构更注重支持慢而强的优化,而不是对构建有竞争力的即时系统很重要的快而弱的优化.

即时系统不值得浪费大量时间去优化仅执行几次程序片段.

介绍执行引擎

LLVM即时系统有个支持执行LLVM模块的执行引擎.在<llvm_source>/include/llvm/ExecutionEngine/ExecutionEngine.h中定义ExecutionEngine类.
设计它通过即时系统或解释器执行.一般,一个执行引擎负责管理执行用户程序,分析要运行的程序片段,采取合理动作来执行它.

即时编译,必须要有执行管理器来协调编译策略,(一次一个片段)运行用户程序.就LLVMExecutionEngine类而言,它把执行部分抛回给用户.

可运行编译管线,产生内存中的代码,但由你决定是否执行此代码.

除了有待执行LLVM模块,引擎支持下面几个场景:
1,懒(lazy)编译:调用函数时,引擎才编译它.关闭懒编译后,一旦请求函数指针,引擎就编译它们.
2,编译外部全局变量:包括解析当前LLVM模块的外部实体符号并分配内存.
3,与运行时动态加载共享对象(DSO)一样,通过dlsym查找和解析外部符号.

LLVM实现了两个执行引擎:llvm::JIT类和llvm::MCJIT类.ExecutionEngine::EngineBuilder()方法根据IR模块参数,实例化一个ExecutionEngine对象.
接着,ExecutionEngine::create()方法创建一个即时MCJIT实例,两者实现截然不同.

注意,解释器实现了一个非传统策略来执行硬件平台(主机平台)不原生地支持的用户代码.如,LLVMIRx86平台上的用户代码,因为x86处理器不能直接执行LLVMIR.

不同于即时编译器,解释器任务是读取每条指令,解码并执行它们,在软件中,模仿了物理处理器.

尽管解释器省去了启动编译器,翻译用户代码时间,它们往往慢得多.

管理内存

一般,即时引擎在运行时由ExecutionManager类,写二进制数据块进内存.随后,就可跳转到分配内存区域(即调用ExecutionManager返回给你的函数指针),来执行这些指令了.

这里,对很多普通任务,如分配内存,释放内存,为加载库提供空间,和内存权限管理,管理内存很重要.

即时MCJIT类都实现了自定义管理内存类,从RTDyldMemoryManager基类继承而来.
ExecutionEngine用户也可提供自定义的RTDyldMemoryManager子类,来指定在哪放置不同的即时组件.

可在<llvm_source>/include/llvm/ExecutionEngine/RTDyldMemoryManager.h文件中找该接口.

如,RTDyldMemoryManager类声明了如下方法:
1,allocateCodeSection()allocateDataSection():这些方法分配内存,来保存给定大小和对齐的可执行代码和数据.
管理内存用户可通过内部标识追踪已分配的.
2,getSymbolAddress():返回当前链接的库中可获得的符号的地址.注意这不是用来取即时编译生成的符号.
调用时,必须提供一个std::string实例以保存符号的名字.

3,finalizeMemory():应该在加载完对象时调用,然后终于可设置内存权限了.如,不能在调用它前,运行生成代码.
它直接定向到MCJIT用户而不是即时用户.

尽管用户可提供自定义管理内存实现,JITMemoryManagerSectionMemoryManager分别是即时MCJIT默认子类.

llvm::即时基础结构

即时类和它的框架代表旧引擎,它是用LLVM生成代码的不同部分实现的.LLVM3.5之后,就要移除它.尽管该引擎大部分是目标无关的,每个目标必须为它的具体指令实现二进制指令输出.

把数据块写到内存

即时类通过MachineCodeEmitter类的JITCodeEmitter子类,输出二进制指令.MachineCodeEmitter类用来输出机器代码,它和新的机器代码(MachineCode,MC)框架没有关联,尽管旧,它依然存在并支持即时类的功能.

局限是只支持多个目标,且对支持目标,不支持所有目标特性.

MachineCodeEmitter类的方法,方便下列任务:

1,为当前要发射的函数分配空间
2,写二进制数据块内存缓冲(emitByte(),emitWordLE(),emitWordBE(),emitAlignment(),等)
3,追踪当前缓冲地址(就是下一条发射指令位置的指针)
4,添加与此缓冲内的指令地址相关联的重定向
由参与发射代码过程JITCodeEmitter类写字节到内存.由JITCodeEmitter子类,实现相关的即时功能和管理.

JITCodeEmitter是相当简单的,只是写字节到缓冲,而JITEmitter有下列改进:
1,特化的管理内存器,JITMemoryManager.
2,(JITResolver)解决者实例,跟踪和解决未编译函数的调用点.对懒编译函数至关重要.

使用JITMemoryManager

JITMemoryManager类(见<llvm_source>/include/llvm/ExecutionEngine/JITMemoryManager.h)实现了低级内存处理,并为前面提及的类提供缓冲.

除了来自RTDyldMemoryManager的方法,它提供具体如allocateGlobal()等方法来协助即时类,为单个全局变量分配内存;
startFunctionBody(),按读/写可执行标记分配内存时,创建调用即时来发射指令.

内部,JITMemoryManager类使用JITSlabAllocatorslab分配器(<llvm_source>/lib/ExecutionEngine/即时/JITMemoryManager.cpp)和MemoryBlock单元(<llvm_source>/include/llvm/Support/Memory.h).

发射目标代码

每个目标都实现一个叫<Target>CodeEmitter(见<llvm_source>/lib/Target/<Target>CodeEmitter.cpp)的机器函数,它按数据块编码指令,并用JITCodeEmitter写到内存.

如,MipsCodeEmitter遍历所有函数基本块,对每个(MI)机器指令,调用emitInstruction():

(...)
MCE.startFunction(MF);
for (MachineFunction::iterator MBB = MF.begin(), E = MF.end(); MBB != E; ++MBB) {
  MCE.StartMachineBasicBlock(MBB);
  for (MachineBasicBlock::instr_iterator I = MBB->instr_begin(), E = MBB->instr_end(); I != E;)
emitInstruction(*I++, *MBB);
}
(...)

MIPS32是固定4字节长度的ISA,因此emitInstruction()实现很简单.

void MipsCodeEmitter::emitInstruction(MachineBasicBlock::instr_iterator MI, MachineBasicBlock &MBB) {
  ...
  MCE.processDebugLoc(MI->getDebugLoc(), true);
  emitWord(getBinaryCodeForInstr(*MI));
  ++NumEmitted;  //跟踪`mi`发射的#
  ...
}

emitWord()方法是包装JITCodeEmitter,getBinaryCodeForInstr()TableGen通过解读.td文件中的指令编码描述,为每个目标生成的.

<Target>CodeEmitter类还必须实现自定义方法以编码操作数和其它目标相关的实体.

如,在MIPS中,内存操作数必须使用getMemEncoding()方法来正确编码(见<llvm_source>/lib/Target/Mips/MipsInstrInfo.td):

def mem : Operand<iPTR> {
  (...)
  let MIOperandInfo = (ops ptr_rc, simm16);
  let EncoderMethod = "getMemEncoding";
  (...)
}

因此,MipsCodeEmitter必须实现MipsCodeEmitter::getMemEncoding()方法以符合该TableGen描述.代码输出器和即时框架关系:

MipsCodeEmitter
MachineCodeEmitter
两个代码发射器

ARMCodeEmitter
JITCodeEmitter
JITEmitter

RTDyldMemoryManager
JITMemoryManager
...
//内存管理器.

目标信息

为了支持即时编译,每个目标还必须提供一个TargetJITInfo的子类(见include/llvm/Target/TargetJITInfo.h),如MipsJITInfoX86JITInfo.

TargetJITInfo类为通用即时功能提供了接口,需要每个目标实现它们.下面,看这些函数的一些示例:

1,为了让执行引擎重编译函数,或许因为修改了它,每个目标要实现TargetJITInfo::replaceMachineCodeForFunction()方法,用跳转指令调用新版本函数修补原先函数的位置.
自修改代码,这是必需的.

2,类似动态链接器,TargetJITInfo::relocate()方法修补当前发射函数中的每个符号引用至正确内存地址.
3,TargetJITInfo::emitFunctionStub()方法发射一个:在给定地址调用另一个函数的函数.每个目标还要用发射的桩的字节大小和对齐提供自定义的TargetJITInfo::StubLayout信息.
JITEmitter用此桩信息在发射它前,为新桩分配空间.

虽然TargetJITInfo方法的目的不是发射如生成函数体等普通指令,但是它们仍要为生成桩发射特定指令,并调用新的内存位置.

然而,当创建即时框架后,没有可依赖的接口来更易发射MachineBasicBlock之外的单独指令.这是今天MCInstsMCJIT做的事情.
没有MCInsts,旧的即时框架强制目标手工编码指令.

为了表示<Target>JITInfo实现,如何需要手工发射指令,看看MipsJITInfo::emitFunctionStub()的代码(见<llvm_source>/lib/Target/Mips/MipsJITInfo.cpp),它用以下代码生成4条指令:
...

// lui $t9, %hi(EmittedAddr)
// addiu $t9, $t9, %lo(EmittedAddr)
// jalr $t8, $t9
// nop
  if (IsLittleEndian) {
JCE.emitWordLE(0xf << 26 | 25 << 16 | Hi);
JCE.emitWordLE(9 << 26 | 25 << 21 | 25 << 16 | Lo);
JCE.emitWordLE(25 << 21 | 24 << 11 | 9);
JCE.emitWordLE(0);
...

学习如何使用即时

即时是一个在<llvm_source>/lib/ExecutionEngine/JIT/JIT.h中声明的ExecutionEngine子类.即时类是借助即时基础结构的编译函数入口.

ExecutionEngine::create()方法用默认JITMemoryManager,调用即时::createJIT().接着,即时构造器执行下面的任务:
1,创建JITEmitter实例
2,初化目标信息对象
3,为生成代码添加
4,添加最后运行的<Target>CodeEmitter

引擎保存了一个趟管理器对象,每当请求即时编译函数时,来调用生成代码及即时发射.

目的是取Sum函数,且用即时系统用运行时参数,计算两个不同加数.如下步骤:
1,首先,创建一个叫sum-jit.cpp的新文件.要包含即时执行引擎的资源:

#include "llvm/ExecutionEngine/JIT.h"

2,包含涉及读写LLVM位码,环境接口等其它的头文件,并导入LLVMnamespace:

#include "llvm/ADT/OwningPtr.h"
#include "llvm/Bitcode/ReaderWriter.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/system_error.h"
#include "llvm/Support/TargetSelect.h"
using namespace llvm;

3,InitializeNativeTarget()方法安装主机目标,确保可链接即时将用到的目标库.和往常一样,每个线程需要一个LLVMContext环境对象和一个MemoryBuffer对象,以从磁盘读取位码文件,如下:

int main() {
    InitializeNativeTarget();
    LLVMContext Context;
    std::string ErrorMessage;
    OwningPtr<MemoryBuffer> Buffer;

4,用getFile()方法从磁盘读文件,如下:

if (MemoryBuffer::getFile("./sum.bc", Buffer)) {
    errs() << "sum.bc not found\n";
    return -1;
}

5,ParseBitcodeFile函数从MemoryBuffer读取数据,并生成相应LLVMModule类:

Module *M = ParseBitcodeFile(Buffer.get(), Context, &ErrorMessage);
if (!M) {
    errs() << ErrorMessage << "\n";
    return -1;
}

6,调用EngineBuilder工厂创建一个ExecutionEngine实例,再调用其create方法,如下:

OwningPtr<ExecutionEngine> EE(EngineBuilder(M).create());

默认创建即时执行引擎,且是即时的设置点;它间接调用创建JITEmitter,JITManager即时构造器,并初化所有生成代码目标相关发射趟.

此刻,尽管引擎是LLVMModule相关,但还没有编译函数.
为了编译函数,还需要调用取本地即时编译的函数指针getPointerToFunction().如果未曾即时编译过该函数,就即时编译它并返回函数指针.流程:

getPointerToFunction()
JIT::runJITOnFunctionUnlocked()
MipsCodeEmitter::emitInstruction
JIT pending
JIT::jitTheFunction()
MipsCodeEmitter::runOnFunction
functions
PassManager::run()
CodeGen Passes

7,通过getFunction()方法,取表示sum函数的函数IR对象:

Function *SumFn = M->getFunction("sum");

这里,触发了即时编译:

int (*Sum)(int, int) = (int (*)(int, int)) EE->getPointerToFunction(SumFn);

你要适当转换类型到匹配该函数函数指针类型.Sum函数的LLVM定义原型是i32@sum(i32%a,i32%b),因此用int(*)(int,int)C原型.

另一个选项是考虑调用getPointerToFunctionOrStub()而不是getPointerToFunction()懒编译.如果目标函数还没有编译且开启懒编译,则生成一个桩函数,并返回它的指针.

桩是一个包含,稍后修改它就可跳转/调用实际函数的占位符简单函数.
8,接着,根据Sum所指向的即时编译了的函数,调用原始Sum函数,如下:

int res = Sum(4, 5);
outs() << "Sum result: " << res << "\n";

懒编译时,Sum调用桩函数(a),它(a)会用一个编译回调函数来即时编译实际函数.然后修改重定向实际函数,并执行它.

除非改变了原始的Module中的Sum函数,不会再次编译该函数.

9,再次调用Sum计算下个结果,如下:

res = Sum(res, 6);
outs() << "Sum result: " << res << "\n";

懒编译中,因为在第一次调用Sum时已编译了原始函数,第二次调用会直接执行原生函数.

10,成功用即时编译的Sum函数计算了两次加法.现在,释放执行引擎分配的保存函数代码的内存,调用llvm_shutdown()函数并返回:

    EE->freeMachineCodeForFunction(SumFn);
    llvm_shutdown();
    return 0;
}

要编译并链接sum-jit.cpp,可如下:

$ clang++ sum-jit.cpp -g -O3 -rdynamic -fno-rtti $(llvm-config --cppflags --ldflags --libs jit native irreader) -o sum-jit

或,用前面的Makefile,添加-rdynamic选项,并更改llvm-config调用以使用前面命令行指定的库.
尽管该示例不用外部函数,-rdynamic选项是重要的,它保证运行时解析外部函数.

运行该示例并查看输出:

$ ./sum-jit
Sum result: 9
Sum result: 15

通用值

在前例中,为了用C风格函数去调用该函数,把返回的函数指针转换为恰当的原型.然而,当处理多个函数且带众多的签名和参数类型时,需要更灵活方法去执行它们.

执行引擎提供了另一个调用即时编译的函数的方法.不需要提前调用getPointerToFunction(),runFunction()方法编译并用GenericValue向量决定函数参数来运行函数.

<llvm_source>/include/llvm/ExecutionEngine/GenericValue.h中定义GenericValuestruct,它可保存任意常见类型.

修改前例,用runFunction()而不是getPointerToFunction()加转换类型.
首先,创建sum-jit-gv.cpp文件,并保存该新版本,在开头添加GenericValue头文件:

#include "llvm/ExecutionEngine/GenericValue.h"

sum-jit.cpp复制其余的内容,关注修改部分.在初化SumFn函数指针后,创建GenericValue向量类型的FnArgs,并用APInt接口(<llvm_source>/include/llvm/ADT/APInt.h)填充整数值.根据sum(i32%a,i32%b)函数原型,用两个32位长度的整数填充:

(...)
Function *SumFn = m->getFunction("sum");
std::vector<GenericValue> FnArgs(2);
FnArgs[0].IntVal = APInt(32, 4);
FnArgs[1].IntVal = APInt(32, 5);

函数变量参数向量调用runFunction().这样,会即时编译并执行函数.相应结果也是i32位可访问的GenericValue.

GenericValue Res = EE->runFunction(SumFn, FnArgs);
outs() << "Sum result: " << Res.IntVal << "\n";

重复相同的过程,执行第二个加法:

FnArgs[0].IntVal = Res.IntVal;
FnArgs[1].IntVal = APInt(32, 6);
Res = EE->runFunction(SumFn, FnArgs);
outs() << "Sum result: " << Res.IntVal << "\n";
(...)

llvm::MCJIT框架

MCJIT类是LLVM新的即时实现.MC提供了统一指令表达方式,且为汇编器,反汇编器,汇编打印器MCJIT所共享的一个框架.

MC库的第一个优势在,目标只需要指定一次指令的编码,因为所有子系统都会得到此信息.因此,编写LLVM后端时,如果实现了目标的发射目标代码功能,也就实现了即时功能.

将在LLVM3.5之后删除llvm::即时,并完全替换为llvm::MCJIT框架.那么,为何学习旧即时呢?虽然它们是不同实现,但是ExecutionEngine类是通用的,大部分概念是两者共有的.

MCJIT引擎

和旧即时引擎相同,调用ExecutionEngine::create()创建MCJIT引擎.它调用执行MCJIT构造器的MCJIT::createJIT().

<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.h文件中声明MCJIT类.在<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp文件中实现createJIT()方法和MCJIT构造器.

MCJIT构造器创建一个SectionMemoryManager实例,添加LLVM模块到它内部的OwningModuleContainer,模块容器,并初化目标信息.

模块的状态

MCJIT类为创建引擎时,为初始LLVM模块实例,插入的表示模块编译阶段指定状态.如下:
1,Added:模块集,还未编译但已添加到执行引擎了.允许模块其它模块暴露函数定义,直到必需时,才编译它们.

2,Loaded:模块,已即时编译,但未准备好执行.还没有重定向,还未授权内存页.愿意在内存中重映射即时编译函数的用户,用loaded状态模块,也许可避免重编译.

3,Finalized:模块包含已准备好执行的函数.在此状态下,因为已重定向,不能重映射函数了.

即时MCJIT的一个主要区别就在于模块状态.在MCJIT中,在请求符号地址(函数和全局变量)前,整个模块必须就绪(finalized).

MCJIT::finalizeObject()函数,把已添加模块转换为已加载模块,接着转为终止化.
首先,它调用generateCodeForModule()生成已加载模块.
接着,通过finalizeLoadedModules()方法,终止化所有模块.

不像旧即时,MCJIT::getPointerToFunction()函数要求在调用之前模块对象已就绪.因此,必须在使用前调用MCJIT::finalizeObject().

LLVM3.4添加的新方法取消了该限制,当使用MCJIT时,getFunctionAddress()淘汰了getPointerToFunction()方法.

在请求符号地址前,该新方法不调用finalizeObject(),就加载并终止化模块.

注意,在旧即时中,执行引擎单独即时编译及执行各个函数.在MCJIT中,在执行函数前必须即时编译整个模块(所有函数).

因为编译粒度变大了,不能再说它是基于函数的,而是基于模块翻译引擎.

理解MCJIT如何编译模块

在模块对象加载阶段生成代码,由在<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp文件中的MCJIT::generateCodeForModule()方法触发.执行下面的任务:

1,创建一个ObjectBuffer实例来保存模块对象.如果已加载(编译)模块对象,就用ObjectCache接口取,避免重编译.
2,假设之前没有缓存(cache),MCJIT::emitObject()就发射MC代码.结果是一个ObjectBufferStream对象(支持流的ObjectBuffer子类).

3,RuntimeDyld动态链接器加载生成的ObjectBuffer对象,并通过RuntimeDyld::loadObject()创建符号表(symboltable).并返回ObjectImage对象.
4,按已加载标记模块.

对象缓冲,缓存,图像

ObjectBuffer类,(<llvm_source>/include/llvm/ExecutionEngine/ObjectBuffer.h)实现了MemoryBuffer(<llvm_source>/include/llvm/Support/MemoryBuffer.h)的包装.

MCObjectStreamer子类用MemoryBuffer类发射指令和数据到内存.此外,ObjectCache类直接引用可从中取ObjectBufferMemoryBuffer实例.

ObjectBufferStream类是一个带额外的让读写内存缓冲容易的标准C++流符号(如,>><<)的ObjectBuffer子类.

ObjectImage对象(<llvm_source>/include/llvm/ExecutionEngine/ObjectImage.h)保持加载的模块,且可直接访问ObjectBufferObjectFile的引用.

目标相关的如ELF,COFF,和MachO目标文件类型特化ObjectFile对象.ObjectFile对象可从MemoryBuffer对象,直接取符号,重定向,和节.

动态链接

ObjectImage实例表示MCJIT加载的模块对象.如前,可通过目标无关ObjectFile接口,透明访问内存缓冲.因此,可处理符号,节,和重定向.

为了生成ObjectImage对象,MCJITRuntimeDyld类提供的动态链接特性.该类提供了访问这些特性的公共接口,而每个对象文件类型特化的RuntimeDyldImpl对象提供实际实现.

因此,从ObjectBuffer生成ObjectImage对象的RuntimeDyld::loadObject()方法,首先创建目标相关RuntimeDyldImpl对象,然后调用RuntimeDyldImpl::loadObject().

过程中,还创建了可通过ObjectImage对象取它的ObjectFile对象.

模块就绪过程中,用运行时RuntimeDyld动态链接器来解决重定向,并为模块对象注册异常处理帧.

回想起getFunctionAddress()getPointerToFunction()执行引擎方法,要求引擎知道符号(函数)地址.
为此,MCJIT还通过RuntimeDyld::getSymbolLoadAddress()方法,用RuntimeDyld任意符号地址.

管理内存器

LinkingMemoryManager类,是另一个RTDyldMemoryManager的子类,是MCJIT引擎所用的实际管理内存器.它聚集了一个SectionMemoryManager实例,并向它发送代理请求.

每当RuntimeDyld动态链接器,通过LinkingMemoryManager::getSymbolAddress()请求符号地址时,它有两个选择:如果在已编译模块中可取得符号,就从MCJIT取地址;
否则,从由SectionMemoryManager实例加载并映射外部库请求地址.
参考<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp中的LinkingMemoryManager::getSymbolAddress()了解详情.

SectionMemoryManager实例是个简单管理器.作为一个RTDyldMemoryManager的子类,SectionMemoryManager继承了它所有的查询库方法,但是通过直接处理低级MemoryBlock单元(<llvm_source>/include/llvm/Support/Memory.h),来分配代码和数据节.

发射MC代码

MCJIT调用MCJIT::emitObject()发射MC代码.如下:
1,创建一个PassManager对象.
2,添加一个目标布局趟,调用addPassesToEmitMC()以添加所有生成代码并发射MC代码.

3,用PassManager::run()方法,运行所有的.在一个ObjectBufferStream对象中存储结果代码.

4,添加已编译的对象ObjectCache实例并返回它.

MCJIT发射代码比旧即时更一致.不是给即时提供自定义的输出器和目标信息,MCJIT透明访问现有MC基础设施的所有信息.

终止化对象

最终,在MCJIT::finalizeLoadedModules()里让模块对象就绪:已解决重定向,把已加载模块移到已就绪模块组,并调用LinkingMemoryManager::finalizeMemory()以改变内存页权限.

对象就绪后,就可执行MCJIT编译的函数了.

使用MCJIT引擎

下面的sum-MCJIT.cpp源文件包含了用MCJIT而不是旧即时框架,即时编译Sum函数所必需的代码.
为了表明它与前面的即时示例相似,保留了旧代码,并用UseMCJIT布尔变量来决定使用旧即时还是MCJIT.

1,首先,如下包含MCJIT头文件:

#include "llvm/ExecutionEngine/MCJIT.h"

2,包含其它必需的头文件,并导入llvm名字空间:

#include "llvm/ADT/OwningPtr.h"
#include "llvm/Bitcode/ReaderWriter.h"
#include "llvm/ExecutionEngine/JIT.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/system_error.h"
#include "llvm/Support/FileSystem.h"
using namespace llvm;

3,设置UseMCJITtrue,以测试MCJIT.设置为false就用旧即时运行该示例,如下:

bool UseMCJIT = true;
int main() {
  InitializeNativeTarget();

4,MCJIT需要初化汇编解析器和打印器:

if (UseMCJIT) {
    InitializeNativeTargetAsmPrinter();
    InitializeNativeTargetAsmParser();
}
LLVMContext Context;
std::string ErrorMessage;
OwningPtr<MemoryBuffer> Buffer;
if (MemoryBuffer::getFile("./sum.bc", Buffer)) {
    errs() << "sum.bc not found\n";
    return -1;
}
Module *M = ParseBitcodeFile(Buffer.get(), Context, &ErrorMessage);
if (!M) {
    errs() << ErrorMessage << "\n";
    return -1;
}

5,创建执行引擎,调用SetUseMCJIT(true)方法,让引擎使用MCJIT,如下:

OwningPtr<ExecutionEngine> EE;
if (UseMCJIT)
    EE.reset(EngineBuilder(M).setUseMCJIT(true).create());
else
    EE.reset(EngineBuilder(M).create());

6,旧即时需要稍后取函数指针并析构分配的内存Function引用:

Function* SumFn = NULL;
if (!UseMCJIT)
    SumFn = cast<Function>(M->getFunction("sum"));

7,如前,MCJIT淘汰了getPointerToFunction(),在MCJIT中只能用getFunctionAddress().因此,对各个即时类型,要用正确方法:

int (*Sum)(int, int) = NULL;
if (UseMCJIT)
    Sum = (int (*)(int, int)) EE->getFunctionAddress(std::string("sum"));
else
    Sum = (int (*)(int, int)) EE->getPointerToFunction(SumFn);
int res = Sum(4, 5);
outs() << "Sum result: " << res << "\n";
res = Sum(res, 6);
outs() << "Sum result: " << res << "\n";

8,因为MCJIT一次编译整个模块,释放Sum函数的机器代码,仅在旧即时中才有意义:

if (!UseMCJIT)
    EE->freeMachineCodeForFunction(SumFn);
    llvm_shutdown();
    return 0;
}

9,用下面命令编译和链接sum-MCJIT.cpp:

$ clang++ sum-MCJIT.cpp -g -O3 -rdynamic -fno-rtti $(llvm-config --cppflags --ldflags --libs 即时 MCJIT native irreader) -o sum-MCJIT

或,修改前面的Makefile.运行该示例,并检查输出:

$ ./sum-MCJIT
Sum result: 9
Sum result: 15

LLVM即时编译工具

LLVM提供了一些即时引擎的工具.如llillvm-rtdyld.

使用lli工具

LLVM执行引擎,(lli)解释工具实现了一个LLVM位码解释器和即时编译器.考虑如下sum-main.c源文件:

#include <stdio.h>
int sum(int a, int b) {
    return a + b;
}
int main() {
    printf("sum: %d\n", sum(2, 3) + sum(3, 4));
    return 0;
}

只要有main函数,lli工具可运行位码文件.用clang生成sum-main.bc位码文件:

$ clang -emit-llvm -c sum-main.c -o sum-main.bc

现在,通过lli用旧即时编译引擎运行位码:

$ lli sum-main.bc
sum: 12

或,用MCJIT引擎:

$ lli -use-MCJIT sum-main.bc
sum: 12

也有应用更慢的解释器的标志:

$ lli -force-interpreter sum-main.bc
sum: 12

使用llvm-rtdyld工具

llvm-rtdyld工具()是个非常简单的测试MCJIT对象加载和链接框架的工具.它可从磁盘读取二进制目标文件,并通过命令行,执行指定函数.

它不即时编译和执行,但允许测试和运行目标文件.
考虑下面三个C源码文件:main.c,add.c,和sub.c:

main.c
int add(int a, int b);
int sub(int a, int b);
int main() {
  return sub(add(3, 4), 2);
}
add.c
int add(int a, int b) {
  return a+b;
}
sub.c
int sub(int a, int b) {
  return a-b;
}

目标文件编译它们:

$ clang -c main.c -o main.o
$ clang -c add.c -o add.o
$ clang -c sub.c -o sub.o

llvm-rtdyld工具,以-entry-execute选项执行main函数:

$ llvm-rtdyld -execute -entry=_main main.o add.o sub.o; echo $  loaded '_main' at: 0x104d98000
5

或用-printline,为编译了调试信息的函数打印行信息.如下:

$ clang -g -c add.c -o add.o
$ llvm-rtdyld -printline add.o
Function: _add, Size = 20
  Line info @ 0: add.c, line: 2
  Line info @ 10: add.c, line: 3
  Line info @ 20: add.c, line: 3

可见,llvm-rtdyld工具,从MCJIT框架抽象的对象.llvm-rtdyld工具读取一系列二进制目标文件ObjectBuffer对象,并通过RuntimeDyld::loadObject()生成ObjectImage实例.

加载完所有目标文件后,由RuntimeDyld::resolveRelocations()解决重定向.
接着,通过getSymbolAddress()取入口(entrypoint),并调用函数.

llvm-rtdyld工具用了自定义的管理内存的TrivialMemoryManager.这是个简单的RTDyldMemoryManager子类.
可帮助你理解MCJIT框架的基本概念.

其它资源

<llvm_source>/examples/HowToUseJIT<llvm_source>/examples/ParallelJIT包含了学习即时基础的简单源码示例.

教程介绍如何使用即时.
MCJIT设计和实现,见这里.