1.构建JIT:从KaleidoscopeJIT开始
- 第1章简介
- JIT API基础知识
- KaleidoscopeJIT
- 完整的代码清单
JIT编译器的目的是在需要时“即时”编译代码,而不是像传统编译器那样提前将整个程序编译到磁盘。为了支持这一目标,我们最初的,简单的JIT API将是:
处理addModule(Module&M) - 使给定的IR模块可用于执行。 JITSymbol findSymbol(const std :: string&Name) - 搜索已添加到JIT的符号(函数或变量)的指针。 void removeModule(Handle H) - 从JIT中删除一个模块,释放已用于编译代码的所有内存。 此API的基本用例,从模块执行’main’函数,如下所示:
我们在这些教程中构建的API都将是这个简单主题的变体。在API的背后,我们将优化JIT的实现,以增加对优化和延迟编译的支持。最终,我们将扩展API本身,以允许将更高级别的程序表示(例如AST)添加到JIT。
#define LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
#include "llvm/ADT/STLExtras.h"
#include "llvm/ExecutionEngine/ExecutionEngine.h"
#include "llvm/ExecutionEngine/JITSymbol.h"
#include "llvm/ExecutionEngine/RTDyldMemoryManager.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/LambdaResolver.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Mangler.h"
#include "llvm/Support/DynamicLibrary.h"
#include "llvm/Target/TargetMachine.h"
#include <algorithm>
#include <memory>
#include <string>
namespace llvm {
namespace orc {
class KaleidoscopeJIT {
private:
std::unique_ptr<TargetMachine> TM;
const DataLayout DL;
RTDyldObjectLinkingLayer ObjectLayer;
IRCompileLayer<decltype(ObjectLayer), SimpleCompiler> CompileLayer;
public:
using ModuleHandle = decltype(CompileLayer)::ModuleHandleT;
我们的类有四个成员:一个TargetMachine,TM,它将用于构建我们的LLVM编译器实例; DataLayout,DL,将用于符号修改(稍后将详细介绍)和两个ORC 层:RTDyldObjectLinkingLayer和CompileLayer。我们将在下一章中更多地讨论层,但是现在您可以将它们视为类似于LLVM Passes:它们将易用的组合接口背后的有用JIT实用程序包装起来。第一层ObjectLayer是我们JIT的基础:它接收由编译器生成的内存中的目标文件,并动态链接它们以使它们可执行。这个JIT-on-of-a-linker设计是在MCJIT中引入的,但链接器隐藏在MCJIT类中。在ORC中,我们公开链接器,以便客户端可以在需要时直接访问和配置它。在本教程中,我们的ObjectLayer将仅用于支持堆栈中的下一层:CompileLayer,它负责获取LLVM IR,编译它,
这就是成员变量,之后我们有一个typedef:ModuleHandle。这是将从我们的JIT的addModule方法返回的句柄类型,并且可以传递给removeModule方法以删除模块。IRCompileLayer类已经提供了一个方便的句柄类型(IRCompileLayer :: ModuleHandleT),所以我们只是将我们的ModuleHandle别名。
接下来我们有我们的类构造函数。我们首先使用EngineBuilder :: selectTarget辅助方法初始化TM,该方法为当前进程构造TargetMachine。然后我们使用新创建的TargetMachine初始化DL,我们的DataLayout。之后我们需要初始化ObjectLayer。ObjectLayer需要一个函数对象,它将为添加的每个模块构建一个JIT内存管理器(JIT内存管理器管理内存分配,内存权限和JIT代码的异常处理程序注册)。为此,我们使用lambda返回一个SectionMemoryManager,这是一个现成的实用程序,提供本章所需的所有基本内存管理功能。接下来我们初始化我们的CompileLayer。CompileLayer需要两件事:(1)对象层的引用,(2)用于执行从IR到目标文件的实际编译的编译器实例。我们现在使用现成的SimpleCompiler实例。最后,在构造函数的主体中,我们使用nullptr参数调用DynamicLibrary :: LoadLibraryPermanently方法。通常使用要加载的动态库的路径调用LoadLibraryPermanently方法,但是当传递空指针时,它将“加载”主机进程本身,使其导出的符号可用于执行。
ModuleHandle addModule(std::unique_ptr<Module> M) {
// Build our symbol resolver:
// Lambda 1: Look back into the JIT itself to find symbols that are part of
// the same "logical dylib".
// Lambda 2: Search for external symbols in the host process.
if (auto Sym = CompileLayer.findSymbol(Name, false))
return Sym;
return JITSymbol(nullptr);
},
[](const std::string &Name) {
if (auto SymAddr =
RTDyldMemoryManager::getSymbolAddressInProcess(Name))
return JITSymbol(SymAddr, JITSymbolFlags::Exported);
return JITSymbol(nullptr);
});
// Add the set to the JIT with the resolver we created above and a newly
// created SectionMemoryManager.
return cantFail(CompileLayer.addModule(std::move(M),
std::move(Resolver)));
}
既然我们可以向JIT添加代码,我们需要一种方法来查找我们添加到它的符号。要做到这一点,我们呼吁我们的CompileLayer的findSymbol方法,但有一个转折:我们必须裂伤我们正在寻找第一个符号的名称。ORC JIT组件在内部使用受损的符号,与静态编译器和链接器的使用方式相同,而不是使用纯IR符号名称。这允许JIT代码与应用程序或共享库中的预编译代码轻松互操作。修改的类型将取决于DataLayout,而DataLayout又取决于目标平台。为了让我们能够保持便携性并根据未损坏的名称进行搜索,我们自己重新制作了这个。
接下来我们有一个便利函数getSymbolAddress,它返回给定符号的地址。与CompileLayer的addModule函数一样,JITSymbol的getAddress函数被允许失败[4],但是我们知道它不会在我们的简单示例中,所以我们将它包装在对cantFail的调用中。
我们现在来到JIT API中的最后一个方法:removeModule。此方法负责销毁随给定模块添加的MemoryManager和SymbolResolver,从而释放它们在进程中使用的任何资源。在我们的Kaleidoscope演示中,我们依靠此方法来移除表示最新顶级表达式的模块,从而防止在输入下一个顶级表达式时将其视为重复定义。通常情况下,释放任何您不需要进一步调用的模块,只需释放专用的资源即可。但是,您并不需要这样做:当您的JIT类被破坏时,如果在此之前尚未释放它们,则将清除所有资源。喜欢 CompileLayer::addModule和JITSymbol::getAddress,removeModule一般可能会失败,但在我们的示例中永远不会失败,所以我们将它包装在对cantFail的调用中。
这将我们带到构建JIT的第1章的末尾。您现在拥有一个基本但功能齐全的JIT堆栈,您可以使用它来获取LLVM IR并使其在JIT进程的上下文中可执行。在下一章中,我们将介绍如何扩展此JIT以生成更高质量的代码,并在此过程中深入了解ORC层概念。
以下是我们正在运行的示例的完整代码清单。要构建此示例,请使用:
# Compile
clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy
# Run
- [1] 实际上,我们使用KaleidoscopeJIT的缩减版本,这是一个简化的假设:符号无法重新定义。这将使得无法在REPL中重新定义符号,但会使我们的符号查找逻辑更简单。重新引入对符号重新定义的支持留给读者练习。(原始教程中使用的KaleidoscopeJIT.h将是一个有用的参考)。
- [2]
文件 包含的原因 STLExtras.h 在使用STL时很有用的LLVM实用程序。 ExecutionEngine.h 访问EngineBuilder :: selectTarget方法。 RTDyldMemoryManager.h 访问RTDyldMemoryManager :: getSymbolAddressInProcess方法。 CompileUtils.h 提供SimpleCompiler类。 IRCompileLayer.h 提供IRCompileLayer类。 LambdaResolver.h 访问createLambdaResolver函数,该函数提供了符号解析器的简单构造。 RTDyldObjectLinkingLayer.h 提供RTDyldObjectLinkingLayer类。 Mangler.h 为平台特定的名称修改提供Mangler类。 DynamicLibrary.h 提供DynamicLibrary类,使主机进程中的符号可搜索。 raw_ostream.h 快速输出流类。我们使用raw_string_ostream子类进行符号修改 TargetMachine.h LLVM目标机器描述类。 - [4] JITSymbol::getAddress 将强制JIT编译符号的定义(如果尚未编译),并且由于编译过程可能失败,getAddress必须能够返回此失败。