ORC 設計與實作

簡介

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

使用案例

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

1. LLVM 教學課程使用基於 ORC 的簡單 JIT 類別,來執行從玩具語言 Kaleidoscope 編譯的表達式。

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

3. 在高效能 JIT(例如 JVM、Julia)中,它們希望在現有的 JIT 基礎架構內使用 LLVM 的最佳化。

  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 處理的程序中。

Eager 與 lazy 編譯

預設情況下,ORC 會在符號於 JIT 會期物件(ExecutionSession)中被查找時立即編譯符號。預設採用 eager 編譯,可以輕鬆地將 ORC 用作現有 JIT 的記憶體內編譯器(類似於 MCJIT 的常用方式)。然而,ORC 也透過 lazy-reexports 提供對 lazy 編譯的內建支援(請參閱 延遲載入)。

支援自訂編譯器與程式表示法

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

並行 JIT 程式碼並行編譯

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

可移除的程式碼

JIT 程式表示法的資源

正交性可組合性

上述每個特性都可以獨立使用。可以將 ORC 組件組合在一起,以建立非 lazy、in-process、單執行緒的 JIT,或是 lazy、out-of-process、並行的 JIT,或介於兩者之間的任何組合。

LLJIT 與 LLLazyJIT

ORC 提供兩個現成的基本 JIT 類別。它們既可以用作如何組裝 ORC 組件以建立 JIT 的範例,也可以用來取代早期的 LLVM JIT API(例如 MCJIT)。

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

LLLazyJIT 擴展了 LLJIT 並新增了 CompileOnDemandLayer,以啟用 LLVM IR 的 lazy 編譯。當透過 addLazyIRModule 方法新增 LLVM IR 模組時,該模組中的函式主體將在首次呼叫之前不會被編譯。LLLazyJIT 旨在提供 LLVM 原始 (pre-MCJIT) JIT API 的替代品。

LLJIT 和 LLLazyJIT 實例可以使用它們各自的 builder 類別來建立: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();

builder 類別提供了許多組態選項,可以在建構 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,包括由普通靜態編譯器(例如 clang)產生的 IR,該編譯器使用符號連結和可見性,以及 weak [3] 和 common 符號定義等結構。

為了了解其運作方式,想像一個程式 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" dylib 的一部分,並且 main dylib 在 LibB 之前連結到 LibA。

許多 JIT 客戶將不需要嚴格遵守通常的 ahead-of-time 連結規則,並且應該能夠透過將所有程式碼放在單個 JITDylib 中來順利運作。然而,想要為傳統上依賴 ahead-of-time 連結的語言/專案(例如 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)

在此上下文中,materializer 是指在請求時提供符號的工作定義的東西。通常,materializer 只是編譯器的包裝器,但它們也可能直接包裝 jit-linker(如果支援定義的程式表示法是物件檔案),甚至可能是一個直接將位元寫入記憶體的類別(例如,如果定義是 stub)。Materialization 是涵蓋任何操作的總稱(編譯、連結、splatting 位元、向運行時註冊等),這些操作是產生可安全呼叫或存取的符號定義所必需的。

當每個 materializer 完成其工作時,它會通知 JITDylib,而 JITDylib 又會通知任何正在等待新 materialization 定義的查詢物件。每個查詢物件都維護一個它仍在等待的符號數計數,一旦此計數達到零,查詢物件就會呼叫查詢處理器,並提供一個描述結果的 SymbolMap(符號名稱到位址的映射)。如果任何符號 materialization 失敗,查詢會立即使用錯誤呼叫查詢處理器。

收集的 materialization unit 會傳送到 ExecutionSession 進行調度,並且調度行為可以由客戶設定。預設情況下,每個 materializer 都在呼叫執行緒上運行。客戶可以自由建立新的執行緒來運行 materializer,或將工作傳送到執行緒池的工作佇列(這就是 LLJIT/LLLazyJIT 所做的事情)。

頂層 API

許多 ORC 的頂層 API 在上面的範例中是可見的:

  • ExecutionSession 代表 JIT 程式並為 JIT 提供上下文:它包含 JITDylibs、錯誤報告機制,並調度 materializer。

  • JITDylibs 提供符號表。

  • Layers (ObjLinkingLayer 和 CXXLayer) 是編譯器的包裝器,允許客戶將這些編譯器支援的未編譯程式表示法新增到 JITDylibs。

  • ResourceTrackers 允許您移除程式碼。

還有幾個其他重要的 API 被明確使用。JIT 客戶無需了解它們,但 Layer 作者將會使用它們:

  • MaterializationUnit - 當 XXXLayer::add 被調用時,它將給定的程式表示法(在本範例中為 C++ 原始碼)包裝在 MaterializationUnit 中,然後將其儲存在 JITDylib 中。MaterializationUnit 負責描述它們提供的定義,並負責解包程式表示法,並在需要編譯時將其傳遞回 layer(這種所有權 shuffle 使編寫執行緒安全的 layer 變得更容易,因為程式表示法的所有權將在堆疊上傳遞回來,而不是必須從 Layer 成員中撈取,那樣將需要同步)。

  • MaterializationResponsibility - 當 MaterializationUnit 將程式表示法交還給 layer 時,它會帶有一個相關聯的 MaterializationResponsibility 物件。此物件追蹤必須 materialization 的定義,並提供一種在它們成功 materialization 或發生失敗時通知 JITDylib 的方法。

絕對符號、別名與重新匯出

ORC 可以輕鬆定義具有絕對位址的符號,或僅僅是其他符號別名的符號:

絕對符號

絕對符號是直接映射到位址而無需進一步 materialization 的符號,例如:"foo" = 0x1234。絕對符號的一個使用案例是允許解析程序符號。例如:

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

透過建立此映射,新增到 JIT 的程式碼可以符號方式引用 printf,而無需將 printf 的位址 "baked in"。反過來,這允許跨 JIT 會期重用 JIT 程式碼的快取版本(例如,編譯的物件),因為 JIT 程式碼不再變更,只有絕對符號定義會變更。

對於程序和函式庫符號,DynamicLibrarySearchGenerator 工具(請參閱 如何將程序和函式庫符號新增到 JITDylibs)可用於自動為您建構絕對符號映射。但是,absoluteSymbols 函式仍然適用於使 JIT 中的非全域物件對 JIT 程式碼可見。例如,想像一下您的 JIT 標準函式庫需要存取您的 JIT 物件才能進行一些呼叫。我們可以將物件的位址 baked 到函式庫中,但這樣每個會期都需要重新編譯:

// 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 中的延遲載入由一個名為 "lazy reexports" 的工具提供。lazy reexport 類似於常規 reexport 或別名:它為現有符號提供一個新名稱。然而,與常規 reexport 不同的是,lazy reexport 的查找不會立即觸發重新匯出符號的 materialization。相反,它們只會觸發函式 stub 的 materialization。此函式 stub 初始化為指向一個 lazy call-through,它提供重新進入 JIT 的途徑。如果在運行時呼叫 stub,則 lazy call-through 將查找重新匯出的符號(如有必要,觸發其 materialization),更新 stub(以便在後續呼叫中直接呼叫重新匯出的符號),然後透過重新匯出的符號返回。透過重複使用現有的符號查找機制,lazy reexport 繼承了相同的並行保證:可以從多個執行緒並行呼叫 lazy reexport,並且重新匯出的符號可以是任何編譯狀態(未編譯、正在編譯中或已編譯),並且呼叫將會成功。這允許延遲載入與遠端編譯、並行編譯、並行 JIT 程式碼和推測編譯等特性安全地混合使用。

常規 reexport 和 lazy reexport 之間還有一個重要的區別,某些客戶必須注意:lazy reexport 的位址將不同於重新匯出符號的位址(而常規 reexport 保證具有與重新匯出符號相同的位址)。關心指標相等性的客戶通常希望使用 reexport 的位址作為重新匯出符號的規範位址。這將允許在不強制 materialization reexport 的情況下取得位址。

使用範例:

如果 JITDylib JD 包含符號 foo_bodybar_body 的定義,我們可以透過呼叫以下程式碼在 JITDylib JD2 中建立 lazy entry points FooBar

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 } }
                }));

如何在 LLJIT 類別中使用 lazyReexports 的完整範例可以在 llvm/examples/OrcV2Examples/LLJITWithLazyReexports 中找到。

支援自訂編譯器

待辦。

從 ORCv1 轉換到 ORCv2

自 LLVM 7.0 以來,新的 ORC 開發工作已集中在新增對並行 JIT 編譯的支援。支援並行的新 API(包括新的 layer 介面和實作,以及新的工具程式)統稱為 ORCv2,而原始的非並行 layer 和工具程式現在稱為 ORCv1。

大多數 ORCv1 layer 和工具程式在 LLVM 8.0 中都以 'Legacy' 前綴重新命名,並且在 LLVM 9.0 中附加了棄用警告。在 LLVM 12.0 中,ORCv1 將被完全移除。

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

  1. ORCv2 完全採用了從 MCJIT 開始的 JIT-as-linker 模型。模組(和其他程式表示法,例如物件檔案)不再直接新增到 JIT 類別或 layer。相反,它們 layer 新增到 JITDylib 實例。 JITDylib 決定定義駐留在何處,layer 決定定義將如何編譯。 JITDylibs 之間的連結關係決定了模組間參考如何解析,並且不再使用符號解析器。有關更多詳細資訊,請參閱 設計概觀 章節。

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

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

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

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

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

    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 名稱執行查找,您還需要先套用平台連結器 mangling,然後再 interning 字串。在 Linux 上,此 mangling 是 no-op,但在其他平台上,它通常涉及在字串中新增前綴(例如 Darwin 上的 '_')。mangling 方案基於目標的 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 的預設 resource tracker 追蹤。

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

可以透過呼叫 ExecutionSession::removeJITDylib 來移除 JITDylibs。這會清除 JITDylib,然後使其進入失效狀態。無法對 JITDylib 執行進一步的操作,並且一旦最後一個 handle 釋放,它將被銷毀。

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

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

為了新增對自訂程式表示法的支援,需要自訂的 MaterializationUnit 用於程式表示法,以及自訂的 Layer。Layer 將具有兩個操作:addemitadd 操作接受程式表示法的實例,建構一個自訂的 MaterializationUnit 來保存它,然後將其新增到 JITDylib。emit 操作接受 MaterializationResponsibility 物件和程式表示法的實例,並 materialization 它,通常是透過編譯它並將產生的物件交付給 ObjectLinkingLayer

您的自訂 MaterializationUnit 將具有兩個操作:materializediscard。當查找由 unit 提供的任何符號時,將為您呼叫 materialize 函式,它應該只呼叫 layer 上的 emit 函式,並傳入給定的 MaterializationResponsibility 和包裝的程式表示法。如果您的 unit 提供的某些 weak 符號不需要(因為 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);
  }
};

請查看 Building A JIT’s Chapter 4 的原始碼以取得完整範例。

如何使用 ThreadSafeModule 與 ThreadSafeContext

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

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 之前,客戶應確保上下文僅在當前執行緒上可存取,或者上下文已鎖定。在上面的範例中(上下文從未鎖定),我們依賴於 TSM1TSM2 以及 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 中,以便它們看起來與執行會期中定義的任何其他符號相同(即,它們可以透過 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 提供 DynamicLibrarySearchGenerator 工具程式,用於為您反映來自程序(或特定動態函式庫)的符號。例如,若要反映運行時函式庫的整個介面:

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(...));

DynamicLibrarySearchGenerator 工具程式也可以使用篩選函式來建構,以限制可能反映的符號集。例如,若要公開主程序的允許符號集:

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 會期中保持可讀性和可重用性。硬編碼位址很難閱讀,並且通常只適用於一個會期。

Roadmap

ORC 仍在積極開發中。以下列出了一些目前和未來的工作:

目前工作

  1. TargetProcessControl:改進對 in-tree 支援 out-of-process 執行的支援

    TargetProcessControl API 提供對 JIT 目標程序(將執行 JIT 程式碼的程序)的各種操作,包括記憶體分配、記憶體寫入、函式執行和程序查詢(例如,目標三元組)。透過以這個 API 為目標,可以開發新的組件,這些組件對於 in-process 和 out-of-process JITing 都同樣有效。

  2. 基於 ORC RPC 的 TargetProcessControl 實作

    基於 ORC RPC 的 TargetProcessControl API 實作目前正在開發中,以透過檔案描述器/sockets 啟用輕鬆的 out-of-process JITing。

  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 使用),以提供推測決策的依據,以及內建工具來簡化推測性編譯的使用。