ORC 設計與實作

簡介

本文件旨在提供 ORC JIT API 設計與實作的高階概觀。除非另有說明,否則所有討論均指現代 ORCv2 API(自 LLVM 7 起可用)。希望從 OrcV1 轉換的用戶應參閱第 從 ORCv1 轉換到 ORCv2 節。

使用案例

ORC 提供了一個用於建構 JIT 編譯器的模組化 API。此類 API 有許多使用案例。例如

1. LLVM 教學課程使用一個簡單的基於 ORC 的 JIT 類別來執行從玩具語言編譯的運算式:Kaleidoscope。

2. LLVM 除錯器 LLDB 使用交叉編譯 JIT 進行運算式求值。在此使用案例中,交叉編譯允許在除錯器程序中編譯的運算式在除錯目標程序上執行,該目標程序可能位於不同的裝置/架構上。

3. 在希望在現有 JIT 基礎架構中使用 LLVM 優化的效能 JIT(例如 JVM、Julia)中。

  1. 在解譯器和 REPL 中,例如 Cling (C++) 和 Swift 解譯器。

透過採用模組化、基於程式庫的設計,我們的目標是讓 ORC 在盡可能多的情況下都可使用。

功能

ORC 提供以下功能

JIT 連結

ORC 提供 API,可在執行階段將可重定位物件檔案(COFF、ELF、MachO)[1] 連結到目標程序中。目標程序可以是包含 JIT 會話物件和 jit-linker 的相同程序,也可以是透過 RPC 與 JIT 通訊的另一個程序(甚至是執行在不同機器或架構上的程序)。

LLVM IR 編譯

ORC 提供現成的元件(IRCompileLayer、SimpleCompiler、ConcurrentIRCompiler),可以輕鬆地將 LLVM IR 新增至 JIT 處理程序。

積極和延遲編譯

根據預設,當 JIT 會話物件 (ExecutionSession) 中的符號被查詢時,ORC 會立即編譯它們。預設情況下,立即編譯使得 ORC 易於用作現有 JIT 的記憶體編譯器(類似於 MCJIT 的常見用法)。但是,ORC 也透過延遲匯出提供了對延遲編譯的內建支援(請參閱延遲性)。

支援自訂編譯器和程式表示

用戶端可以為其在 JIT 會話中定義的每個符號提供自訂編譯器。當需要符號的定義時,ORC 將執行用戶端提供的編譯器。ORC 實際上是完全與語言無關的:LLVM IR 並未被特別處理,而是透過用於自訂編譯器的相同包裝器機制(MaterializationUnit 類別)來支援。

並行程式碼的 JIT 編譯並行編譯

JIT 編譯的程式碼可以在多個執行緒中執行,可以產生新的執行緒,並且可以從多個執行緒並行地重新進入 ORC(例如,請求延遲編譯)。由 ORC 啟動的編譯器可以並行執行(前提是用戶端設置了適當的調度程式)。內建的依賴關係追蹤確保 ORC 在所有依賴關係也被 JIT 編譯並且它們可以安全地呼叫或使用之前,不會釋放指向 JIT 編譯的程式碼或資料的指標。

可移除的程式碼

JIT 編譯的程式表示的資源

正交性可組合性

上述每個功能都可以獨立使用。可以將 ORC 元件組合在一起,以建立非延遲、程序內、單執行緒 JIT,或延遲、程序外、並行 JIT,或介於兩者之間的任何東西。

LLJIT 和 LLLazyJIT

ORC 提供了兩個基本的現成 JIT 類別。它們既可以用作如何組裝 ORC 元件以製作 JIT 的範例,也可以用作先前 LLVM JIT API(例如 MCJIT)的替代品。

LLJIT 類別使用 IRCompileLayer 和 RTDyldObjectLinkingLayer 來支援 LLVM IR 的編譯和可重定位物件檔案的連結。所有操作都在符號查詢時立即執行(即,一旦您嘗試查詢符號的地址,就會編譯該符號的定義)。在多數情況下,LLJIT 是 MCJIT 的合適替代品(注意:某些更進階的功能,例如 JITEventListeners,尚不支援)。

LLLazyJIT 擴展了 LLJIT,並添加了一個 CompileOnDemandLayer 以啟用 LLVM IR 的延遲編譯。當透過 addLazyIRModule 方法添加 LLVM IR 模組時,該模組中的函式主體在第一次被呼叫之前不會被編譯。LLLazyJIT 旨在替換 LLVM 的原始(MCJIT 之前的)JIT API。

可以使用 LLJIT 和 LLLazyJIT 的各自建構器類別 LLJITBuilder 和 LLazyJITBuilder 來建立它們的實例。例如,假設您在 ThreadSafeContext Ctx 上載入了一個模組 M

// Try to detect the host arch and construct an LLJIT instance.
auto JIT = LLJITBuilder().create();

// If we could not construct an instance, return an error.
if (!JIT)
  return JIT.takeError();

// Add the module.
if (auto Err = JIT->addIRModule(TheadSafeModule(std::move(M), Ctx)))
  return Err;

// Look up the JIT'd code entry point.
auto EntrySym = JIT->lookup("entry");
if (!EntrySym)
  return EntrySym.takeError();

// Cast the entry point address to a function pointer.
auto *Entry = EntrySym.getAddress().toPtr<void(*)()>();

// Call into JIT'd code.
Entry();

建構器類別提供了一些可以在建構 JIT 實例之前指定的配置選項。例如

// Build an LLLazyJIT instance that uses four worker threads for compilation,
// and jumps to a specific error handler (rather than null) on lazy compile
// failures.

void handleLazyCompileFailure() {
  // JIT'd code will jump here if lazy compilation fails, giving us an
  // opportunity to exit or throw an exception into JIT'd code.
  throw JITFailed();
}

auto JIT = LLLazyJITBuilder()
             .setNumCompileThreads(4)
             .setLazyCompileFailureAddr(
                 ExecutorAddr::fromPtr(&handleLazyCompileFailure))
             .create();

// ...

對於想要開始使用 LLJIT 的使用者,可以在 llvm/examples/HowToUseLLJIT 中找到一個最小範例程式。

設計概述

ORC 的 JIT 程式模型旨在模擬靜態和動態鏈接器所使用的鏈接和符號解析規則。這讓 ORC 能夠 JIT 任何 LLVM IR,包括由使用符號鏈接和可見性,以及弱 [3] 和通用符號定義等結構的普通靜態編譯器(例如 clang)產生的 IR。

為了瞭解其運作方式,假設一個程式 foo 鏈接到一對動態函式庫:libAlibB。在命令列上,建置此程式可能如下所示

$ clang++ -shared -o libA.dylib a1.cpp a2.cpp
$ clang++ -shared -o libB.dylib b1.cpp b2.cpp
$ clang++ -o myapp myapp.cpp -L. -lA -lB
$ ./myapp

在 ORC 中,這會轉換為對假設的 CXXCompilingLayer 的 API 呼叫(為簡潔起見,省略了錯誤檢查)

ExecutionSession ES;
RTDyldObjectLinkingLayer ObjLinkingLayer(
    ES, []() { return std::make_unique<SectionMemoryManager>(); });
CXXCompileLayer CXXLayer(ES, ObjLinkingLayer);

// Create JITDylib "A" and add code to it using the CXX layer.
auto &LibA = ES.createJITDylib("A");
CXXLayer.add(LibA, MemoryBuffer::getFile("a1.cpp"));
CXXLayer.add(LibA, MemoryBuffer::getFile("a2.cpp"));

// Create JITDylib "B" and add code to it using the CXX layer.
auto &LibB = ES.createJITDylib("B");
CXXLayer.add(LibB, MemoryBuffer::getFile("b1.cpp"));
CXXLayer.add(LibB, MemoryBuffer::getFile("b2.cpp"));

// Create and specify the search order for the main JITDylib. This is
// equivalent to a "links against" relationship in a command-line link.
auto &MainJD = ES.createJITDylib("main");
MainJD.addToLinkOrder(&LibA);
MainJD.addToLinkOrder(&LibB);
CXXLayer.add(MainJD, MemoryBuffer::getFile("main.cpp"));

// Look up the JIT'd main, cast it to a function pointer, then call it.
auto MainSym = ExitOnErr(ES.lookup({&MainJD}, "main"));
auto *Main = MainSym.getAddress().toPtr<int(*)(int, char *[])>();

int Result = Main(...);

這個例子沒有說明編譯將*如何*或*何時*發生。這取決於假設的 CXXCompilingLayer 的實現。然而,無論實現為何,都將套用相同的基於鏈接器的符號解析規則。例如,如果 a1.cpp 和 a2.cpp 都定義了一個函數「foo」,那麼 ORCv2 將會產生重複定義錯誤。另一方面,如果 a1.cpp 和 b1.cpp 都定義了「foo」,則不會出現錯誤(不同的動態函式庫可以定義相同的符號)。如果 main.cpp 引用「foo」,它應該綁定到 LibA 中的定義,而不是 LibB 中的定義,因為 main.cpp 是「main」動態函式庫的一部分,而 main 動態函式庫在 LibB 之前鏈接到 LibA。

許多 JIT 客戶端不需要嚴格遵守通常的預先鏈接規則,並且應該能夠通過將所有代碼放在單個 JITDylib 中來順利完成工作。但是,想要為傳統上依賴預先鏈接的語言/專案(例如 C++)JIT 代碼的客戶端會發現此功能使工作變得更加輕鬆。

除了為符號提供位址之外,ORC 中的符號查找還提供另外兩個重要功能:(1) 它會觸發所搜尋符號的編譯(如果尚未編譯),以及 (2) 它提供並行編譯的同步機制。查找過程的偽代碼如下

construct a query object from a query set and query handler
lock the session
lodge query against requested symbols, collect required materializers (if any)
unlock the session
dispatch materializers (if any)

在此上下文中,實例化器是一種在請求時提供符號工作定義的東西。通常,實例化器只是編譯器的包裝器,但它們也可以直接包裝 jit-linker(如果定義背後的程式表示形式是一個目標檔案),或者甚至可以是一個直接將位元寫入記憶體的類別(例如,如果定義是存根)。實例化是任何操作的總稱(編譯、鏈接、分散位元、向執行時註冊等),這些操作是產生可以安全呼叫或訪問的符號定義所必需的。

當每個實例化器完成其工作時,它會通知 JITDylib,而 JITDylib 又會通知任何正在等待新實例化定義的查詢物件。每個查詢物件都會維護一個計數,記錄它仍在等待的符號數量,一旦此計數達到零,查詢物件就會使用描述結果的*符號映射*(符號名稱到位址的映射)呼叫查詢處理常式。如果任何符號無法實例化,查詢會立即使用錯誤呼叫查詢處理常式。

收集到的實例化單元會發送到 ExecutionSession 進行分派,並且分派行為可以由客戶端設定。默認情況下,每個實例化器都在呼叫執行緒上運行。客戶端可以自由創建新執行緒來運行實例化器,或者將工作發送到執行緒池的工作佇列(這就是 LLJIT/LLLazyJIT 的做法)。

頂級 API

在上面的例子中可以看到許多 ORC 的頂層 API

  • ExecutionSession 代表 JIT 編譯的程式,並為 JIT 提供上下文:它包含 JITDylibs、錯誤報告機制,並分派實例化器。

  • JITDylibs 提供符號表。

  • Layers(ObjLinkingLayer 和 CXXLayer)是編譯器的包裝器,允許用戶端將這些編譯器支援的未編譯程式表示加入 JITDylibs。

  • ResourceTrackers 允許您移除程式碼。

還有一些其他的重要 API 被明確使用。JIT 用戶端不需要知道它們,但 Layer 作者會使用它們

  • MaterializationUnit - 當呼叫 XXXLayer::add 時,它會將給定的程式表示(在本例中為 C++ 原始碼)包裝在一個 MaterializationUnit 中,然後將其儲存在 JITDylib 中。MaterializationUnits 負責描述它們提供的定義,並在需要編譯時解開程式表示並將其傳遞回 Layer(這種所有權轉移使得編寫線程安全的 Layer 更加容易,因為程式表示的所有權將在堆疊上传遞,而不是必須從 Layer 成員中取出,這將需要同步)。

  • MaterializationResponsibility - 當 MaterializationUnit 將程式表示傳遞回 Layer 時,它會帶有一個相關聯的 MaterializationResponsibility 物件。這個物件會追蹤必須實例化的定義,並提供一種方法在實例化成功或失敗時通知 JITDylib。

絕對符號、別名和重匯出

ORC 可以很容易地定義具有絕對地址的符號,或者只是其他符號的別名的符號

絕對符號

絕對符號是直接映射到地址的符號,不需要進一步的實例化,例如:"foo" = 0x1234。絕對符號的一個用例是允許解析程序符號。例如:

JD.define(absoluteSymbols(SymbolMap({
    { Mangle("printf"),
      { ExecutorAddr::fromPtr(&printf),
        JITSymbolFlags::Callable } }
  });

建立了這種映射之後,添加到 JIT 的程式碼就可以符號化地引用 printf,而不需要將 printf 的地址“嵌入”進去。這反過來又允許在 JIT 會話中重複使用 JIT 程式碼的快取版本(例如編譯好的物件),因為 JIT 程式碼不再改變,只有絕對符號定義會改變。

對於程序和程式庫符號,可以使用 DynamicLibrarySearchGenerator 工具(請參閱如何將程序和程式庫符號添加到 JITDylibs)自動為您建立絕對符號映射。但是,absoluteSymbols 函數對於使 JIT 中的非全域物件對 JIT 程式碼可見仍然很有用。例如,假設您的 JIT 標準程式庫需要存取您的 JIT 物件才能進行某些呼叫。我們可以將您的物件的地址嵌入到程式庫中,但是這樣每次會話都需要重新編譯它

// From standard library for JIT'd code:

class MyJIT {
public:
  void log(const char *Msg);
};

void log(const char *Msg) { ((MyJIT*)0x1234)->log(Msg); }

我們可以將其轉換為 JIT 標準程式庫中的符號引用

extern MyJIT *__MyJITInstance;

void log(const char *Msg) { __MyJITInstance->log(Msg); }

然後在 JIT 啟動時使用絕對符號定義使我們的 JIT 物件對 JIT 標準程式庫可見

MyJIT J = ...;

auto &JITStdLibJD = ... ;

JITStdLibJD.define(absoluteSymbols(SymbolMap({
    { Mangle("__MyJITInstance"),
      { ExecutorAddr::fromPtr(&J), JITSymbolFlags() } }
  });

別名和重匯出

別名和重匯出允許您定義映射到現有符號的新符號。這對於在不需要重新編譯程式碼的情況下更改會話之間符號的鏈接關係非常有用。例如,假設 JIT 程式碼可以存取一個日誌函數,void log(const char*),它在 JIT 標準程式庫中有兩個實現:log_fastlog_detailed。您的 JIT 可以通過在 JIT 啟動時設置別名來選擇在引用 log 符號時使用哪個定義

auto &JITStdLibJD = ... ;

auto LogImplementationSymbol =
 Verbose ? Mangle("log_detailed") : Mangle("log_fast");

JITStdLibJD.define(
  symbolAliases(SymbolAliasMap({
      { Mangle("log"),
        { LogImplementationSymbol
          JITSymbolFlags::Exported | JITSymbolFlags::Callable } }
    });

`symbolAliases` 函數允許您在單個 JITDylib 中定義別名。 `reexports` 函數提供了相同的功能,但可在 JITDylib 邊界之間運作。例如:

auto &JD1 = ... ;
auto &JD2 = ... ;

// Make 'bar' in JD2 an alias for 'foo' from JD1.
JD2.define(
  reexports(JD1, SymbolAliasMap({
      { Mangle("bar"), { Mangle("foo"), JITSymbolFlags::Exported } }
    });

reexports 工具很方便用於通過從其他幾個 JITDylib 重新匯出符號來組成單個 JITDylib 接口。

惰性

ORC 中的惰性由稱為“惰性重新匯出”的工具提供。惰性重新匯出類似於常規重新匯出或別名:它為現有符號提供新名稱。然而,與常規重新匯出不同,惰性重新匯出的查找不會觸發重新匯出符號的立即具體化。相反,它們只會觸發函數存根的具體化。此函數存根被初始化為指向*惰性調用*,它提供重新進入 JIT 的途徑。如果在運行時調用了存根,則惰性調用將查找重新匯出的符號(如有必要,將觸發其具體化),更新存根(以便在後續調用中直接調用重新匯出的符號),然後通過重新匯出的符號返回。通過重新使用現有的符號查找機制,惰性重新匯出繼承了相同的併發保證:可以從多個線程同時調用惰性重新匯出,並且重新匯出的符號可以處於任何編譯狀態(未編譯、已在編譯過程中或已編譯),並且調用將會成功。這使得惰性可以安全地與遠程編譯、併發編譯、併發 JIT 代碼和推測性編譯等功能混合使用。

常規重新匯出和惰性重新匯出之間還有一個關鍵區別,一些客戶端必須注意:惰性重新匯出的地址將與重新匯出符號的地址*不同*(而常規重新匯出保證具有與重新匯出符號相同的地址)。關心指針相等的客戶端通常希望使用重新匯出的地址作為重新匯出符號的規範地址。這將允許在不強制具體化重新匯出的情況下獲取地址。

使用範例

如果 JITDylib `JD` 包含符號 `foo_body` 和 `bar_body` 的定義,我們可以通過調用在 JITDylib `JD2` 中創建惰性入口點 `Foo` 和 `Bar`

auto ReexportFlags = JITSymbolFlags::Exported | JITSymbolFlags::Callable;
JD2.define(
  lazyReexports(CallThroughMgr, StubsMgr, JD,
                SymbolAliasMap({
                  { Mangle("foo"), { Mangle("foo_body"), ReexportedFlags } },
                  { Mangle("bar"), { Mangle("bar_body"), ReexportedFlags } }
                }));

有關如何將 lazyReexports 與 LLJIT 類一起使用的完整示例,請參見 `llvm/examples/OrcV2Examples/LLJITWithLazyReexports`。

支援自訂編譯器

待定。

從 ORCv1 轉換到 ORCv2

自 LLVM 7.0 起,新的 ORC 開發工作一直集中在增加對併發 JIT 編譯的支援。支援併發性的新 API(包括新的層接口和實現,以及新的工具)統稱為 ORCv2,而原始的非併發層和工具現在稱為 ORCv1。

大多數 ORCv1 層和工具在 LLVM 8.0 中都已重命名為帶有“Legacy”前綴,並在 LLVM 9.0 中附加了棄用警告。在 LLVM 12.0 中,ORCv1 將被完全刪除。

對於大多數客戶端而言,從 ORCv1 轉換到 ORCv2 應該很容易。大多數 ORCv1 層和工具都有可以直接替換的 ORCv2 對應物 [2]。但是,ORCv1 和 ORCv2 之間存在一些設計差異需要注意

  1. ORCv2 完全採用了從 MCJIT 開始的 JIT-as-linker 模型。模組(和其他程式表示法,例如物件檔案)不再直接添加到 JIT 類別或層級。相反的,它們會透過層級被添加到 JITDylib 實例中。JITDylib 決定定義所在的位置,而層級則決定定義的編譯方式。JITDylibs 之間的連結關係決定了如何解析模組間的引用,並且不再使用符號解析器。有關更多詳細資訊,請參閱設計概覽章節。

    除非需要多個 JITDylib 來模擬連結關係,否則 ORCv1 客戶端應將所有程式碼放在單一 JITDylib 中。MCJIT 客戶端應使用 LLJIT(請參閱LLJIT 和 LLLazyJIT),並且可以將程式碼放在 LLJIT 預設建立的主要 JITDylib 中(請參閱LLJIT::getMainJITDylib())。

  2. 現在所有 JIT 堆疊都需要一個 ExecutionSession 實例。ExecutionSession 管理字串池、錯誤報告、同步和符號查詢。

  3. ORCv2 使用唯一化的字串(SymbolStringPtr 實例),而不是字串值,以便減少記憶體開銷並提高查詢效能。請參閱如何管理符號字串小節。

  4. IR 層級需要 ThreadSafeModule 實例,而不是 std::unique_ptr<Module>。ThreadSafeModule 是一個包裝器,可確保不會同時存取使用相同 LLVMContext 的模組。請參閱如何使用 ThreadSafeModule 和 ThreadSafeContext

  5. 符號查詢不再由層級處理。相反的,JITDylib 上有一個 lookup 方法,它接受一個要掃描的 JITDylib 列表。

    ExecutionSession ES;
    JITDylib &JD1 = ...;
    JITDylib &JD2 = ...;
    
    auto Sym = ES.lookup({&JD1, &JD2}, ES.intern("_main"));
    
  6. removeModule/removeObject 方法已被 ResourceTracker::remove 取代。請參閱如何移除程式碼小節。

有關程式碼範例和如何使用 ORCv2 API 的建議,請參閱使用方法章節。

使用方法

如何管理符號字串

ORC 中的符號字串是唯一化的,以提高查詢效能、減少記憶體開銷,並允許符號名稱作為有效的鍵值。若要取得字串值的唯一 SymbolStringPtr,請呼叫 ExecutionSession::intern 方法

ExecutionSession ES;
/// ...
auto MainSymbolName = ES.intern("main");

如果您希望使用符號的 C/IR 名稱進行查詢,則還需要在將字串內部化之前應用平台連結器名稱修飾。在 Linux 上,此名稱修飾是無操作的,但在其他平台上,它通常涉及在字串中添加前綴(例如,在 Darwin 上為「_」)。名稱修飾方案基於目標的 DataLayout。給定 DataLayout 和 ExecutionSession,您可以建立一個 MangleAndInterner 函數物件,它將為您執行這兩項工作

ExecutionSession ES;
const DataLayout &DL = ...;
MangleAndInterner Mangle(ES, DL);

// ...

// Portable IR-symbol-name lookup:
auto Sym = ES.lookup({&MainJD}, Mangle("main"));

如何建立 JITDylibs 並設定連結關係

在 ORC 中,所有符號定義都位於 JITDylibs 中。JITDylibs 是透過使用唯一名稱呼叫 ExecutionSession::createJITDylib 方法來建立的

ExecutionSession ES;
auto &JD = ES.createJITDylib("libFoo.dylib");

JITDylib 由 ExecutionEngine 實例擁有,並將在其被銷毀時釋放。

如何移除程式碼

要從 JITDylib 中移除個別模組,必須先使用明確的 ResourceTracker 將其加入。然後,可以透過呼叫 ResourceTracker::remove 來移除該模組。

auto &JD = ... ;
auto M = ... ;

auto RT = JD.createResourceTracker();
Layer.add(RT, std::move(M)); // Add M to JD, tracking resources with RT

RT.remove(); // Remove M from JD.

直接加入 JITDylib 的模組將由該 JITDylib 的預設資源追蹤器追蹤。

可以透過呼叫 JITDylib::clear 從 JITDylib 中移除所有程式碼。這會使已清除的 JITDylib 處於空但可用的狀態。

可以透過呼叫 ExecutionSession::removeJITDylib 來移除 JITDylib。這會清除 JITDylib,然後將其置於失效狀態。無法再對 JITDylib 執行任何操作,並且一旦最後一個指向它的控制代碼被釋放,它就會被銷毀。

您可以在 llvm/examples/OrcV2Examples/LLJITRemovableCode 中找到如何使用資源管理 API 的範例。

如何新增對自定義程式表示法的支援

為了新增對自定義程式表示法的支援,需要一個自定義的 MaterializationUnit 來表示程式,以及一個自定義的 Layer。Layer 將會有兩個操作:addemitadd 操作會接收一個程式表示法的實例,建立一個自定義的 MaterializationUnits 來存放它,然後將其加入到 JITDylib 中。emit 操作會接收一個 MaterializationResponsibility 物件和一個程式表示法的實例,並將其具體化,通常是透過編譯它並將產生的物件交給 ObjectLinkingLayer

自定義的 MaterializationUnit 將會有兩個操作:materializediscard。當任何由單元提供的符號被查詢時,會呼叫 materialize 函式,它應該只呼叫 Layer 上的 emit 函式,並傳入給定的 MaterializationResponsibility 和包裝的程式表示法。如果由單元提供的某些弱符號不需要(因為 JIT 找到了覆蓋的定義),則會呼叫 discard 函式。您可以使用它來提前捨棄定義,或者忽略它並讓連結器稍後捨棄定義。

以下是 ASTLayer 的範例

// ... In you JIT class
AstLayer astLayer;
// ...


class AstMaterializationUnit : public orc::MaterializationUnit {
public:
  AstMaterializationUnit(AstLayer &l, Ast &ast)
  : llvm::orc::MaterializationUnit(l.getInterface(ast)), astLayer(l),
  ast(ast) {};

  llvm::StringRef getName() const override {
    return "AstMaterializationUnit";
  }

  void materialize(std::unique_ptr<orc::MaterializationResponsibility> r) override {
    astLayer.emit(std::move(r), ast);
  };

private:
  void discard(const llvm::orc::JITDylib &jd, const llvm::orc::SymbolStringPtr &sym) override {
    llvm_unreachable("functions are not overridable");
  }


  AstLayer &astLayer;
  Ast &ast;
};

class AstLayer {
  llvhm::orc::IRLayer &baseLayer;
  llvhm::orc::MangleAndInterner &mangler;

public:
  AstLayer(llvm::orc::IRLayer &baseLayer, llvm::orc::MangleAndInterner &mangler)
  : baseLayer(baseLayer), mangler(mangler){};

  llvm::Error add(llvm::orc::ResourceTrackerSP &rt, Ast &ast) {
    return rt->getJITDylib().define(std::make_unique<AstMaterializationUnit>(*this, ast), rt);
  }

  void emit(std::unique_ptr<orc::MaterializationResponsibility> mr, Ast &ast) {
    // compileAst is just function that compiles the given AST and returns
    // a `llvm::orc::ThreadSafeModule`
    baseLayer.emit(std::move(mr), compileAst(ast));
  }

  llvm::orc::MaterializationUnit::Interface getInterface(Ast &ast) {
      SymbolFlagsMap Symbols;
      // Find all the symbols in the AST and for each of them
      // add it to the Symbols map.
      Symbols[mangler(someNameFromAST)] =
        JITSymbolFlags(JITSymbolFlags::Exported | JITSymbolFlags::Callable);
      return MaterializationUnit::Interface(std::move(Symbols), nullptr);
  }
};

如需完整的範例,請參閱 建構 JIT 第 4 章 的原始碼。

如何使用 ThreadSafeModule 和 ThreadSafeContext

ThreadSafeModule 和 ThreadSafeContext 分別是 Modules 和 LLVMContexts 的包裝器。ThreadSafeModule 是一對 std::unique_ptr<Module> 和一個(可能共享的)ThreadSafeContext 值。ThreadSafeContext 是一對 std::unique_ptr<LLVMContext> 和一個鎖。這種設計有兩個目的:為 LLVMContexts 提供鎖定機制和生命週期管理。可以鎖定 ThreadSafeContext 以防止使用相同 LLVMContext 的兩個模組意外地同時存取。一旦指向底層 LLVMContext 的所有 ThreadSafeContext 值都被銷毀,就會釋放它,從而在引用它的模組被銷毀後立即回收上下文記憶體。

ThreadSafeContexts 可以從 std::unique_ptr<LLVMContext> 顯式建構

ThreadSafeContext TSCtx(std::make_unique<LLVMContext>());

ThreadSafeModules 可以從一對 std::unique_ptr<Module> 和一個 ThreadSafeContext 值建構。ThreadSafeContext 值可以在多個 ThreadSafeModules 之間共享

ThreadSafeModule TSM1(
  std::make_unique<Module>("M1", *TSCtx.getContext()), TSCtx);

ThreadSafeModule TSM2(
  std::make_unique<Module>("M2", *TSCtx.getContext()), TSCtx);

在使用 ThreadSafeContext 之前,用戶端應該確保上下文只能在當前執行緒上存取,或者上下文已鎖定。在上面的範例中(從未鎖定上下文),我們依賴於 TSM1 和 TSM2 以及 TSCtx 都在一個執行緒上建立的事實。如果要在執行緒之間共享上下文,則必須先鎖定它,然後才能存取或建立附加到它的任何模組。例如:

ThreadSafeContext TSCtx(std::make_unique<LLVMContext>());

DefaultThreadPool TP(NumThreads);
JITStack J;

for (auto &ModulePath : ModulePaths) {
  TP.async(
    [&]() {
      auto Lock = TSCtx.getLock();
      auto M = loadModuleOnContext(ModulePath, TSCtx.getContext());
      J.addModule(ThreadSafeModule(std::move(M), TSCtx));
    });
}

TP.wait();

為了更容易管理對模組的獨佔存取,ThreadSafeModule 類別提供了一個便捷函數 withModuleDo,它隱式地 (1) 鎖定關聯的上下文,(2) 運行給定的函數物件,(3) 解鎖上下文,以及 (3) 返回函數物件生成的結果。例如:

ThreadSafeModule TSM = getModule(...);

// Dump the module:
size_t NumFunctionsInModule =
  TSM.withModuleDo(
    [](Module &M) { // <- Context locked before entering lambda.
      return M.size();
    } // <- Context unlocked after leaving.
  );

希望最大限度地提高並行編譯可能性的用戶端將希望在新 ThreadSafeContext 上建立每個新的 ThreadSafeModule。為此,提供了一個用於 ThreadSafeModule 的便捷建構函數,它從 std::unique_ptr<LLVMContext> 隱式建構一個新的 ThreadSafeContext 值

// Maximize concurrency opportunities by loading every module on a
// separate context.
for (const auto &IRPath : IRPaths) {
  auto Ctx = std::make_unique<LLVMContext>();
  auto M = std::make_unique<Module>("M", *Ctx);
  CompileLayer.add(MainJD, ThreadSafeModule(std::move(M), std::move(Ctx)));
}

計劃單執行緒運行的用戶端可以選擇透過載入相同上下文中的所有模組來節省記憶體

// Save memory by using one context for all Modules:
ThreadSafeContext TSCtx(std::make_unique<LLVMContext>());
for (const auto &IRPath : IRPaths) {
  ThreadSafeModule TSM(parsePath(IRPath, *TSCtx.getContext()), TSCtx);
  CompileLayer.add(MainJD, ThreadSafeModule(std::move(TSM));
}

如何將處理程序和程式庫符號添加到 JITDylibs

JIT 程式碼可能需要存取主程式或支援程式庫中的符號。實現此目的的最佳方法是將這些符號反映到您的 JITDylibs 中,以便它們看起來與執行階段中定義的任何其他符號相同(即,它們可透過 <cite>ExecutionSession::lookup> 找到,因此在連結期間對 JIT 連結器可見)。

反映外部符號的一種方法是使用 absoluteSymbols 函數手動添加它們

const DataLayout &DL = getDataLayout();
MangleAndInterner Mangle(ES, DL);

auto &JD = ES.createJITDylib("main");

JD.define(
  absoluteSymbols({
    { Mangle("puts"), ExecutorAddr::fromPtr(&puts)},
    { Mangle("gets"), ExecutorAddr::fromPtr(&getS)}
  }));

如果要反映的符號集很小且固定,則使用 absoluteSymbols 是合理的。另一方面,如果符號集很大或可變,則讓 *定義產生器* 按需為您添加定義可能更有意義。定義產生器是一個可以附加到 JITDylib 的物件,每當在該 JITDylib 中查找失敗時接收回調找不到一個或多個符號。在查找繼續之前,定義產生器有機會生成缺少符號的定義。

ORC 提供了 <code class="docutils literal notranslate"><span class="pre">DynamicLibrarySearchGenerator</span></code> 工具,用於從處理程序(或特定動態程式庫)中反映符號。例如,要反映執行階段程式庫的整個介面

const DataLayout &DL = getDataLayout();
auto &JD = ES.createJITDylib("main");

if (auto DLSGOrErr =
    DynamicLibrarySearchGenerator::Load("/path/to/lib"
                                        DL.getGlobalPrefix()))
  JD.addGenerator(std::move(*DLSGOrErr);
else
  return DLSGOrErr.takeError();

// IR added to JD can now link against all symbols exported by the library
// at '/path/to/lib'.
CompileLayer.add(JD, loadModule(...));

<code class="docutils literal notranslate"><span class="pre">DynamicLibrarySearchGenerator</span></code> 工具也可以使用過濾函數建構,以限制可以反映的符號集。例如,公開主處理程序中允許的符號集

const DataLayout &DL = getDataLayout();
MangleAndInterner Mangle(ES, DL);

auto &JD = ES.createJITDylib("main");

DenseSet<SymbolStringPtr> AllowList({
    Mangle("puts"),
    Mangle("gets")
  });

// Use GetForCurrentProcess with a predicate function that checks the
// allowed list.
JD.addGenerator(cantFail(DynamicLibrarySearchGenerator::GetForCurrentProcess(
      DL.getGlobalPrefix(),
      [&](const SymbolStringPtr &S) { return AllowList.count(S); })));

// IR added to JD can now link against any symbols exported by the process
// and contained in the list.
CompileLayer.add(JD, loadModule(...));

雖然您可以將程序或程式庫符號的引用使用符號的原始地址硬編碼到您的 IR 或目標文件中,但應該優先使用 JIT 符號表進行符號解析:它可以保持 IR 和目標文件的可讀性,並可在後續的 JIT 會話中重複使用。硬編碼的地址難以閱讀,而且通常只對一個會話有效。

發展藍圖

ORC 仍在積極開發中。以下列出了一些目前和未來的發展方向。

目前的工作

  1. TargetProcessControl:改進對程序外執行的內建支援

    TargetProcessControl API 提供了對 JIT 目標程序(將執行 JIT 編譯程式碼的程序)的各種操作,包括記憶體分配、記憶體寫入、函式執行和程序查詢(例如,目標三元組)。通過以這個 API 為目標,可以開發新的元件,這些元件在程序內和程序外 JIT 編譯中都能同樣出色地工作。

  2. 基於 ORC RPC 的 TargetProcessControl 實作

    目前正在開發基於 ORC RPC 的 TargetProcessControl API 實作,以便通過文件描述符/套接字輕鬆進行程序外 JIT 編譯。

  3. 核心狀態機清理

    核心 ORC 狀態機目前在 JITDylib 和 ExecutionSession 之間實作。方法正在逐漸移至 ExecutionSession。這將整理程式碼庫,並允許我們支援非同步移除 JITDylibs(實際上是在 ExecutionSession 中刪除關聯的狀態物件,並將 JITDylib 實例保留在失效狀態,直到所有對它的引用都被釋放)。

近期工作

  1. ORC JIT 執行時期程式庫

    我們需要一個用於 JIT 編譯程式碼的執行時期程式庫。這將包括 TLS 註冊、重入函式、語言執行時期(例如 Objective C 和 Swift)的註冊程式碼以及其他特定於 JIT 的執行時期程式碼。這應該以類似於 compiler-rt 的方式構建(甚至可能作為它的一部分)。

  2. 遠端 jit_dlopen / jit_dlclose

    為了更完整地模擬靜態程式運行的環境,我們希望 JIT 編譯的程式碼能夠“dlopen”和“dlclose”JITDylibs,並在當前執行緒上運行它們的所有初始化器/解構函式。這需要上述執行時期程式庫的支援。

  3. 除錯支援

    當使用 RuntimeDyld 作為底層 JIT 連結器時,ORC 目前支援 GDBRegistrationListener API。我們將需要為基於 JITLink 的平台提供新的解決方案。

未來工作

  1. 推測式編譯

    ORC 對並發編譯的支援使我們能夠輕鬆啟用*推測性* JIT 編譯:編譯尚未需要但我們有理由相信將來會需要的程式碼。這可以用於隱藏編譯延遲並提高 JIT 吞吐量。已經開發了一個使用 ORC 進行推測性編譯的概念驗證範例(請參閱 llvm/examples/SpeculativeJIT)。未來的這方面工作可能會側重於重複使用和改進現有的性能分析支援(目前由 PGO 使用)以提供推測決策,以及簡化推測性編譯使用的內建工具。