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)中。
在解释器和 REPL 中,例如 Cling(C++)和 Swift 解释器。
通过采用模块化、基于库的设计,我们的目标是使 ORC 在尽可能多的这些上下文中都能发挥作用。
特性¶
ORC 提供以下特性
- JIT 链接
ORC 提供 API 用于在运行时将可重定位目标文件(COFF、ELF、MachO)[1] 链接到目标进程。目标进程可以是包含 JIT 会话对象和 JIT 链接器的同一个进程,也可以是另一个进程(甚至是在不同机器或架构上运行的进程),该进程通过 RPC 与 JIT 通信。
- LLVM IR 编译
ORC 提供现成的组件(IRCompileLayer、SimpleCompiler、ConcurrentIRCompiler),使向 JIT 进程添加 LLVM IR 变得容易。
- 急切编译和延迟编译
默认情况下,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 原生(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
,它链接到一对动态库: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”dylib 的一部分,并且 main dylib 在 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 链接器(如果支持定义的程序表示是目标文件),或者甚至可能是一个直接将位写入内存的类(例如,如果定义是存根)。物化是生成可以安全调用或访问的符号定义所需的所有操作(编译、链接、填充位、在运行时注册等)的总称。
每个物化器完成其工作后,它都会通知 JITDylib,后者又会通知正在等待新物化定义的任何查询对象。每个查询对象都维护一个它仍在等待的符号数量的计数,并且一旦此计数达到零,查询对象就会使用SymbolMap(一个符号名称到地址的映射)来描述结果,从而调用查询处理程序。如果任何符号未能物化,查询会立即使用错误调用查询处理程序。
收集到的物化单元被发送到 ExecutionSession 以进行分派,并且客户端可以设置分派行为。默认情况下,每个物化器都在调用线程上运行。客户端可以自由地创建新线程来运行物化器,或将工作发送到线程池的工作队列(这是 LLJIT/LLLazyJIT 所做的)。
顶级 API¶
许多 ORC 的顶级 API 在上面的示例中可见
ExecutionSession 表示 JIT 程序并为 JIT 提供上下文:它包含 JITDylibs、错误报告机制,并分派物化器。
JITDylibs 提供符号表。
层(ObjLinkingLayer 和 CXXLayer)是编译器的包装器,允许客户端将这些编译器支持的未编译程序表示添加到 JITDylibs 中。
资源跟踪器允许您移除代码。
还使用了其他几个重要的 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 可以通过在 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 } }
});
重新导出实用程序可以通过从其他几个 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 } }
}));
如何在 LLJIT 类中使用 lazyReexports 的完整示例可以在 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
确定定义驻留的位置,层确定定义将如何编译。JITDylibs
之间的链接关系决定如何解析模块间引用,并且不再使用符号解析器。有关更多详细信息,请参见 设计概述 部分。除非需要多个 JITDylib 来模拟链接关系,否则 ORCv1 客户端应将所有代码放在单个 JITDylib 中。MCJIT 客户端应使用 LLJIT(请参阅 LLJIT 和 LLLazyJIT),并且可以将代码放在 LLJIT 创建的默认主 JITDylib 中(请参阅
LLJIT::getMainJITDylib()
)。所有 JIT 栈现在都需要一个
ExecutionSession
实例。ExecutionSession 管理字符串池、错误报告、同步和符号查找。ORCv2 使用唯一字符串(
SymbolStringPtr
实例)而不是字符串值,以减少内存开销并提高查找性能。请参阅 如何管理符号字符串 小节。IR 层需要 ThreadSafeModule 实例,而不是 std::unique_ptr<Module>。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
操作接收程序表示的实例,构建一个自定义的MaterializationUnits
来保存它,然后将其添加到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 分别是 Module 和 LLVMContexts 的包装器。ThreadSafeModule 是一对 std::unique_ptr<Module> 和一个(可能共享的)ThreadSafeContext 值。ThreadSafeContext 是一对 std::unique_ptr<LLVMContext> 和一个锁。这种设计有两个目的:为 LLVMContexts 提供锁定方案和生命周期管理。可以锁定 ThreadSafeContext 以防止两个使用相同 LLVMContext 的 Module 意外并发访问。一旦指向它的所有 ThreadSafeContext 值都被销毁,底层的 LLVMContext 就会被释放,允许在引用它的 Module 被销毁后立即回收上下文内存。
可以从 std::unique_ptr<LLVMContext> 显式构造 ThreadSafeContexts
ThreadSafeContext TSCtx(std::make_unique<LLVMContext>());
可以从 std::unique_ptr<Module> 和 ThreadSafeContext 值对构造 ThreadSafeModules。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 都在一个线程上创建的事实。如果上下文将在线程之间共享,则必须在访问或创建任何附加到它的 Module 之前锁定它。例如:
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();
为了使对 Module 的独占访问更容易管理,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 会话中保持可读和可重用。硬编码地址难以阅读,通常只适用于一个会话。
路线图¶
ORC 仍在积极开发中。下面列出了一些当前和未来的工作。
当前工作¶
TargetProcessControl:改进对进程外执行的树内支持
TargetProcessControl
API 提供了对 JIT 目标进程(将执行 JIT 代码的进程)的各种操作,包括内存分配、内存写入、函数执行和进程查询(例如,用于目标三元组)。通过定位此 API,可以开发新的组件,这些组件对于进程内和进程外 JIT 都同样有效。基于 ORC RPC 的 TargetProcessControl 实现
目前正在开发基于 ORC RPC 的
TargetProcessControl
API 实现,以通过文件描述符/套接字轻松实现进程外 JIT。核心状态机清理
核心 ORC 状态机当前在 JITDylib 和 ExecutionSession 之间实现。方法正在缓慢地转移到ExecutionSession。这将整理代码库,并且还允许我们支持异步移除 JITDylibs(实际上是在 ExecutionSession 中删除关联的状态对象,并将 JITDylib 实例保留在失效状态,直到所有对它的引用都被释放)。
近期工作¶
ORC JIT 运行时库
我们需要一个用于 JIT 代码的运行时库。这将包括 TLS 注册、重新进入函数、语言运行时(例如 Objective C 和 Swift)的注册代码以及其他 JIT 特定的运行时代码。这应该以类似于 compiler-rt 的方式构建(甚至可能作为其一部分)。
远程 jit_dlopen / jit_dlclose
为了更全面地模拟静态程序运行的环境,我们希望 JIT 代码能够“dlopen”和“dlclose”JITDylibs,在当前线程上运行所有初始值设定项/去初始化程序。这需要上面描述的运行时库的支持。
调试支持
ORC 当前在使用 RuntimeDyld 作为底层 JIT 链接器时支持 GDBRegistrationListener API。我们将需要一个用于基于 JITLink 的平台的新解决方案。
更远的未来工作¶
推测性编译
ORC 对并发编译的支持使我们能够轻松启用推测性 JIT 编译:编译尚未需要的代码,但我们有理由相信将来会需要。这可以用来隐藏编译延迟并提高 JIT 吞吐量。已经开发了一个使用 ORC 进行推测性编译的概念验证示例(请参阅
llvm/examples/SpeculativeJIT
)。未来在这方面的工作可能会集中在重用和改进现有的分析支持(目前由 PGO 使用)以提供推测决策,以及内置工具以简化推测性编译的使用。