本章中,您將學(xué)習(xí)到即時編譯(JIT)編譯器的應(yīng)用,以及LLVM JIT編譯器的工作原理。您將探索LLVM動態(tài)編譯器和解釋器,并學(xué)習(xí)如何實現(xiàn)自己的JIT編譯器工具。此外,您還將了解如何將JIT編譯器作為靜態(tài)編譯器的一部分使用,以及相關(guān)的挑戰(zhàn)。
本章將涵蓋以下主題:
到本章結(jié)束時,您將理解并知道如何開發(fā)JIT編譯器,無論是使用預(yù)配置的類還是適合您需求的定制版本。
技術(shù)要求 您可以在 https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter09 找到本章中使用的代碼。
LLVM的JIT實現(xiàn)和用例 到目前為止,我們只看了提前編譯(AOT)編譯器。這些編譯器編譯整個應(yīng)用程序。只有在編譯完成后,應(yīng)用程序才能運行。如果編譯在應(yīng)用程序運行時執(zhí)行,那么編譯器就是JIT編譯器。JIT編譯器有一些有趣的用例:
LLVM的靜態(tài)編譯模型與JIT模型并不像人們想象的那么遠(yuǎn)。LLVM靜態(tài)編譯器llc將LLVM IR編譯成機(jī)器代碼,并將結(jié)果保存為磁盤上的對象文件。如果對象文件不是存儲在磁盤上而是在內(nèi)存中,代碼可以直接執(zhí)行嗎?不直接,因為對全局函數(shù)和全局?jǐn)?shù)據(jù)的引用使用重定位而不是絕對地址。從概念上講,重定位描述了如何計算地址 - 例如,作為已知地址的偏移量。如果我們將重定位解析為地址,就像鏈接器和動態(tài)加載器所做的那樣,那么我們就可以執(zhí)行對象代碼。運行靜態(tài)編譯器將IR代碼編譯成內(nèi)存中的對象文件,在內(nèi)存中的對象文件上執(zhí)行鏈接步驟,并運行代碼,這給了我們一個JIT編譯器。LLVM核心庫中的JIT實現(xiàn)是基于這個想法的。
在LLVM的發(fā)展歷史中,有幾種JIT實現(xiàn),具有不同的功能集。最新的JIT API是按需編譯(ORC)引擎。如果你好奇這個縮寫,它是主要開發(fā)人員的意圖,繼可執(zhí)行和鏈接格式(ELF)和調(diào)試標(biāo)準(zhǔn)(DWARF)之后,再發(fā)明一個基于托爾金宇宙的縮寫。
ORC引擎建立在并擴(kuò)展了使用靜態(tài)編譯器和動態(tài)鏈接器對內(nèi)存中對象文件的想法。實現(xiàn)采用了分層方法。兩個基本層是編譯層和鏈接層。在這之上是一個支持按需編譯的層。可以在懶編譯層的頂部或底部堆疊一個轉(zhuǎn)換層,允許開發(fā)人員添加任意轉(zhuǎn)換或簡單地通知某些事件。此外,這種分層方法的優(yōu)點是JIT引擎可以根據(jù)不同的需求進(jìn)行定制。例如,高性能虛擬機(jī)可以選擇預(yù)先編譯所有內(nèi)容并不使用懶編譯層。另一方面,其他虛擬機(jī)會強(qiáng)調(diào)啟動時間和對用戶的響應(yīng)性,并通過懶編譯層的幫助實現(xiàn)這一點。
舊的MCJIT引擎仍然可用,其API源自一個更舊的、已經(jīng)被移除的JIT引擎。隨著時間的推移,這個API逐漸變得膨脹,并且缺乏ORC API的靈活性。目標(biāo)是移除這個實現(xiàn),因為ORC引擎現(xiàn)在提供了MCJIT引擎的所有功能,新的發(fā)展應(yīng)該使用ORC API。
在下一節(jié)中,我們將在深入實現(xiàn)JIT編譯器之前,看看lli(LLVM解釋器)和動態(tài)編譯器。
使用JIT編譯進(jìn)行直接執(zhí)行 直接運行LLVM IR是想到JIT編譯器時出現(xiàn)的第一個想法。lli工具(LLVM解釋器)和動態(tài)編譯器就是這樣做的。我們將在下一節(jié)中探索lli工具。
探索lli工具 讓我們用一個非常簡單的例子嘗試lli工具。下面的LLVM IR可以存儲為一個名為hello.ll的文件,它是C hello world應(yīng)用程序的等效項。該文件聲明了C庫中printf()函數(shù)的原型。hellostr常量包含要打印的消息。在main()函數(shù)內(nèi)部,生成了對printf()函數(shù)的調(diào)用,這個函數(shù)包含一個將被打印的hellostr消息。該應(yīng)用程序總是返回0。
完整的源代碼如下:
declare i32 @printf(ptr, ...)
@hellostr=private unnamed_addr constant [13 x i8] c"Hello world\0A\00"
define dso_local i32 @main(i32 %argc, ptr %argv) {
%res=call i32 (ptr, ...) @printf(ptr @hellostr)
ret i32 0
}
這個LLVM IR文件足夠通用,以至于對所有平臺都是有效的。我們可以使用lli工具直接執(zhí)行IR,命令如下:
$ lli hello.ll
Hello world
這里有趣的一點是printf()函數(shù)是如何找到的。IR代碼被編譯成機(jī)器代碼,并觸發(fā)了對printf符號的查找。由于IR中沒有找到該符號,因此搜索當(dāng)前進(jìn)程中的該符號。lli工具動態(tài)鏈接到C庫,在那里找到了該符號。
當(dāng)然,lli工具不會鏈接到您創(chuàng)建的庫。為了啟用使用這些函數(shù),lli工具支持加載共享庫和對象。以下C源代碼只是打印一個友好的消息:
#include <stdio.h>
void greetings() {
puts("Hi!");
}
存儲在greetings.c中,我們使用它來探索使用lli加載對象。以下命令將編譯此源代碼為共享庫。-fPIC選項指示clang生成位置無關(guān)代碼,這對于共享庫是必需的。此外,編譯器使用–shared創(chuàng)建了一個名為greetings.so的共享庫:
$ clang greetings.c -fPIC -shared -o greetings.so
我們還編譯文件為greetings.o對象文件:
$ clang greetings.c -c -o greetings.o
現(xiàn)在我們有g(shù)reetings.so共享庫和greetings.o對象文件的兩個文件,我們將它們加載到lli工具中。
我們還需要一個調(diào)用greetings()函數(shù)的LLVM IR文件。為此,創(chuàng)建一個包含單個對函數(shù)調(diào)用的主.ll文件:
declare void @greetings(...)
define dso_local i32 @main(i32 %argc, i8** %argv) {
call void (...) @greetings()
ret i32 0
}
請注意,在執(zhí)行時,以前的IR會崩潰,因為lli無法定位greetings符號:
$ lli main.ll
JIT session error: Symbols not found: [ _greetings ]
lli: Failed to materialize symbols: { (main, { _main }) }
greetings()函數(shù)定義在外部文件中,為了修復(fù)崩潰,我們必須告訴lli工具需要加載的附加文件。為了使用共享庫,您必須使用–load選項,該選項將共享庫的路徑作為參數(shù):
$ lli –load ./greetings.so main.ll
Hi!
重要的是要指定共享庫的路徑,如果包含共享庫的目錄不在動態(tài)加載器的搜索路徑中。如果省略,那么庫將找不到。
或者,我們可以指示lli使用–extra-object加載對象文件:
$ lli –extra-object greetings.o main.ll
Hi!
其他支持的選項是–extra-archive(加載歸檔)和–extra-module(加載另一個位碼文件)。這兩個選項都需要文件的路徑作為參數(shù)。
現(xiàn)在您知道如何使用lli工具直接執(zhí)行LLVM IR。在下一節(jié)中,我們將實現(xiàn)我們自己的JIT工具。
使用LLJIT實現(xiàn)我們自己的JIT編譯器 lli工具只不過是LLVM API的一個薄包裝。在第一段中,我們了解到ORC引擎使用分層方法。ExecutionSession類表示一個運行中的JIT程序。除了其他項目,此類持有信息,例如使用的JITDylib實例。JITDylib實例是一個符號表,將符號名稱映射到地址。例如,這些可以是LLVM IR文件中定義的符號,或者是加載的共享庫的符號。
對于執(zhí)行LLVM IR,我們不需要自己創(chuàng)建JIT堆棧,因為LLJIT類提供了這個功能。當(dāng)從舊的MCJIT實現(xiàn)遷移時,您也可以使用這個類,因為這個類基本上提供了相同的功能。
為了說明LLJIT實用程序的功能,我們將創(chuàng)建一個交互式計算器應(yīng)用程序,同時合并JIT功能。我們的JIT計算器的主要源代碼將從第2章的calc示例擴(kuò)展而來。
我們交互式JIT計算器的主要思想如下:
允許用戶輸入函數(shù)定義,例如def f(x)=x*2。 然后,用戶輸入的函數(shù)由LLJIT實用程序編譯成函數(shù) - 在這種情況下,是f。 允許用戶使用數(shù)值調(diào)用他們定義的函數(shù):f(3)。 使用提供的參數(shù)評估函數(shù),并將結(jié)果打印到控制臺:6。
在我們討論將JIT功能整合到計算器源代碼之前,有一點主要的區(qū)別需要指出,與原始計算器示例相比:
首先,我們之前只輸入并解析以with關(guān)鍵字開頭的函數(shù),而不是前面描述的def關(guān)鍵字。對于這一章,我們只接受以def開頭的函數(shù)定義,并將其表示為AST類中的一個特定節(jié)點,稱為DefDecl。DefDecl類知道它定義的參數(shù)及其名稱,函數(shù)名稱也存儲在此類中。 其次,我們還需要AST知道函數(shù)調(diào)用,以表示LLJIT實用程序消耗或JIT的函數(shù)。每當(dāng)用戶輸入函數(shù)名稱,后跟括號中的參數(shù)時,AST將這些識別為FuncCallFromDef節(jié)點。這個類本質(zhì)上知道與DefDecl類相同的信息。 由于添加了這兩個AST類,很明顯,語義分析、解析器和代碼生成類將相應(yīng)地適應(yīng)我們AST中的更改。另外要注意的是,添加了一個新的數(shù)據(jù)結(jié)構(gòu),稱為JITtedFunctions,所有這些類都了解這個數(shù)據(jù)結(jié)構(gòu)。這個數(shù)據(jù)結(jié)構(gòu)是一個映射,定義的函數(shù)名稱作為鍵,函數(shù)定義的參數(shù)數(shù)量作為值存儲在映射中。我們將在后面看到如何在我們的JIT計算器中使用這個數(shù)據(jù)結(jié)構(gòu)。
有關(guān)我們對calc示例所做的更改的更多詳細(xì)信息,包含calc的更改和本節(jié)JIT實現(xiàn)的完整源代碼可以在lljit源目錄中找到。
將LLJIT引擎整合到計算器中 首先,讓我們討論如何在交互式計算器中設(shè)置JIT引擎。所有與JIT引擎相關(guān)的實現(xiàn)都存在于Calc.cpp中,該文件有一個main()循環(huán)來執(zhí)行程序:
我們必須包含幾個頭文件,除了包含我們的代碼生成、語義分析器和解析器實現(xiàn)的頭文件。LLJIT.h頭文件定義了LLJIT類和ORC API的核心類。接下來,需要InitLLVM.h頭文件來進(jìn)行工具的基本初始化,TargetSelect.h頭文件用于初始化本地目標(biāo)。最后,我們還包括了<iostream> C++頭文件,以允許用戶輸入到我們的計算器應(yīng)用程序:
#include "CodeGen.h"
#include "Parser.h"
#include "Sema.h"
#include "llvm/ExecutionEngine/Orc/LLJIT.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/TargetSelect.h"
#include <iostream>
接下來,我們將llvm和llvm::orc命名空間添加到當(dāng)前范圍:
using namespace llvm;
using namespace llvm::orc;
我們的LLJIT實例的許多調(diào)用返回錯誤類型,Error。ExitOnError類允許我們在丟棄由LLJIT實例的調(diào)用返回的錯誤值時,將它們記錄到stderr并退出應(yīng)用程序。我們?nèi)缦侣暶饕粋€全局ExitOnError變量:
ExitOnError ExitOnErr;
然后,我們添加main()函數(shù),該函數(shù)初始化工具和本地目標(biāo):
int main(int argc, const char **argv) {
InitLLVM X(argc, argv);
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
我們使用LLJITBuilder類創(chuàng)建一個LLJIT實例,如果發(fā)生錯誤,則用之前聲明的ExitOnErr變量包裝。可能的錯誤來源是平臺尚未支持JIT編譯:
auto JIT=ExitOnErr(LLJITBuilder().create());
接下來,我們聲明我們的JITtedFunctions映射,該映射跟蹤函數(shù)定義,如我們之前所述:
StringMap<size_t> JITtedFunctions;
為了便于創(chuàng)建一個等待用戶輸入的環(huán)境,我們添加一個while()循環(huán),并允許用戶輸入一個表達(dá)式,將用戶輸入的行保存在名為calcExp的字符串中:
while (true) {
outs() << "JIT calc > ";
std::string calcExp;
std::getline(std::cin, calcExp);
之后,初始化LLVM上下文類和一個新的LLVM模塊。模塊的數(shù)據(jù)布局也相應(yīng)設(shè)置,我們還聲明了一個代碼生成器,它將用于為用戶在命令行上定義的函數(shù)生成IR:
std::unique_ptr<LLVMContext> Ctx=std::make_unique<LLVMContext>();
std::unique_ptr<Module> M=std::make_unique<Module>("JIT calc.expr", *Ctx);
M->setDataLayout(JIT->getDataLayout());
CodeGen CodeGenerator;
我們必須解釋用戶輸入的行,以確定用戶是定義了一個新函數(shù)還是調(diào)用了他們之前定義的函數(shù)。定義一個Lexer類,同時輸入用戶提供的輸入行。我們將看到,有兩種主要情況是lexer關(guān)心的:
Lexer Lex(calcExp);
Token::TokenKind CalcTok=Lex.peek();
lexer可以檢查用戶輸入的第一個標(biāo)記。如果用戶正在定義一個新函數(shù)(由def關(guān)鍵字表示,或Token::KW_def標(biāo)記),那么我們解析它并檢查其語義。如果解析器或語義分析器檢測到用戶定義的函數(shù)有任何問題,將相應(yīng)地發(fā)出錯誤,計算器程序?qū)⑼V埂H绻馕銎骰蛘Z義分析器沒有檢測到任何錯誤,這意味著我們有一個有效的AST數(shù)據(jù)結(jié)構(gòu),DefDecl:
if (CalcTok==Token::KW_def) {
Parser Parser(Lex);
AST *Tree=Parser.parse();
if (!Tree || Parser.hasError()) {
llvm::errs() << "Syntax errors occured\n";
return 1;
}
Sema Semantic;
if (Semantic.semantic(Tree, JITtedFunctions)) {
llvm::errs() << "Semantic errors occured\n";
return 1;
}
CodeGenerator.compileToIR(Tree, M.get(), JITtedFunctions);
ExitOnErr(
JIT->addIRModule(ThreadSafeModule(std::move(M), std::move(Ctx))));
相反,如果用戶使用參數(shù)調(diào)用函數(shù),這由Token::ident標(biāo)記表示,我們還需要在將輸入轉(zhuǎn)換為有效的AST之前解析并語義檢查用戶輸入是否有效。這里的解析和檢查與之前略有不同,因為它可以包括檢查,例如確保用戶提供給函數(shù)調(diào)用的參數(shù)數(shù)量與函數(shù)最初定義的參數(shù)數(shù)量匹配:
} else if (CalcTok==Token::ident) {
outs() << "Attempting to evaluate expression:\n";
Parser Parser(Lex);
AST *Tree=Parser.parse();
if (!Tree || Parser.hasError()) {
llvm::errs() << "Syntax errors occured\n";
return 1;
}
Sema Semantic;
if (Semantic.semantic(Tree, JITtedFunctions)) {
llvm::errs() << "Semantic errors occured\n";
return 1;
}
llvm::StringRef FuncCallName=Tree->getFnName();
CodeGenerator.prepareCalculationCallFunc(Tree, M.get(), FuncCallName, JITtedFunctions);
auto RT=JIT->getMainJITDylib().createResourceTracker();
auto TSM=ThreadSafeModule(std::move(M), std::move(Ctx));
ExitOnErr(JIT->addIRModule(RT, std::move(TSM)));
auto CalcExprCall=ExitOnErr(JIT->lookup("calc_expr_func"));
int (*UserFnCall)()=CalcExprCall.toPtr<int (*)()>();
outs() << "User defined function evaluated to: " << UserFnCall() << "\n";
ExitOnErr(RT->remove());
在完成函數(shù)調(diào)用后,先前與我們的函數(shù)關(guān)聯(lián)的內(nèi)存然后由ResourceTracker釋放。
支持JIT編譯的代碼生成更改 現(xiàn)在,讓我們簡要看一下我們在CodeGen.cpp中所做的一些更改,以支持我們的基于JIT的計算器:
如前所述,代碼生成類有兩個重要的方法:一個將用戶定義的函數(shù)編譯成LLVM IR并打印IR到控制臺,另一個準(zhǔn)備計算評估函數(shù)calc_expr_func,其中包含對原始用戶定義函數(shù)的調(diào)用以進(jìn)行評估。這個第二個函數(shù)還將生成的IR打印給用戶:
void CodeGen::compileToIR(AST *Tree, Module *M,
StringMap<size_t> &JITtedFunctions) {
ToIRVisitor ToIR(M, JITtedFunctions);
ToIR.run(Tree);
M->print(outs(), nullptr);
}
void CodeGen::prepareCalculationCallFunc(AST *FuncCall,
Module *M, llvm::StringRef FnName,
StringMap<size_t> &JITtedFunctions) {
ToIRVisitor ToIR(M, JITtedFunctions);
ToIR.genFuncEvaluationCall(FuncCall);
M->print(outs(), nullptr);
}
如上源代碼所述,這些代碼生成函數(shù)定義了一個ToIRVisitor實例,該實例在初始化時接受我們的模塊和一個JITtedFunctions映射:
class ToIRVisitor : public ASTVisitor {
Module *M;
IRBuilder<> Builder;
StringMap<size_t> &JITtedFunctionsMap;
...
public:
ToIRVisitor(Module *M,
StringMap<size_t> &JITtedFunctions)
: M(M), Builder(M->getContext()),
JITtedFunctionsMap(JITtedFunctions) {
這些信息用于生成IR或評估先前生成IR的函數(shù)。在生成IR時,代碼生成器期望看到一個DefDecl節(jié)點,該節(jié)點表示定義了一個新函數(shù)。函數(shù)名稱以及它定義的參數(shù)數(shù)量存儲在函數(shù)定義映射中:
virtual void visit(DefDecl &Node) override {
llvm::StringRef FnName=Node.getFnName();
llvm::SmallVector<llvm::StringRef, 8> FunctionVars=Node.getVars();
(JITtedFunctionsMap)[FnName]=FunctionVars.size();
Function *DefFunc=genUserDefinedFunction(FnName);
...
在genUserDefinedFunction()中,第一步是檢查模塊中是否存在該函數(shù)。如果不存在,我們確保函數(shù)原型存在于我們的映射數(shù)據(jù)結(jié)構(gòu)中。然后,我們使用名稱和參數(shù)數(shù)量構(gòu)建一個函數(shù),該函數(shù)具有用戶定義的參數(shù)數(shù)量,并使函數(shù)返回單個整數(shù)值:
Function *genUserDefinedFunction(llvm::StringRef Name) {
if (Function *F=M->getFunction(Name))
return F;
Function *UserDefinedFunction=nullptr;
auto FnNameToArgCount=JITtedFunctionsMap.find(Name);
if (FnNameToArgCount !=JITtedFunctionsMap.end()) {
std::vector<Type *> IntArgs(FnNameToArgCount->second, Int32Ty);
FunctionType *FuncType=FunctionType::get(Int32Ty, IntArgs, false);
UserDefinedFunction=Function::Create(FuncType, GlobalValue::ExternalLinkage, Name, M);
}
return UserDefinedFunction;
}
生成用戶定義的函數(shù)后,創(chuàng)建一個新的基本塊,并將我們的函數(shù)插入到基本塊中。每個函數(shù)參數(shù)還與用戶定義的名稱相關(guān)聯(lián),因此我們還相應(yīng)地設(shè)置所有函數(shù)參數(shù)的名稱,并生成在函數(shù)內(nèi)對參數(shù)進(jìn)行操作的數(shù)學(xué)運算:
BasicBlock *BB=BasicBlock::Create(M->getContext(), "entry", DefFunc);
Builder.SetInsertPoint(BB);
unsigned FIdx=0;
for (auto &FArg : DefFunc->args()) {
nameMap[FunctionVars[FIdx]]=&FArg;
FArg.setName(FunctionVars[FIdx++]);
}
Node.getExpr()->accept(*this);
在評估用戶定義的函數(shù)時,我們示例中預(yù)期的AST稱為FuncCallFromDef節(jié)點。首先,我們定義評估函數(shù)并將其命名為calc_expr_func(不接受任何參數(shù)并返回一個結(jié)果):
virtual void visit(FuncCallFromDef &Node) override {
llvm::StringRef CalcExprFunName="calc_expr_func";
FunctionType *CalcExprFunTy=FunctionType::get(Int32Ty, {}, false);
Function *CalcExprFun=Function::Create(
CalcExprFunTy, GlobalValue::ExternalLinkage, CalcExprFunName, M);
...
接下來,我們創(chuàng)建一個新的基本塊將calc_expr_func插入到其中:
BasicBlock *BB=BasicBlock::Create(M->getContext(), "entry", CalcExprFun);
Builder.SetInsertPoint(BB);
與之前類似,通過genUserDefinedFunction()檢索用戶定義的函數(shù),并將要評估的原始函數(shù)的數(shù)值參數(shù)傳遞到我們剛剛重新生成的原始函數(shù)中:
llvm::StringRef CalleeFnName=Node.getFnName();
Function *CalleeFn=genUserDefinedFunction(CalleeFnName);
一旦我們有了實際的llvm::Function實例,我們就使用IRBuilder創(chuàng)建對定義函數(shù)的調(diào)用,并將結(jié)果返回,以便在最終將結(jié)果打印給用戶時可以訪問:
auto CalleeFnVars=Node.getArgs();
llvm::SmallVector<Value *> IntParams;
for (unsigned i=0, end=CalleeFnVars.size(); i !=end; ++i) {
int ArgsToIntType;
CalleeFnVars[i].getAsInteger(10, ArgsToIntType);
Value *IntParam=ConstantInt::get(Int32Ty, ArgsToIntType, true);
IntParams.push_back(IntParam);
}
Builder.CreateRet(Builder.CreateCall(CalleeFn, IntParams, "calc_expr_res"));
構(gòu)建基于LLJIT的計算器 最后,要編譯我們的JIT計算器源代碼,我們還需要創(chuàng)建一個CMakeLists.txt文件,其中包含構(gòu)建描述,該文件保存在Calc.cpp和我們的其他源文件旁邊:
我們設(shè)置所需的CMake最小版本號為LLVM所需的版本,并給項目命名:
cmake_minimum_required (VERSION 3.20.0)
project ("jit")
需要加載LLVM包,并將LLVM提供的CMake模塊目錄添加到搜索路徑中。然后,我們包含DetermineGCCCompatible和ChooseMSVCCRT模塊,這些模塊檢查編譯器是否具有GCC兼容的命令行語法,并確保使用與LLVM相同的C運行時:
find_package(LLVM REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
include(DetermineGCCCompatible)
include(ChooseMSVCCRT)
我們還需要添加來自LLVM的定義和包含路徑。使用函數(shù)調(diào)用將使用的LLVM組件映射到庫名稱:
add_definitions(${LLVM_DEFINITIONS})
include_directories(SYSTEM ${LLVM_INCLUDE_DIRS})
llvm_map_components_to_libnames(llvm_libs Core OrcJIT
Support native)
之后,如果確定編譯器具有GCC兼容的命令行語法,我們還檢查運行時類型信息和異常處理是否已啟用。如果它們沒有啟用,我們將向我們的編譯中添加C++標(biāo)志以相應(yīng)地關(guān)閉這些功能:
if(LLVM_COMPILER_IS_GCC_COMPATIBLE)
if(NOT LLVM_ENABLE_RTTI)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
endif()
if(NOT LLVM_ENABLE_EH)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
endif()
endif()
最后,我們定義可執(zhí)行文件的名稱、要編譯的源文件以及要鏈接的庫:
add_executable (calc
Calc.cpp CodeGen.cpp Lexer.cpp Parser.cpp Sema.cpp)
target_link_libraries(calc PRIVATE ${llvm_libs})
上述步驟是創(chuàng)建我們的基于JIT的交互式計算器工具所需的全部。接下來,創(chuàng)建并進(jìn)入構(gòu)建目錄,然后運行以下命令創(chuàng)建并編譯應(yīng)用程序:
$ cmake –G Ninja <path to source directory>
$ ninja
這將編譯calc工具。然后,我們可以啟動計算器,開始定義函數(shù),并看到我們的計算器如何能夠評估我們定義的函數(shù)。
以下示例調(diào)用顯示了首先定義的函數(shù)的IR,然后是創(chuàng)建calc_expr_func函數(shù)以生成對我們最初定義的函數(shù)的調(diào)用,以便用傳入它的任何參數(shù)評估函數(shù):
$ ./calc
JIT calc > def f(x)=x*2
define i32 @f(i32 %x) {
entry:
%0=mul nsw i32 %x, 2
ret i32 %0
}
JIT calc > f(20)
Attempting to evaluate expression:
define i32 @calc_expr_func() {
entry:
%calc_expr_res=call i32 @f(i32 20)
ret i32 %calc_expr_res
}
declare i32 @f(i32)
User defined function evaluated to: 40
JIT calc > def g(x,y)=x*y+100
define i32 @g(i32 %x, i32 %y) {
entry:
%0=mul nsw i32 %x, %y
%1=add nsw i32 %0, 100
ret i32 %1
}
JIT calc > g(8,9)
Attempting to evaluate expression:
define i32 @calc_expr_func() {
entry:
%calc_expr_res=call i32 @g(i32 8, i32 9)
ret i32 %calc_expr_res
}
declare i32 @g(i32, i32)
User defined function evaluated to: 172
就是這樣!我們剛剛創(chuàng)建了一個基于JIT的計算器應(yīng)用程序!
由于我們的JIT計算器旨在成為描述如何將LLJIT納入我們項目中的簡單示例,值得注意的是,存在一些限制:
對于第二個限制,這是設(shè)計使然,因此是預(yù)期的,并且由ORC API本身強(qiáng)制執(zhí)行:
$ ./calc
JIT calc > def f(x)=x*2
define i32 @f(i32 %x) {
entry:
%0=mul nsw i32 %x, 2
ret i32 %0
}
JIT calc > def f(x,y)=x+y
define i32 @f(i32 %x, i32 %y) {
entry:
%0=add nsw i32 %x, %y
ret i32 %0
}
Duplicate definition of symbol '_f'
請記住,除了為當(dāng)前進(jìn)程或共享庫公開符號之外,還有許多其他公開名稱的可能性。例如,StaticLibraryDefinitionGenerator類公開在靜態(tài)存檔中找到的符號,并且可以在DynamicLibrarySearchGenerator類中使用。
此外,LLJIT類還有一個addObjectFile()方法來公開對象文件的符號。如果現(xiàn)有的實現(xiàn)不適合您的需求,您還可以提供自己的DefinitionGenerator實現(xiàn)。
正如我們所看到的,使用預(yù)定義的LLJIT類很方便,但它可能限制了我們的靈活性。在下一節(jié)中,我們將探討如何使用ORC API提供的層實現(xiàn)JIT編譯器。
從頭開始構(gòu)建JIT編譯器類 使用ORC的分層方法,可以很容易地構(gòu)建一個針對需求定制的JIT編譯器。沒有一種JIT編譯器適合所有情況,本章的第一節(jié)給出了一些示例。讓我們來看看如何從頭開始設(shè)置JIT編譯器。
ORC API使用堆疊在一起的層。最低層是對象鏈接層,由llvm::orc::RTDyldObjectLinkingLayer類表示。它負(fù)責(zé)鏈接內(nèi)存中的對象并將其轉(zhuǎn)換為可執(zhí)行代碼。為此任務(wù)所需的內(nèi)存由MemoryManager接口的實例管理。有一個默認(rèn)實現(xiàn),但如果我們需要,也可以使用自定義版本。
在對象鏈接層之上是編譯層,負(fù)責(zé)創(chuàng)建內(nèi)存中的對象文件。llvm::orc::IRCompileLayer類以IR模塊為輸入并將其編譯為對象文件。IRCompileLayer類是IRLayer類的子類,IRLayer是接受LLVM IR的層實現(xiàn)的通用類。
這兩個層已經(jīng)構(gòu)成了JIT編譯器的核心:它們將LLVM IR模塊作為輸入,將其編譯并鏈接在內(nèi)存中。為了添加額外的功能,我們可以在這兩個層之上合并更多層。
例如,CompileOnDemandLayer類將模塊分割,以便僅編譯請求的函數(shù)。這可以用于實現(xiàn)按需編譯。此外,CompileOnDemandLayer類也是IRLayer類的子類。以非常通用的方式,IRTransformLayer類,也是IRLayer類的子類,允許我們對模塊應(yīng)用轉(zhuǎn)換。
另一個重要的類是ExecutionSession類。這個類代表一個運行中的JIT程序。從本質(zhì)上講,這意味著該類管理JITDylib符號表,為符號提供查找功能,并跟蹤使用的資源管理器。
JIT編譯器的通用配方如下:
JIT編譯器的一般用法也非常簡單:
在下一個子節(jié)中,我們按照通用配方實現(xiàn)一個JIT編譯器類。
創(chuàng)建JIT編譯器類 為了保持JIT編譯器類的實現(xiàn)簡單,一切都放在JIT.h中,我們在可以創(chuàng)建的名為jit的源目錄中。然而,與使用LLJIT相比,類的初始化有點更復(fù)雜。由于需要處理可能的錯誤,我們需要一個工廠方法來預(yù)先創(chuàng)建一些對象,然后才能調(diào)用構(gòu)造函數(shù)。創(chuàng)建類的步驟如下:
我們首先使用JIT_H預(yù)處理器定義保護(hù)頭文件免受多重包含:
#ifndef JIT_H
#define JIT_H
首先,需要一些包含文件。它們中的大多數(shù)提供了與頭文件同名的類。Core.h頭文件提供了一些基本類,包括ExecutionSession類。此外,ExecutionUtils.h頭文件提供了DynamicLibrarySearchGenerator類來搜索庫中的符號。此外,CompileUtils.h頭文件提供了ConcurrentIRCompiler類:
#include "llvm/Analysis/AliasAnalysis.h"
#include "llvm/ExecutionEngine/JITSymbol.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/IRTransformLayer.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/Mangling.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/ExecutionEngine/Orc/TargetProcessControl.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Error.h"
聲明一個新類。我們的新類將被稱為JIT:
class JIT {
// ...
};
私有數(shù)據(jù)成員反映了ORC層和一些輔助類。ExecutionSession、ObjectLinkingLayer、CompileLayer、OptIRLayer和MainJITDylib實例分別代表正在運行的JIT程序、層和符號表,如上所述。此外,TargetProcessControl實例用于與JIT目標(biāo)進(jìn)程交互。這可以是相同的進(jìn)程、同一臺機(jī)器上的另一個進(jìn)程,或者是不同機(jī)器上的遠(yuǎn)程進(jìn)程,可能具有不同的架構(gòu)。DataLayout和MangleAndInterner類需要以正確的方式對符號名稱進(jìn)行mangling。此外,符號名稱被內(nèi)部化,這意味著所有相等的名稱具有相同的地址。這意味著要檢查兩個符號名稱是否相等,只需比較地址就足夠了,這是一個非常快速的操作:
std::unique_ptr<llvm::orc::TargetProcessControl> TPC;
std::unique_ptr<llvm::orc::ExecutionSession> ES;
llvm::DataLayout DL;
llvm::orc::MangleAndInterner Mangle;
std::unique_ptr<llvm::orc::RTDyldObjectLinkingLayer> ObjectLinkingLayer;
std::unique_ptr<llvm::orc::IRCompileLayer> CompileLayer;
std::unique_ptr<llvm::orc::IRTransformLayer> OptIRLayer;
llvm::orc::JITDylib &MainJITDylib;
初始化分為三個部分。在C++中,構(gòu)造函數(shù)不能返回錯誤。簡單且推薦的解決方案是創(chuàng)建一個靜態(tài)工廠方法,在構(gòu)造對象之前進(jìn)行錯誤處理。層的初始化更為復(fù)雜,因此我們也為它們引入了工廠方法。
在create()工廠方法中,我們首先創(chuàng)建一個SymbolStringPool實例,該實例用于實現(xiàn)字符串內(nèi)部化,并由幾個類共享。為了控制當(dāng)前進(jìn)程,我們創(chuàng)建了一個SelfTargetProcessControl實例。如果我們想針對一個不同的進(jìn)程,那么我們需要更改這個實例。
接下來,我們構(gòu)造一個JITTargetMachineBuilder實例,為了知道JIT進(jìn)程的目標(biāo)三元組。然后,我們向目標(biāo)機(jī)器構(gòu)建器查詢數(shù)據(jù)布局。這一步可能會失敗,如果構(gòu)建器無法根據(jù)提供的目標(biāo)三元組實例化目標(biāo)機(jī)器,例如,因為LLVM庫中沒有編譯對此目標(biāo)的支持:
public:
static llvm::Expected<std::unique_ptr<JIT>> create() {
auto SSP=std::make_shared<llvm::orc::SymbolStringPool>();
auto TPC=llvm::orc::SelfTargetProcessControl::Create(SSP);
if (!TPC)
return TPC.takeError();
llvm::orc::JITTargetMachineBuilder JTMB(
(*TPC)->getTargetTriple());
auto DL=JTMB.getDefaultDataLayoutForTarget();
if (!DL)
return DL.takeError();
// ...
};
此時,我們已經(jīng)處理了所有可能失敗的調(diào)用。現(xiàn)在我們可以初始化ExecutionSession實例。最后,調(diào)用JIT類的構(gòu)造函數(shù),將所有實例化的對象傳遞給它,并將結(jié)果返回給調(diào)用者:
return std::make_unique<JIT>(
std::move(*TPC), std::move(ES), std::move(*DL),
std::move(JTMB));
JIT類的構(gòu)造函數(shù)將傳遞的參數(shù)移動到私有數(shù)據(jù)成員。層對象是通過調(diào)用帶有create前綴的靜態(tài)工廠方法構(gòu)建的。此外,每個層工廠方法都需要對ExecutionSession實例的引用,這將層連接到正在運行的JIT會話。除了對象鏈接層(它在層棧的底部)之外,每層都需要對前一層的引用,說明了堆疊順序:
JIT(std::unique_ptr<llvm::orc::ExecutorProcessControl>
EPCtrl,
std::unique_ptr<llvm::orc::ExecutionSession>
ExeS,
llvm::DataLayout DataL,
llvm::orc::JITTargetMachineBuilder JTMB)
: EPC(std::move(EPCtrl)), ES(std::move(ExeS)),
DL(std::move(DataL)), Mangle(*ES, DL),
ObjectLinkingLayer(std::move(
createObjectLinkingLayer(*ES, JTMB))),
CompileLayer(std::move(createCompileLayer(
*ES, *ObjectLinkingLayer,
std::move(JTMB)))),
OptIRLayer(std::move(
createOptIRLayer(*ES, *CompileLayer))),
MainJITDylib(
ES->createBareJITDylib("<main>")) {
// ...
}
在構(gòu)造函數(shù)主體中,我們向MainJITDylib添加一個生成器,以搜索當(dāng)前進(jìn)程中的符號。GetForCurrentProcess()方法很特殊,因為返回值被包裹在Expected<>模板中,表明也可以返回Error對象。然而,由于我們知道當(dāng)前進(jìn)程最終會運行,所以我們使用cantFail()函數(shù)解開結(jié)果,如果發(fā)生錯誤,該函數(shù)將終止應(yīng)用程序:
MainJITDylib.addGenerator(llvm::cantFail(
llvm::orc::DynamicLibrarySearchGenerator::
GetForCurrentProcess(DL.getGlobalPrefix())));
要創(chuàng)建對象鏈接層,我們需要提供一個內(nèi)存管理器。這里,我們堅持使用默認(rèn)的SectionMemoryManager類,但如果需要,我們也可以提供不同的實現(xiàn):
為了創(chuàng)建一個對象鏈接層,我們需要提供一個內(nèi)存管理器。在這里,我們堅持使用默認(rèn)的SectionMemoryManager類,但如果需要,我們也可以提供一個不同的實現(xiàn):
static std::unique_ptr<
llvm::orc::RTDyldObjectLinkingLayer>
createObjectLinkingLayer(
llvm::orc::ExecutionSession &ES,
llvm::orc::JITTargetMachineBuilder &JTMB) {
auto GetMemoryManager=[]() {
return std::make_unique<
llvm::SectionMemoryManager>();
};
auto OLLayer=std::make_unique<
llvm::orc::RTDyldObjectLinkingLayer>(
ES, GetMemoryManager);
// 對于在Windows上使用的通用對象文件格式(COFF)對象文件格式,存在一些復(fù)雜性。
// 這種文件格式不允許將函數(shù)標(biāo)記為導(dǎo)出的。
// 這隨后導(dǎo)致在對象鏈接層內(nèi)的檢查中出現(xiàn)故障:存儲在符號中的標(biāo)記與IR中的標(biāo)記進(jìn)行比較,
// 由于缺少導(dǎo)出標(biāo)記而導(dǎo)致不匹配。
// 解決方案是僅為此文件格式覆蓋標(biāo)記。
// 這完成了對象層的構(gòu)建,并將對象返回給調(diào)用者:
if (JTMB.getTargetTriple().isOSBinFormatCOFF()) {
OLLayer
->setOverrideObjectFlagsWithResponsibilityFlags(
true);
OLLayer
->setAutoClaimResponsibilityForObjectSymbols(
true);
}
return OLLayer;
}
要初始化編譯層,需要一個IRCompiler實例。IRCompiler實例負(fù)責(zé)將IR模塊編譯成對象文件。如果我們的JIT編譯器不使用線程,那么我們可以使用SimpleCompiler類,該類使用給定的目標(biāo)機(jī)器編譯IR模塊。TargetMachine類不是線程安全的,因此SimpleCompiler類也不是。為了支持多線程編譯,我們使用ConcurrentIRCompiler類,它為要編譯的每個模塊創(chuàng)建一個新的TargetMachine實例。這種方法解決了多線程的問題:
static std::unique_ptr<llvm::orc::IRCompileLayer>
createCompileLayer(
llvm::orc::ExecutionSession &ES,
llvm::orc::RTDyldObjectLinkingLayer &OLLayer,
llvm::orc::JITTargetMachineBuilder JTMB) {
auto IRCompiler=std::make_unique<
llvm::orc::ConcurrentIRCompiler>(
std::move(JTMB));
auto IRCLayer=std::make_unique<llvm::orc::IRCompileLayer>(
ES, OLLayer, std::move(IRCompiler));
return IRCLayer;
}
我們不直接將IR模塊編譯成機(jī)器代碼,而是安裝一個首先優(yōu)化IR的層。這是一個深思熟慮的設(shè)計決策:我們將我們的JIT編譯器變成了一個優(yōu)化的JIT編譯器,它產(chǎn)生的代碼運行速度更快,但生成速度更長,意味著用戶的延遲。我們沒有添加延遲編譯,所以當(dāng)查找一個符號時,整個模塊都被編譯了。這可能會增加用戶看到代碼執(zhí)行之前的顯著時間。
注意
引入延遲編譯并不是所有情況下的合適解決方案。延遲編譯是通過將每個函數(shù)移動到自己的模塊中來實現(xiàn)的,當(dāng)查找函數(shù)名時進(jìn)行編譯。這阻止了像內(nèi)聯(lián)這樣的跨過程優(yōu)化,因為內(nèi)聯(lián)器通過需要訪問被調(diào)用函數(shù)的主體來進(jìn)行內(nèi)聯(lián)。結(jié)果,用戶在使用延遲編譯時看到更快的啟動,但產(chǎn)生的代碼并不像它可以的那樣最優(yōu)。這些設(shè)計決策取決于預(yù)期的用途。在這里,我們決定使用快速代碼,接受較慢的啟動時間。此外,這意味著優(yōu)化層本質(zhì)上是一個轉(zhuǎn)換層。
IRTransformLayer類將轉(zhuǎn)換委托給一個函數(shù)——在我們的情況下,是optimizeModule函數(shù):
static std::unique_ptr<llvm::orc::IRTransformLayer>
createOptIRLayer(
llvm::orc::ExecutionSession &ES,
llvm::orc::IRCompileLayer &CompileLayer) {
auto OptIRLayer=std::make_unique<llvm::orc::IRTransformLayer>(
ES, CompileLayer,
optimizeModule);
return OptIRLayer;
}
optimizeModule()函數(shù)是對IR模塊進(jìn)行轉(zhuǎn)換的一個示例。該函數(shù)將模塊作為參數(shù)進(jìn)行轉(zhuǎn)換,并返回轉(zhuǎn)換后的IR模塊版本。由于JIT編譯器可能以多線程運行,因此IR模塊被包裝在ThreadSafeModule實例中:
static llvm::Expected<llvm::orc::ThreadSafeModule>
optimizeModule(
llvm::orc::ThreadSafeModule TSM,
const llvm::orc::MaterializationResponsibility
&R) {
// 為了優(yōu)化IR,我們從第七章《優(yōu)化IR》中回憶一些信息,在“為你的編譯器添加優(yōu)化管道”部分。
// 我們需要一個`PassBuilder`實例來創(chuàng)建一個優(yōu)化管道。首先,我們定義幾個分析管理器并將它們注冊到傳遞構(gòu)建器中。
// 之后,我們使用O2級別的默認(rèn)優(yōu)化管道填充一個`ModulePassManager`實例。
// 這再次是一個設(shè)計決策:O2級別已經(jīng)產(chǎn)生快速的機(jī)器代碼,但它在O3級別產(chǎn)生更快的代碼。
// 接下來,我們在模塊上運行管道,最后,優(yōu)化后的模塊返回給調(diào)用者:
TSM.withModuleDo([](llvm::Module &M) {
bool DebugPM=false;
llvm::PassBuilder PB(DebugPM);
llvm::LoopAnalysisManager LAM(DebugPM);
llvm::FunctionAnalysisManager FAM(DebugPM);
llvm::CGSCCAnalysisManager CGAM(DebugPM);
llvm::ModuleAnalysisManager MAM(DebugPM);
FAM.registerPass(
[&]() { return PB.buildDefaultAAPipeline(); });
PB.registerModuleAnalyses(MAM);
PB.registerCGSCCAnalyses(CGAM);
PB.registerFunctionAnalyses(FAM);
PB.registerLoopAnalyses(LAM);
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
llvm::ModulePassManager MPM= PB.buildPerModuleDefaultPipeline(
llvm::PassBuilder::OptimizationLevel::O2,
DebugPM);
MPM.run(M, MAM);
});
return TSM;
}
JIT類的客戶端需要一種添加IR模塊的方法,我們通過addIRModule()函數(shù)提供:
llvm::Error addIRModule(
llvm::orc::ThreadSafeModule TSM,
llvm::orc::ResourceTrackerSP RT=nullptr) {
if (!RT)
RT=MainJITDylib.getDefaultResourceTracker();
return OptIRLayer->add(RT, std::move(TSM));
}
同樣,我們的JIT類的客戶端需要一種查找符號的方法。我們將其委托給ExecutionSession實例,傳入主符號表的引用和請求符號的名稱的名稱:
llvm::Expected<llvm::orc::ExecutorSymbolDef>
lookup(llvm::StringRef Name) {
return ES->lookup({&MainJITDylib},
Mangle(Name.str()));
}
正如我們所看到的,初始化這個JIT類可能是棘手的,因為它涉及到JIT類的工廠方法和構(gòu)造函數(shù)調(diào)用,以及每個層的工廠方法。盡管這種分配是由C++的限制引起的,但代碼本身是直接的。
接下來,我們將使用新的JIT編譯器類來實現(xiàn)一個簡單的命令行實用程序,它接受LLVM IR文件作為輸入。
使用我們的新JIT編譯器類 我們首先創(chuàng)建一個名為JIT.cpp的文件,在JIT.h文件相同的目錄下,并添加以下內(nèi)容到這個源文件中:
首先,包括幾個頭文件。我們必須包括JIT.h以使用我們的新類,以及IRReader.h頭文件,因為它定義了一個讀取LLVM IR文件的函數(shù)。CommandLine.h頭文件允許我們以LLVM風(fēng)格解析命令行選項。接下來,InitLLVM.h是需要的,用于工具的基本初始化。最后,TargetSelect.h是需要的,用于初始化本地目標(biāo):
#include "JIT.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/TargetSelect.h"
接下來,我們將llvm命名空間添加到當(dāng)前范圍:
using namespace llvm;
我們的JIT工具期望在命令行上恰好有一個輸入文件,我們通過cl::opt<>類聲明它:
static cl::opt<std::string>
InputFile(cl::Positional, cl::Required,
cl::desc("<input-file>"));
為了讀取IR文件,我們調(diào)用parseIRFile()函數(shù)。該文件可以是文本IR表示或字節(jié)碼文件。該函數(shù)返回創(chuàng)建的模塊的指針。此外,錯誤處理有些不同,因為可以解析文本IR文件,這不一定是語法正確的。最后,SMDiagnostic實例在語法錯誤的情況下保存錯誤信息。如果出現(xiàn)錯誤,將打印錯誤消息并退出應(yīng)用程序:
std::unique_ptr<Module>
loadModule(StringRef Filename, LLVMContext &Ctx,
const char *ProgName) {
SMDiagnostic Err;
std::unique_ptr<Module> Mod=parseIRFile(Filename, Err, Ctx);
if (!Mod.get()) {
Err.print(ProgName, errs());
exit(-1);
}
return Mod;
}
jitmain()函數(shù)放在loadModule()方法之后。這個函數(shù)設(shè)置我們的JIT引擎并編譯一個LLVM IR模塊。該函數(shù)需要帶有IR的LLVM模塊。還需要這個模塊的LLVM上下文類,因為上下文類包含重要的類型信息。目標(biāo)是調(diào)用main()函數(shù),所以我們也傳遞了通常的argc和argv參數(shù):
Error jitmain(std::unique_ptr<Module> M,
std::unique_ptr<LLVMContext> Ctx,
int argc, char *argv[]) {
// ...
}
接下來,我們創(chuàng)建了我們之前構(gòu)建的JIT類的實例。如果發(fā)生錯誤,那么我們相應(yīng)地返回錯誤消息:
auto JIT=JIT::create();
if (!JIT)
return JIT.takeError();
然后,我們將模塊添加到主JITDylib實例中,再次將模塊和上下文包裝在ThreadSafeModule實例中。如果發(fā)生錯誤,那么我們返回錯誤消息:
if (auto Err=(*JIT)->addIRModule(
orc::ThreadSafeModule(std::move(M),
std::move(Ctx))))
return Err;
接下來,我們查找main符號。這個符號必須在命令行上給出的IR模塊中。查找觸發(fā)了該IR模塊的編譯。如果IR模塊內(nèi)引用了其他符號,那么它們使用前一步中添加的生成器進(jìn)行解析。結(jié)果是ExecutorAddr類的地址,它表示執(zhí)行程序的地址:
llvm::orc::ExecutorAddr MainExecutorAddr=MainSym->getAddress();
auto *Main=MainExecutorAddr.toPtr<int(int, char**)>();
現(xiàn)在,我們可以在IR模塊中調(diào)用main()函數(shù),并傳遞函數(shù)期望的argc和argv參數(shù)。我們忽略返回值:
(void)Main(argc, argv);
我們報告函數(shù)執(zhí)行后的成功:
return Error::success();
在實現(xiàn)jitmain()函數(shù)之后,我們添加了一個main()函數(shù),該函數(shù)初始化工具和本地目標(biāo)并解析命令行:
int main(int argc, char *argv[]) {
InitLLVM X(argc, argv);
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
cl::ParseCommandLineOptions(argc, argv, "JIT\n");
// ...
}
之后,初始化LLVM上下文類,并加載命令行上指定的IR模塊:
auto Ctx=std::make_unique<LLVMContext>();
std::unique_ptr<Module> M=loadModule(InputFile, *Ctx, argv[0]);
加載IR模塊后,我們可以調(diào)用jitmain()函數(shù)。為了處理錯誤,我們使用ExitOnError實用類在遇到錯誤時打印錯誤消息并退出應(yīng)用程序。我們還設(shè)置了應(yīng)用程序名稱的橫幅,它會在錯誤消息之前打印:
ExitOnError ExitOnErr(std::string(argv[0]) + ": ");
ExitOnErr(jitmain(std::move(M), std::move(Ctx),
argc, argv));
如果控制流到達(dá)這一點,那么IR已成功執(zhí)行。我們返回0以表示成功:
return 0;
現(xiàn)在,我們可以通過編譯一個簡單的示例來測試我們新實現(xiàn)的JIT編譯器,該示例將Hello World!打印到控制臺。在幕后,新類使用固定的優(yōu)化級別,因此對于足夠大的模塊,我們可以注意到啟動和運行時間的差異。
要構(gòu)建我們的JIT編譯器,我們可以按照在“實現(xiàn)我們自己的JIT編譯器與LLJIT部分”的末尾所做的相同CMake步驟,并且我們只需要確保JIT.cpp源文件正在與正確的庫一起編譯:
add_executable(JIT JIT.cpp)
include_directories(${CMAKE_SOURCE_DIR})
target_link_libraries(JIT ${llvm_libs})
然后,我們進(jìn)入構(gòu)建目錄并編譯應(yīng)用程序:
$ cmake –G Ninja <jit源目錄的路徑>
$ ninja
我們的JIT工具現(xiàn)在可以使用了。一個簡單的Hello World!程序可以用C語言編寫,如下所示:
$ cat main.c
#include <stdio.h>
int main(int argc, char** argv) {
printf("Hello world!\n");
return 0;
}
接下來,我們可以使用以下命令將Hello World C源代碼編譯成LLVM IR:
$ clang -S -emit-llvm main.c
記住——我們將C源代碼編譯成LLVM IR,因為我們的JIT編譯器接受IR文件作為輸入。最后,我們可以使用我們的IR示例調(diào)用我們的JIT編譯器,如下所示:
$ JIT main.ll
Hello world!
總結(jié) 在本章中,您學(xué)習(xí)了如何開發(fā)一個JIT編譯器。您從了解JIT編譯器的可能應(yīng)用開始,并探索了lli,LLVM動態(tài)編譯器和解釋器。使用預(yù)定義的LLJIT類,您構(gòu)建了一個交互式基于JIT的計算器工具,并了解了查找符號和向LLJIT添加IR模塊。為了能夠利用ORC API的分層結(jié)構(gòu),您還實現(xiàn)了一個優(yōu)化的JIT類。
在下一章中,您將學(xué)習(xí)如何利用LLVM工具進(jìn)行調(diào)試。
22 分鐘前
據(jù)百度百家消息,萬達(dá)將于9月30日上午十點與美國流媒體視頻服務(wù)商N(yùn)etflix戰(zhàn)略合作簽約儀式。另外制作人、導(dǎo)演、風(fēng)山漸文化傳播(北京)有限公司董事長也在微博中稱,“收購美國AMC、澳洲Hoyts、英國Odeon,再加上Netflix,一個世界級的娛樂帝國冉冉升起......”萬達(dá)方面回應(yīng)稱“以萬達(dá)院線公告為準(zhǔn)”。 [原文鏈接]
大約 3 小時
螞蟻金服宣布與日本 Recruit 集團(tuán)分公司 Recruit Lifestyle 合作,將支付寶接入該公司旗下 17.6萬 家商戶的智能 POS 網(wǎng)絡(luò)。據(jù)稱,未來中國用戶在日本也能夠直接使用支付寶消費,不用再進(jìn)行貨幣兌換。
1 分鐘前
DoveConviene成立于2010年,總部位于米蘭。DoveConviene為用戶提供帶有地理坐標(biāo)的購物傳單和促銷信息,消費者可通過該平臺了解打折信息,從而達(dá)到省錢的目的。該輪投資由Highland Capital Partners Europe 領(lǐng)投,Anthony Zappalà、360 Capital Partners、Anthony Zappalà、Merifin Capital和Principia SGR跟投。[原文鏈接]
16 分鐘前
Collibra成立于2008年6月,總部位于布魯塞爾。Collibra以業(yè)務(wù)為中心,提供易操作的自動化數(shù)據(jù)管理流程應(yīng)用程序。該輪投資由Index Ventures 領(lǐng)投、Dawn Capital跟投,投資將用于在數(shù)據(jù)管理中引入人工智能,以便企業(yè)用戶管理數(shù)據(jù)。 [原文鏈接]
33 分鐘前
雅虎在周一披露的文件中表示,董事會在上周三決定無論美國國稅局最終判決結(jié)果如何,都將堅持剝離所持阿里巴巴集團(tuán)的3.84億股股票,交由獨立上市公司“SpinCo”管理,并預(yù)計在今年第四季度完成交易。今年1月,雅虎宣布公司董事會批準(zhǔn)了一項免稅分拆計劃,但這項避稅方案未獲美國稅務(wù)機(jī)構(gòu)的支持。 [原文鏈接]
42 分鐘前
夏普公司28日宣布,夏普與家具連鎖公司NITORI和NTT都市開發(fā)公司簽訂了合同,以188億日元(約合人民幣10億元)的價格,出售公司位于大阪市阿倍野區(qū)的兩棟總部大樓和土地。夏普預(yù)定于2016年3月完成移交,但將仍以租借形式繼續(xù)使用至2018年3月左右。[原文鏈接]
大約 1 小時
專注于社區(qū)共享經(jīng)濟(jì)C2C的種子基金“眾創(chuàng)共享基金”今日在北京成立,主發(fā)起人為最近獲得B輪融資一億美元的e袋洗,發(fā)起方表示:該基金總規(guī)模5億元人民幣,第一期的募資規(guī)模為1億元人民幣。據(jù)GP團(tuán)隊稱,本期基金計劃在未來一年內(nèi)投出20到30個項目。[原文鏈接]
大約 1 小時
酷6網(wǎng)聯(lián)合精英集團(tuán)今日宣布,雙方共同投資打造的模特線上互動社區(qū)“魔度”上線。“魔都”是以模特為核心、以電商為特色,面向模特與粉絲的垂直視頻互動社區(qū)。據(jù)魔度總經(jīng)理金子杰介紹,自6月底上線測試以來,魔度已集聚了T臺名模、選美冠軍、宅男女神等多元化美女近400名。據(jù)悉,魔度已于9月初推出Android版的App,IOS版本將于近日發(fā)布。[原文鏈接]
大約 1 小時
D-Wave于周一宣布,包括Google,NASA和USRA在內(nèi)的三家機(jī)構(gòu)已經(jīng)與其續(xù)約7年。這意味著在在今后的七年內(nèi),D-Wave Two 電腦以及該公司研發(fā)的新電腦將繼續(xù)在NASA Ames研究中心留用。由Google牽線的這項合作旨在研究現(xiàn)今科技對發(fā)展人工智能和機(jī)器學(xué)習(xí)系統(tǒng)的作用。 [原文鏈接]
大約 2 小時
搭載 Android Wear 系統(tǒng)的智能手表用戶能夠在手表上查看 Skype 提醒,回復(fù)聊天,接通或者掛斷來電。 [原文鏈接]
大約 2 小時
總部位于美國紐約的Wheels Up于美國當(dāng)?shù)貢r間9月28日獲得T Rowe Price Associates、Fidelity Management and Research Company 和NEA三家投資機(jī)構(gòu)的1.15億美元的私募股權(quán)融資。據(jù)稱,目前公司市場估值已經(jīng)高于5億美元。Wheels Up公司是一家致力于為追求豪華和舒適且具有消費能力的會員提供高品質(zhì)的私人飛機(jī)服務(wù)公司。他們的服務(wù)實行會員制,通過繳納入會費和年費,會員可以享受較低價格的按需付費的私人飛機(jī)的停放費用,公司旗下的King Air 350i和Citation Excel/XLS飛機(jī)的專屬使用權(quán),以及公司為會員帶來的各種生活服務(wù)的活動項目。[原文鏈接]
大約 3 小時
本次改版之前,工作交流軟件Slack的運作方式更像是聊天室,可以支持短信息的推送,但較長的信息只能通過郵件的形式發(fā)送。改版后的Post2.0將允許用戶在軟件內(nèi)編寫更長的短信且能夠選擇字體,信息發(fā)出后其他的用戶也可以直接在該信息下進(jìn)行評論。另外,用戶在之前創(chuàng)建的所有內(nèi)容將會自動轉(zhuǎn)入星標(biāo)提示。據(jù)稱,此次改版將會為用戶更加便捷的編寫體驗。[原文鏈接]
大約 3 小時
洛桑聯(lián)邦理工學(xué)院(EPFL)實驗室近日研制出了一款Origami機(jī)器人,據(jù)稱,該款命名為Tribot的機(jī)器人靈感來源于槐蠶。它能夠連貫的完成爬行、彈跳等一系列動作。記憶卡用材質(zhì)輕的鎳和鈦制成。研究團(tuán)隊稱由于其極小的體積,該款機(jī)器人將被“派遣”到地球上的任何地方。理論上講,Tribot有望與相機(jī)和聲納結(jié)合用于搜尋及救援工作。[原文鏈接]
大約 3 小時
產(chǎn)品討論網(wǎng)站Product Hunt在美國當(dāng)?shù)貢r間29號早上7點推出在Podcast上推出每日更新頻道。據(jù)稱,本次開通Podcast頻道被看作是年初推出的游戲和書籍討論組的業(yè)務(wù)延伸。Product Hunt是一家類似于Reddit和Hacker News的創(chuàng)業(yè)公司,用戶可以通過他們的網(wǎng)站對產(chǎn)品進(jìn)行投票和評論。另外,公司將在定期的邀請產(chǎn)品廠商到訪節(jié)目,恰如Reddit 的ASK Me Anything(AMA)節(jié)目的播放模式。 [原文鏈接]
大約 3 小時
Walker and Company公司成立于2013年,總部位于加州帕羅奧多。本次的B輪融資的投資人分別為:Institutional Venture Partners (IVP)、Andreessen Horowitz、Collaborative Fund、Daher Capital、Felicis Ventures、Google Ventures、Melo7 Tech Partners、Upfront Ventures。Walker and Company是一家生活服務(wù)類的創(chuàng)業(yè)公司,公司以為有色人種提供健康美容類產(chǎn)品被業(yè)界熟知,旗下產(chǎn)品包括Bevel剃須刀等。 [原文鏈接]
大約 3 小時
微軟正在通過一款低成本DIY的虛擬現(xiàn)實技術(shù)頭戴設(shè)備吸引開發(fā)者參加其10月17日在俄羅斯舉辦的Hackathon。這被外界看作是微軟對Google Cardboard的挑戰(zhàn)。虛擬現(xiàn)實技術(shù)一直被用于價格昂貴制作精良的設(shè)備上,直到去年Google Cardboard的出現(xiàn)。Google Cardboard實則為一組自助組裝的簡易紙板套裝,用戶通過折疊組成頭套,并將適合尺寸的安卓手機(jī)安置于頭套前端便可以模擬出虛擬現(xiàn)實技術(shù)的效果。據(jù)稱,微軟此次的簡易紙板與Google相似,只是在手機(jī)使用上將安卓手機(jī)換成了Windows Phone。[原文鏈接]
大約 3 小時
微軟將在11月3日推出Xbox One 1TB 古墓麗影:崛起,以促進(jìn)其年底節(jié)日季的游戲市場銷量。游戲包將包含1TB的硬盤空間,配備3.5毫米耳機(jī)插口的Xbox One無線手柄,全套游戲加2015假期特輯,主角勞拉及裝備新皮膚加技能套裝以及2013年古墓麗影9:最終版高清重制下載。一直以來,索尼PS4被看作Xbox One的最大競爭對手,微軟也通過減價和捆綁銷售免費游戲的方式來增加銷量。據(jù)稱,相較于2014年8月,Xbox One目前在美國的銷量增長了26%。新游戲在微軟在線商店和Best Buy’s網(wǎng)站上可以開始預(yù)定,價格為399美元。[原文鏈接]
大約 3 小時
網(wǎng)商銀行宣布,將推出一項“小微企業(yè)成長計劃”,幫助小微企業(yè)解決融資難的問題。作為“成長計劃”的首期,網(wǎng)商銀行在近期將提供60億元的信貸資金,支持淘寶、天貓平臺的小微商家和創(chuàng)業(yè)者,備戰(zhàn)今年的雙十一。
大約 3 小時
今天阿里旅行宣布與中國酒店聯(lián)盟針對“未來酒店”項目開展戰(zhàn)略合作,合作內(nèi)容主要包括品牌宣傳、會員互通、運營支持、支付業(yè)務(wù)等合作領(lǐng)域。其中“未來酒店”是阿里旅行基于用戶分享、數(shù)據(jù)能力和營銷平臺,為酒店業(yè)做的“一站式解決方案”,目前的核心產(chǎn)品是酒店信用住。而酒店聯(lián)盟體是今年7月,城市名人、華天、開元、紐賓凱、曙光、粵海六家酒店集團(tuán)發(fā)起設(shè)立的,旨在為聯(lián)盟體所有會員帶來增值服務(wù),打通積分體系
大約 6 小時
Kreditech公司成立于2012年,總部位于德國漢堡。本次C輪融資的投資者分別為:J.C.Flowers & Co. 、Amadeus Capital Partners、Blumberg Capital、HPE Growth Capital、Peter Thiel、V?rde Partners。Kreditech致力于運用個人網(wǎng)上數(shù)據(jù)分析客戶的信用額度向用戶提供貸款服務(wù)。[原文鏈接]