3. 建立 JIT:每個函式的惰性編譯

本教學正在積極開發中。它尚未完工,細節可能會經常變更。儘管如此,我們還是邀請您試用它,並且歡迎任何回饋。

3.1. 第 3 章 簡介

警告:由於 ORC API 更新,本文目前已過時。

範例程式碼已更新,可以使用。API 變更穩定後,本文將會更新。

歡迎來到「在 LLVM 中建置基於 ORC 的 JIT」教學的第 3 章。本章討論惰性 JIT,並向您展示如何透過將 ORC CompileOnDemand 層新增至第 2 章的 JIT 來啟用它。

3.2. 惰性編譯

當我們將模組新增至第 2 章的 KaleidoscopeJIT 類別時,它會分別由 IRTransformLayer、IRCompileLayer 和 RTDyldObjectLinkingLayer 為我們立即最佳化、編譯和連結。這種方案(所有使模組可執行的工作都在前面完成)易於理解,並且其效能特徵易於推斷。但是,如果要編譯的程式碼量很大,它將導致非常長的啟動時間,並且如果在執行階段只呼叫了幾個已編譯的函式,則可能會進行許多不必要的編譯。真正的「即時」編譯器應該允許我們將任何給定函式的編譯推遲到第一次呼叫該函式的那一刻,從而縮短啟動時間並消除冗餘工作。事實上,ORC API 為我們提供了一個惰性編譯 LLVM IR 的層:CompileOnDemandLayer

CompileOnDemandLayer 類別符合第 2 章中描述的層介面,但其 addModule 方法的行為與我們目前為止看到的層有很大不同:它不會預先執行任何工作,而只是掃描正在新增的模組,並安排在第一次呼叫每個函式時才進行編譯。為此,CompileOnDemandLayer 為其掃描的每個函式建立兩個小型工具程式:一個「虛擬程式碼」和一個「編譯回呼」。虛擬程式碼是一對函式指標(一旦函式被編譯,它將指向函式的實作)和透過指標的間接跳轉。透過在程式生命週期內修正間接跳轉的位址,我們可以為函式提供一個永久的「有效位址」,即使函式的實作從未被編譯,或者它被編譯多次(例如,以更高的優化級別重新編譯函式)並且位址發生變化,這個位址也可以安全地用於間接尋址和函式指標比較。第二個工具程式,編譯回呼,表示從程式重新進入編譯器的入口點,它將觸發編譯,然後執行函式。透過將函式的虛擬程式碼初始化為指向函式的編譯回呼,我們可以實現延遲編譯:第一次嘗試呼叫函式時,將遵循函式指標並觸發編譯回呼。編譯回呼將編譯函式,更新虛擬程式碼的函式指標,然後執行函式。在隨後對函式的所有呼叫中,函式指標將指向已編譯的函式,因此編譯器不會產生額外開銷。我們將在本教學的下一章更詳細地研究這個過程,但現在我們將信任 CompileOnDemandLayer 為我們設定所有虛擬程式碼和回呼。我們只需要將 CompileOnDemandLayer 新增到堆疊頂部,就可以獲得延遲編譯的優點。我們只需要對原始碼進行一些更改

...
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/ExecutionEngine/Orc/CompileOnDemandLayer.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
...

...
class KaleidoscopeJIT {
private:
  std::unique_ptr<TargetMachine> TM;
  const DataLayout DL;
  RTDyldObjectLinkingLayer ObjectLayer;
  IRCompileLayer<decltype(ObjectLayer), SimpleCompiler> CompileLayer;

  using OptimizeFunction =
      std::function<std::shared_ptr<Module>(std::shared_ptr<Module>)>;

  IRTransformLayer<decltype(CompileLayer), OptimizeFunction> OptimizeLayer;

  std::unique_ptr<JITCompileCallbackManager> CompileCallbackManager;
  CompileOnDemandLayer<decltype(OptimizeLayer)> CODLayer;

public:
  using ModuleHandle = decltype(CODLayer)::ModuleHandleT;

首先,我們需要包含 CompileOnDemandLayer.h 標頭檔,然後在我們的類別中新增兩個新成員:std::unique_ptr<JITCompileCallbackManager> 和 CompileOnDemandLayer。CompileCallbackManager 成員由 CompileOnDemandLayer 使用,用於建立每個函式所需的編譯回呼。

KaleidoscopeJIT()
    : TM(EngineBuilder().selectTarget()), DL(TM->createDataLayout()),
      ObjectLayer([]() { return std::make_shared<SectionMemoryManager>(); }),
      CompileLayer(ObjectLayer, SimpleCompiler(*TM)),
      OptimizeLayer(CompileLayer,
                    [this](std::shared_ptr<Module> M) {
                      return optimizeModule(std::move(M));
                    }),
      CompileCallbackManager(
          orc::createLocalCompileCallbackManager(TM->getTargetTriple(), 0)),
      CODLayer(OptimizeLayer,
               [this](Function &F) { return std::set<Function*>({&F}); },
               *CompileCallbackManager,
               orc::createLocalIndirectStubsManagerBuilder(
                 TM->getTargetTriple())) {
  llvm::sys::DynamicLibrary::LoadLibraryPermanently(nullptr);
}

接下來,我們必須更新建構函式以初始化新成員。為了建立適當的編譯回呼管理器,我們使用 createLocalCompileCallbackManager 函式,該函式接受一個 TargetMachine 和一個 ExecutorAddr,如果它收到編譯未知函式的請求,則會呼叫該函式。在我們簡單的 JIT 中,這種情況不太可能發生,所以我們在這裡作弊,只傳遞「0」。在生產級別的 JIT 中,您可以提供一個函式的位址,該函式會在收到編譯未知函式的請求時丟擲異常,以便展開 JIT 程式碼的堆疊。

現在我們可以構建我們的 CompileOnDemandLayer。按照先前 Layer 的模式,我們首先傳遞對堆疊中下一層(OptimizeLayer)的引用。接下來,我們需要提供一個「分區函數」:當調用尚未編譯的函數時,CompileOnDemandLayer 將調用此函數以詢問我們要編譯什麼。我們至少需要編譯被調用的函數(由分區函數的參數給出),但我們也可以請求 CompileOnDemandLayer 編譯從被調用函數無條件調用(或很可能被調用)的其他函數。對於 KaleidoscopeJIT,我們將保持簡單,只請求編譯被調用的函數。接下來,我們傳遞對 CompileCallbackManager 的引用。最後,我們需要提供一個「間接 stubs 管理器構建器」:一個用於構建 IndirectStubManagers 的實用函數,而 IndirectStubManagers 又用於為每個模組中的函數構建 stubs。CompileOnDemandLayer 將為每次調用 addModule 調用一次間接 stub 管理器構建器,並使用生成的間接 stubs 管理器為集合中所有模組中的所有函數創建 stubs。如果/當模組集合從 JIT 中移除時,間接 stubs 管理器將被刪除,釋放分配給 stubs 的任何內存。我們使用 createLocalIndirectStubsManagerBuilder 工具程序提供此函數。

// ...
        if (auto Sym = CODLayer.findSymbol(Name, false))
// ...
return cantFail(CODLayer.addModule(std::move(Ms),
                                   std::move(Resolver)));
// ...

// ...
return CODLayer.findSymbol(MangledNameStream.str(), true);
// ...

// ...
CODLayer.removeModule(H);
// ...

最後,我們需要替換 addModule、findSymbol 和 removeModule 方法中對 OptimizeLayer 的引用。這樣一來,我們就可以開始運行了。

待完成

**章節結論。**

3.3. 完整程式碼清單

以下是我們運行示例的完整程式碼清單,其中添加了一個 CompileOnDemand 層以啟用延遲函數級編譯。要構建此示例,請使用

# 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.dev.org.tw/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/CompileOnDemandLayer.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/EPCIndirectionUtils.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/ExecutorProcessControl.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/IRTransformLayer.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 "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/InstCombine/InstCombine.h"
#include "llvm/Transforms/Scalar.h"
#include "llvm/Transforms/Scalar/GVN.h"
#include <memory>

namespace llvm {
namespace orc {

class KaleidoscopeJIT {
private:
  std::unique_ptr<ExecutionSession> ES;
  std::unique_ptr<EPCIndirectionUtils> EPCIU;

  DataLayout DL;
  MangleAndInterner Mangle;

  RTDyldObjectLinkingLayer ObjectLayer;
  IRCompileLayer CompileLayer;
  IRTransformLayer OptimizeLayer;
  CompileOnDemandLayer CODLayer;

  JITDylib &MainJD;

  static void handleLazyCallThroughError() {
    errs() << "LazyCallThrough error: Could not find function body";
    exit(1);
  }

public:
  KaleidoscopeJIT(std::unique_ptr<ExecutionSession> ES,
                  std::unique_ptr<EPCIndirectionUtils> EPCIU,
                  JITTargetMachineBuilder JTMB, DataLayout DL)
      : ES(std::move(ES)), EPCIU(std::move(EPCIU)), 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))),
        OptimizeLayer(*this->ES, CompileLayer, optimizeModule),
        CODLayer(*this->ES, OptimizeLayer,
                 this->EPCIU->getLazyCallThroughManager(),
                 [this] { return this->EPCIU->createIndirectStubsManager(); }),
        MainJD(this->ES->createBareJITDylib("<main>")) {
    MainJD.addGenerator(
        cantFail(DynamicLibrarySearchGenerator::GetForCurrentProcess(
            DL.getGlobalPrefix())));
  }

  ~KaleidoscopeJIT() {
    if (auto Err = ES->endSession())
      ES->reportError(std::move(Err));
    if (auto Err = EPCIU->cleanup())
      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));

    auto EPCIU = EPCIndirectionUtils::Create(*ES);
    if (!EPCIU)
      return EPCIU.takeError();

    (*EPCIU)->createLazyCallThroughManager(
        *ES, ExecutorAddr::fromPtr(&handleLazyCallThroughError));

    if (auto Err = setUpInProcessLCTMReentryViaEPCIU(**EPCIU))
      return std::move(Err);

    JITTargetMachineBuilder JTMB(
        ES->getExecutorProcessControl().getTargetTriple());

    auto DL = JTMB.getDefaultDataLayoutForTarget();
    if (!DL)
      return DL.takeError();

    return std::make_unique<KaleidoscopeJIT>(std::move(ES), std::move(*EPCIU),
                                             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 CODLayer.add(RT, std::move(TSM));
  }

  Expected<ExecutorSymbolDef> lookup(StringRef Name) {
    return ES->lookup({&MainJD}, Mangle(Name.str()));
  }

private:
  static Expected<ThreadSafeModule>
  optimizeModule(ThreadSafeModule TSM, const MaterializationResponsibility &R) {
    TSM.withModuleDo([](Module &M) {
      // Create a function pass manager.
      auto FPM = std::make_unique<legacy::FunctionPassManager>(&M);

      // Add some optimizations.
      FPM->add(createInstructionCombiningPass());
      FPM->add(createReassociatePass());
      FPM->add(createGVNPass());
      FPM->add(createCFGSimplificationPass());
      FPM->doInitialization();

      // Run the optimizations over all functions in the module being added to
      // the JIT.
      for (auto &F : M)
        FPM->run(F);
    });

    return std::move(TSM);
  }
};

} // end namespace orc
} // end namespace llvm

#endif // LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H

下一步:極致的延遲 - 使用編譯回調直接從 AST 進行 JIT 編譯