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 将只有两个函数

  1. Error addModule(std::unique_ptr<Module> M):使给定的 IR 模块可供执行。

  2. 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 DLMangle,将用于符号修饰(稍后详细介绍);最后是一个 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(它们不是线程安全的)以进行编译。在此之后,我们初始化我们的支持成员:DLManglerCtx 分别使用输入 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 层的概念。

下一章:扩展 KaleidoscopeJIT

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