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

  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 都会向 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 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 TargetMachine(它们不是线程安全的),根据编译的需要。在此之后,我们初始化我们的支持成员: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 将反过来将 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 层概念。

下一步:扩展 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