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 的这种联系,以帮助从早期 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 都会添加一个包含该表达式代码的新 IR 模块到 JIT 中。如果表达式是顶级表达式,例如“1+1”或“sin(x)”,REPL 还会使用 JIT 类的 lookup 方法查找并执行表达式的代码。在本教程的后续章节中,我们将修改 REPL 以启用与 JIT 类的新交互,但现在我们将假设此设置并专注于 JIT 本身的实现。
我们的 KaleidoscopeJIT 类在 KaleidoscopeJIT.h 头文件中定义。在通常的包含保护和 #include [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
,可用于将 LLVM 模块添加到我们的 JIT(并基于 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 TargetMachines(它们不是线程安全的)以进行编译。在此之后,我们初始化我们的支持成员: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 又会将模块存储在主 JITDylib 中。此过程将在 JITDylib 中为模块中的每个定义创建新的符号表条目,并将延迟模块的编译,直到查找其任何定义。请注意,这不是延迟编译:即使从未使用,仅仅引用定义就足以触发编译。在后续章节中,我们将教我们的 JIT 在函数实际被调用之前延迟编译它们。要添加我们的模块,我们必须首先将其包装在一个 ThreadSafeModule 实例中,该实例以线程安全的方式管理模块的 LLVMContext(我们的 Ctx 成员)的生命周期。在我们的示例中,所有模块将共享 Ctx 成员,该成员将在 JIT 的整个持续时间内存在。在后续章节切换到并发编译后,我们将为每个模块使用一个新的上下文。
我们的最后一个方法是lookup
,它允许我们根据函数和变量定义的符号名称查找基于 JIT 添加的地址。如上所述,lookup 会隐式地触发任何尚未编译的符号的编译。我们的 lookup 方法会调用 ExecutionSession::lookup,传入一个要搜索的动态库列表(在我们的例子中只是主动态库),以及要搜索的符号名称,但有一个小技巧:我们必须首先修改我们要搜索的符号的名称。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