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 基础设施内使用 LLVM 优化的、高性能 JIT(例如 JVM、Julia)中。
在解释器和 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 进程中。
- 预先编译和延迟编译
默认情况下,ORC 将在符号在 JIT 会话对象(
ExecutionSession
)中被查找时立即编译它们。默认情况下急切编译使得将 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 原始(pre-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,包括由普通静态编译器(例如 clang)生成的 IR,该编译器使用符号链接和可见性以及弱[3]和公共符号定义等构造。
要了解这是如何工作的,请想象一个程序 foo
,它链接到一对动态库:libA
和 libB
。在命令行中,构建此程序可能如下所示:
$ 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 反过来通知任何正在等待新物化定义的查询对象。每个查询对象维护一个它仍在等待的符号数量的计数,一旦此计数达到零,查询对象就会使用描述结果的 SymbolMap(符号名称到地址的映射)调用查询处理程序。如果任何符号物化失败,查询会立即使用错误调用查询处理程序。
收集的物化单元被发送到 ExecutionSession 进行调度,调度行为可以由客户端设置。默认情况下,每个物化器都在调用线程上运行。客户端可以自由创建新线程来运行物化器,或者将工作发送到线程池的工作队列(这就是 LLJIT/LLLazyJIT 所做的)。
顶层 API¶
许多 ORC 的顶层 API 在上面的示例中可见:
ExecutionSession 代表 JIT 程序并为 JIT 提供上下文:它包含 JITDylib、错误报告机制并调度物化器。
JITDylib 提供符号表。
层(ObjLinkingLayer 和 CXXLayer)是编译器周围的包装器,允许客户端将这些编译器支持的未编译程序表示添加到 JITDylib。
ResourceTracker 允许您移除代码。
明确使用了其他几个重要的 API。JIT 客户端不必知道它们,但层作者将使用它们:
MaterializationUnit - 当 XXXLayer::add 被调用时,它将给定的程序表示(在本例中为 C++ 源代码)包装在 MaterializationUnit 中,然后将其存储在 JITDylib 中。MaterializationUnit 负责描述它们提供的定义,并在需要编译时解开程序表示并将其传递回层(这种所有权洗牌使编写线程安全层变得更容易,因为程序表示的所有权将传递回堆栈,而不是必须从需要同步的层成员中捞取出来)。
MaterializationResponsibility - 当 MaterializationUnit 将程序表示传递回层时,它会附带一个关联的 MaterializationResponsibility 对象。此对象跟踪必须物化的定义,并提供一种在它们成功物化或发生故障时通知 JITDylib 的方法。
绝对符号、别名和重新导出¶
ORC 可以轻松地定义具有绝对地址的符号,或只是其他符号的别名的符号。
绝对符号¶
绝对符号是直接映射到地址而无需进一步物化的符号,例如:“foo” = 0x1234。绝对符号的一个用例是允许解析进程符号。例如:
JD.define(absoluteSymbols(SymbolMap({
{ Mangle("printf"),
{ ExecutorAddr::fromPtr(&printf),
JITSymbolFlags::Callable } }
});
通过建立此映射,添加到 JIT 的代码可以符号方式引用 printf,而无需将 printf 的地址“烘焙”到代码中。反过来,这允许跨 JIT 会话重用 JIT 代码的缓存版本(例如,编译后的对象),因为 JIT 代码不再更改,只有绝对符号定义会更改。
对于进程和库符号,可以使用 DynamicLibrarySearchGenerator 实用程序(请参阅如何将进程和库符号添加到 JITDylib)来自动为您构建绝对符号映射。但是,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_fast
和 log_detailed
。您的 JIT 可以选择当 log
符号被引用时将使用哪个定义,方法是在 JIT 启动时设置别名:
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 } }
});
重新导出实用程序可以方便地通过从其他几个 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 之间的一些设计差异:
ORCv2 完全采用了从 MCJIT 开始的 JIT 即链接器模型。模块(和其他程序表示,例如目标文件)不再直接添加到 JIT 类或层。相反,它们由层添加到
JITDylib
实例。JITDylib
确定定义驻留在哪里,层确定定义将如何编译。JITDylib
之间的链接关系决定了模块间引用如何解析,并且不再使用符号解析器。有关更多详细信息,请参阅设计概述部分。除非需要多个 JITDylib 来建模链接关系,否则 ORCv1 客户端应将所有代码放在单个 JITDylib 中。MCJIT 客户端应使用 LLJIT(请参阅LLJIT 和 LLLazyJIT),并且可以将代码放在 LLJIT 默认创建的主 JITDylib 中(请参阅
LLJIT::getMainJITDylib()
)。所有 JIT 堆栈现在都需要一个
ExecutionSession
实例。ExecutionSession 管理字符串池、错误报告、同步和符号查找。ORCv2 使用唯一字符串(
SymbolStringPtr
实例)而不是字符串值,以减少内存开销并提高查找性能。请参阅如何管理符号字符串小节。IR 层需要 ThreadSafeModule 实例,而不是 std::unique_ptr<Module>s。ThreadSafeModule 是一个包装器,用于确保不并发访问使用相同 LLVMContext 的模块。请参阅如何使用 ThreadSafeModule 和 ThreadSafeContext。
符号查找不再由层处理。相反,JITDylib 上有一个
lookup
方法,该方法接受要扫描的 JITDylib 列表。ExecutionSession ES; JITDylib &JD1 = ...; JITDylib &JD2 = ...; auto Sym = ES.lookup({&JD1, &JD2}, ES.intern("_main"));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"));
如何创建 JITDylib 并设置链接关系¶
在 ORC 中,所有符号定义都驻留在 JITDylib 中。JITDylib 通过调用带有唯一名称的 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 执行任何操作,并且一旦释放对它的最后一个句柄,它将被销毁。
有关如何使用资源管理 API 的示例,请参见 llvm/examples/OrcV2Examples/LLJITRemovableCode
。
如何添加对自定义程序表示的支持¶
为了添加对自定义程序表示的支持,需要自定义的程序表示 MaterializationUnit
和自定义的 Layer
。Layer 将具有两个操作:add
和 emit
。add
操作接受程序表示的实例,构建自定义的 MaterializationUnit
之一以保存它,然后将其添加到 JITDylib
。emit
操作接受 MaterializationResponsibility
对象和程序表示的实例,并对其进行物化,通常通过编译它并将结果对象传递给 ObjectLinkingLayer
。
您的自定义 MaterializationUnit
将具有两个操作:materialize
和 discard
。当查找单元提供的任何符号时,将为您调用 materialize
函数,它应该只调用您层上的 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 * }; 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 的两个模块意外并发访问。一旦指向它的所有 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 之前,客户端应确保上下文仅在当前线程上可访问,或者上下文已锁定。在上面的示例中(上下文永远不会被锁定),我们依赖于 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)); }
如何将进程和库符号添加到 JITDylib¶
JIT 代码可能需要访问主机程序或支持库中的符号。启用此功能的最佳方法是将这些符号反映到您的 JITDylib 中,以便它们看起来与执行会话中定义的任何其他符号相同(即,它们可以通过 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 会话中保持可读和可重用性。硬编码的地址难以阅读,通常只适用于一个会话。
路线图¶
ORC 仍在积极开发中。下面列出了一些当前和未来的工作。
当前工作¶
TargetProcessControl:改进对进程外执行的内联支持
TargetProcessControl
API 提供了对 JIT 目标进程(将执行 JIT 代码的进程)的各种操作,包括内存分配、内存写入、函数执行和进程查询(例如,针对目标三元组)。通过以这个 API 为目标,可以开发新的组件,这些组件对于进程内和进程外 JIT 都能同样良好地工作。基于 ORC RPC 的 TargetProcessControl 实现
基于 ORC RPC 的
TargetProcessControl
API 实现目前正在开发中,以通过文件描述符/套接字轻松实现进程外 JIT。核心状态机清理
核心 ORC 状态机目前在 JITDylib 和 ExecutionSession 之间实现。方法正在慢慢地移动到 ExecutionSession。这将整理代码库,并允许我们支持异步删除 JITDylib(实际上是删除 ExecutionSession 中的关联状态对象,并使 JITDylib 实例处于失效状态,直到所有对它的引用都被释放)。
近期未来工作¶
ORC JIT 运行时库
我们需要一个用于 JIT 代码的运行时库。这将包括 TLS 注册、重入函数、语言运行时(例如 Objective-C 和 Swift)的注册代码以及其他 JIT 特定的运行时代码。这应该以类似于 compiler-rt 的方式构建(甚至可能作为其一部分)。
远程 jit_dlopen / jit_dlclose
为了更充分地模拟静态程序运行的环境,我们希望 JIT 代码能够“dlopen”和“dlclose” JITDylib,并在当前线程上运行它们的所有初始化器/反初始化器。这将需要上述运行时库的支持。
调试支持
当使用 RuntimeDyld 作为底层 JIT 链接器时,ORC 当前支持 GDBRegistrationListener API。我们将需要基于 JITLink 平台的新解决方案。
更遥远的未来工作¶
推测性编译
ORC 对并发编译的支持使我们能够轻松地启用推测性 JIT 编译:编译尚不需要但我们有理由相信将来需要的代码。这可以用于隐藏编译延迟并提高 JIT 吞吐量。使用 ORC 进行推测性编译的概念验证示例已经开发出来(参见
llvm/examples/SpeculativeJIT
)。未来的工作可能侧重于重用和改进现有的分析支持(目前由 PGO 使用)以支持推测决策,以及内置工具以简化推测性编译的使用。