1. 构建 JIT:KaleidoscopeJIT 入门¶
1.1. 第 1 章 介绍¶
警告:本教程目前正在更新以适应 ORC API 的更改。只有第 1 章和第 2 章是最新的。
第 3 章到第 5 章的示例代码可以编译和运行,但尚未更新
欢迎来到“在 LLVM 中构建基于 ORC 的 JIT”教程的第 1 章。本教程将引导您完成使用 LLVM 的按需编译 (ORC) API 实现 JIT 编译器的过程。它从 使用 LLVM 实现语言 教程中使用的简化版 KaleidoscopeJIT 类开始,然后介绍并发编译、优化、延迟编译和远程执行等新功能。
本教程的目标是向您介绍 LLVM 的 ORC JIT API,展示这些 API 如何与 LLVM 的其他部分交互,并教您如何重新组合它们以构建适合您用例的自定义 JIT。
本教程的结构如下
第 1 章:研究简单的 KaleidoscopeJIT 类。这将介绍 ORC JIT API 的一些基本概念,包括 ORC *层* 的概念。
第 2 章:通过添加一个新层来扩展基本的 KaleidoscopeJIT,该层将优化 IR 和生成的代码。
第 3 章:通过添加按需编译层来进一步扩展 JIT,以延迟编译 IR。
第 4 章:通过用一个自定义层替换按需编译层来提高 JIT 的延迟性,该自定义层直接使用 ORC 编译回调 API 来延迟 IR 生成,直到函数被调用。
第 5 章:使用 JIT 远程 API 将代码 JIT 到具有降低权限的远程进程中,从而添加进程隔离。
为了给我们的 JIT 提供输入,我们将使用 第 7 章 “在 LLVM 教程中实现语言”中 Kaleidoscope REPL 的轻微修改版本。
最后,关于 API 世代:ORC 是 LLVM JIT API 的第三代。它之前是 MCJIT,再之前是(现在已删除的)旧版 JIT。这些教程不假设您有任何使用这些早期 API 的经验,但熟悉它们的读者会看到许多熟悉的元素。在适当的情况下,我们将明确地将此与早期 API 联系起来,以帮助正在从它们过渡到 ORC 的人。
1.2. JIT API 基础知识¶
JIT 编译器的目的是在需要时“即时”编译代码,而不是像传统编译器那样提前将整个程序编译到磁盘。为了支持这一目标,我们最初的、最基本的 JIT API 将只有两个函数
Error addModule(std::unique_ptr<Module> M)
:使给定的 IR 模块可用于执行。Expected<ExecutorSymbolDef> lookup()
:搜索已添加到 JIT 的符号(函数或变量)的指针。
此 API 的基本用例(执行模块中的“main”函数)将如下所示
JIT J;
J.addModule(buildModule());
auto *Main = J.lookup("main").getAddress().toPtr<int(*)(int, char *[])>();
int Result = Main();
我们在这些教程中构建的 API 都将是这个简单主题的变体。在这个 API 的背后,我们将改进 JIT 的实现,以添加对并发编译、优化和延迟编译的支持。最终,我们将扩展 API 本身,以允许将更高级别的程序表示形式(例如 AST)添加到 JIT。
1.3. KaleidoscopeJIT¶
在前一节中,我们描述了我们的 API,现在我们来研究它的一个简单实现:KaleidoscopeJIT 类 [1],它在 使用 LLVM 实现语言 教程中使用。我们将使用该教程 第 7 章 中的 REPL 代码来为我们的 JIT 提供输入:每次用户输入表达式时,REPL 都会向 JIT 添加一个新的 IR 模块,其中包含该表达式的代码。如果表达式是顶级表达式,例如 “1+1” 或 “sin(x)”,REPL 还会使用我们的 JIT 类的 lookup 方法来查找和执行该表达式的代码。在本教程的后续章节中,我们将修改 REPL 以启用与我们的 JIT 类的新交互,但现在我们将默认接受此设置,并将注意力集中在 JIT 本身的实现上。
我们的 KaleidoscopeJIT 类在 KaleidoscopeJIT.h 头文件中定义。在通常的包含保护和 #includes [2] 之后,我们开始定义我们的类
#ifndef LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
#define LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
#include "llvm/ADT/StringRef.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/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/LLVMContext.h"
#include <memory>
namespace llvm {
namespace orc {
class KaleidoscopeJIT {
private:
ExecutionSession ES;
RTDyldObjectLinkingLayer ObjectLayer;
IRCompileLayer CompileLayer;
DataLayout DL;
MangleAndInterner Mangle;
ThreadSafeContext Ctx;
public:
KaleidoscopeJIT(JITTargetMachineBuilder JTMB, DataLayout DL)
: ObjectLayer(ES,
[]() { return std::make_unique<SectionMemoryManager>(); }),
CompileLayer(ES, ObjectLayer, ConcurrentIRCompiler(std::move(JTMB))),
DL(std::move(DL)), Mangle(ES, this->DL),
Ctx(std::make_unique<LLVMContext>()) {
ES.getMainJITDylib().addGenerator(
cantFail(DynamicLibrarySearchGenerator::GetForCurrentProcess(DL.getGlobalPrefix())));
}
我们的类以六个成员变量开始:一个 ExecutionSession 成员 ES
,它为我们正在运行的 JIT 代码提供上下文(包括字符串池、全局互斥锁和错误报告工具);一个 RTDyldObjectLinkingLayer ObjectLayer
,可以用于向我们的 JIT 添加目标文件(尽管我们不会直接使用它);一个 IRCompileLayer CompileLayer
,可以用于向我们的 JIT 添加 LLVM 模块(它构建在 ObjectLayer 之上),一个 DataLayout 和 MangleAndInterner DL
和 Mangle
,将用于符号修饰(稍后会详细介绍);最后是一个 LLVMContext,客户端将在为 JIT 构建 IR 文件时使用它。
接下来是我们的类构造函数,它接受一个 JITTargetMachineBuilder,它将被我们的 IRCompiler 使用,以及一个 DataLayout
,我们将使用它来初始化我们的 DL 成员。构造函数首先初始化我们的 ObjectLayer。ObjectLayer 需要对 ExecutionSession 的引用,以及一个函数对象,该对象将为添加的每个模块构建一个 JIT 内存管理器(JIT 内存管理器管理内存分配、内存权限和 JIT 代码的异常处理程序注册)。为此,我们使用一个 lambda,它返回一个 SectionMemoryManager,这是一个现成的实用程序,它提供了本章所需的所有基本内存管理功能。接下来,我们初始化我们的 CompileLayer。CompileLayer 需要三件事:(1)对 ExecutionSession 的引用,(2)对我们的对象层的引用,以及(3)一个编译器实例,用于执行从 IR 到目标文件的实际编译。我们使用现成的 ConcurrentIRCompiler 实用程序作为我们的编译器,我们使用此构造函数的 JITTargetMachineBuilder 参数来构造它。ConcurrentIRCompiler 实用程序将使用 JITTargetMachineBuilder 来构建 llvm TargetMachine(它们不是线程安全的),根据编译的需要。在此之后,我们初始化我们的支持成员:DL
、Mangler
和 Ctx
,分别使用输入 DataLayout、ExecutionSession 和 DL 成员,以及一个新的默认构造的 LLVMContext。现在我们的成员已经初始化,剩下的唯一一件事是调整我们将要存储代码的 *JITDylib* 的配置。我们希望修改这个 dylib,使其不仅包含我们添加到其中的符号,还包含来自我们的 REPL 进程的符号。我们通过使用 DynamicLibrarySearchGenerator::GetForCurrentProcess
方法附加一个 DynamicLibrarySearchGenerator
实例来做到这一点。
static Expected<std::unique_ptr<KaleidoscopeJIT>> Create() {
auto JTMB = JITTargetMachineBuilder::detectHost();
if (!JTMB)
return JTMB.takeError();
auto DL = JTMB->getDefaultDataLayoutForTarget();
if (!DL)
return DL.takeError();
return std::make_unique<KaleidoscopeJIT>(std::move(*JTMB), std::move(*DL));
}
const DataLayout &getDataLayout() const { return DL; }
LLVMContext &getContext() { return *Ctx.getContext(); }
接下来我们有一个命名构造函数 Create
,它将构建一个 KaleidoscopeJIT 实例,该实例配置为为我们的主机进程生成代码。它通过首先使用该类的 detectHost 方法生成一个 JITTargetMachineBuilder 实例,然后使用该实例为目标进程生成一个 datalayout 来实现这一点。每个操作都可能失败,因此每个操作都将其结果包装在 Expected 值 [3] 中,我们必须在继续之前检查错误。如果两个操作都成功,我们可以解包它们的结果(使用解引用运算符),并将它们传递给 KaleidoscopeJIT 构造函数的最后一行。
在命名构造函数之后,我们有 getDataLayout()
和 getContext()
方法。这些方法用于使 JIT 创建和管理的数据结构(尤其是 LLVMContext)可用于将构建我们的 IR 模块的 REPL 代码。
void addModule(std::unique_ptr<Module> M) {
cantFail(CompileLayer.add(ES.getMainJITDylib(),
ThreadSafeModule(std::move(M), Ctx)));
}
Expected<ExecutorSymbolDef> lookup(StringRef Name) {
return ES.lookup({&ES.getMainJITDylib()}, Mangle(Name.str()));
}
现在我们来到了我们的第一个 JIT API 方法:addModule。此方法负责将 IR 添加到 JIT 并使其可用于执行。在我们 JIT 的初始实现中,我们将通过将模块添加到 CompileLayer 来使我们的模块“可用于执行”,CompileLayer 将反过来将 Module 存储在主 JITDylib 中。此过程将在 JITDylib 中为模块中的每个定义创建新的符号表条目,并将模块的编译推迟到查找其任何定义时。请注意,这不是延迟编译:仅仅引用一个定义,即使它从未使用过,也足以触发编译。在后面的章节中,我们将教我们的 JIT 将函数的编译推迟到实际调用它们时。要添加我们的 Module,我们必须首先将其包装在 ThreadSafeModule 实例中,该实例以线程友好的方式管理 Module 的 LLVMContext(我们的 Ctx 成员)的生命周期。在我们的示例中,所有模块将共享 Ctx 成员,该成员将在 JIT 的整个生命周期内存在。一旦我们在后面的章节中切换到并发编译,我们将为每个模块使用一个新的上下文。
我们的最后一个方法是 lookup
,它允许我们根据符号名称查找已添加到 JIT 的函数和变量定义的地址。如上所述,lookup 将隐式触发尚未编译的任何符号的编译。我们的 lookup 方法调用到 ExecutionSession::lookup,传入要搜索的 dylib 列表(在我们的例子中只是主 dylib),以及要搜索的符号名称,但有一个小技巧:我们必须首先 *修饰* 我们要搜索的符号的名称。ORC JIT 组件在内部使用修饰后的符号,就像静态编译器和链接器一样,而不是使用纯 IR 符号名称。这允许 JIT 代码轻松地与应用程序或共享库中的预编译代码互操作。修饰的种类将取决于 DataLayout,而 DataLayout 又取决于目标平台。为了使我们能够保持可移植性并基于未修饰的名称进行搜索,我们只需使用我们的 Mangle
成员函数对象来重新生成这种修饰。
这使我们来到了构建 JIT 的第 1 章的末尾。您现在拥有一个基本但功能齐全的 JIT 堆栈,您可以使用它来获取 LLVM IR 并使其在您的 JIT 进程的上下文中可执行。在下一章中,我们将研究如何扩展此 JIT 以生成更高质量的代码,并在过程中更深入地了解 ORC 层概念。
1.4. 完整代码清单¶
这是我们的运行示例的完整代码清单。要构建此示例,请使用
# Compile
clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy
# Run
./toy
这是代码
//===- KaleidoscopeJIT.h - A simple JIT for Kaleidoscope --------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.net.cn/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Contains a simple JIT definition for use in the kaleidoscope tutorials.
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
#define LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
#include "llvm/ADT/StringRef.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/ExecutorProcessControl.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/ExecutionEngine/Orc/Shared/ExecutorSymbolDef.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/LLVMContext.h"
#include <memory>
namespace llvm {
namespace orc {
class KaleidoscopeJIT {
private:
std::unique_ptr<ExecutionSession> ES;
DataLayout DL;
MangleAndInterner Mangle;
RTDyldObjectLinkingLayer ObjectLayer;
IRCompileLayer CompileLayer;
JITDylib &MainJD;
public:
KaleidoscopeJIT(std::unique_ptr<ExecutionSession> ES,
JITTargetMachineBuilder JTMB, DataLayout DL)
: ES(std::move(ES)), DL(std::move(DL)), Mangle(*this->ES, this->DL),
ObjectLayer(*this->ES,
[]() { return std::make_unique<SectionMemoryManager>(); }),
CompileLayer(*this->ES, ObjectLayer,
std::make_unique<ConcurrentIRCompiler>(std::move(JTMB))),
MainJD(this->ES->createBareJITDylib("<main>")) {
MainJD.addGenerator(
cantFail(DynamicLibrarySearchGenerator::GetForCurrentProcess(
DL.getGlobalPrefix())));
}
~KaleidoscopeJIT() {
if (auto Err = ES->endSession())
ES->reportError(std::move(Err));
}
static Expected<std::unique_ptr<KaleidoscopeJIT>> Create() {
auto EPC = SelfExecutorProcessControl::Create();
if (!EPC)
return EPC.takeError();
auto ES = std::make_unique<ExecutionSession>(std::move(*EPC));
JITTargetMachineBuilder JTMB(
ES->getExecutorProcessControl().getTargetTriple());
auto DL = JTMB.getDefaultDataLayoutForTarget();
if (!DL)
return DL.takeError();
return std::make_unique<KaleidoscopeJIT>(std::move(ES), std::move(JTMB),
std::move(*DL));
}
const DataLayout &getDataLayout() const { return DL; }
JITDylib &getMainJITDylib() { return MainJD; }
Error addModule(ThreadSafeModule TSM, ResourceTrackerSP RT = nullptr) {
if (!RT)
RT = MainJD.getDefaultResourceTracker();
return CompileLayer.add(RT, std::move(TSM));
}
Expected<ExecutorSymbolDef> lookup(StringRef Name) {
return ES->lookup({&MainJD}, Mangle(Name.str()));
}
};
} // end namespace orc
} // end namespace llvm
#endif // LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H