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。

    1. #define LLVM_EXECUTIONENGINE_ORC_KALEIDOSCOPEJIT_H
    2. #include "llvm/ADT/STLExtras.h"
    3. #include "llvm/ExecutionEngine/ExecutionEngine.h"
    4. #include "llvm/ExecutionEngine/JITSymbol.h"
    5. #include "llvm/ExecutionEngine/RTDyldMemoryManager.h"
    6. #include "llvm/ExecutionEngine/SectionMemoryManager.h"
    7. #include "llvm/ExecutionEngine/Orc/CompileUtils.h"
    8. #include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
    9. #include "llvm/ExecutionEngine/Orc/LambdaResolver.h"
    10. #include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
    11. #include "llvm/IR/DataLayout.h"
    12. #include "llvm/IR/Mangler.h"
    13. #include "llvm/Support/DynamicLibrary.h"
    14. #include "llvm/Target/TargetMachine.h"
    15. #include <algorithm>
    16. #include <memory>
    17. #include <string>
    18. namespace llvm {
    19. namespace orc {
    20. class KaleidoscopeJIT {
    21. private:
    22. std::unique_ptr<TargetMachine> TM;
    23. const DataLayout DL;
    24. RTDyldObjectLinkingLayer ObjectLayer;
    25. IRCompileLayer<decltype(ObjectLayer), SimpleCompiler> CompileLayer;
    26. public:
    27. 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方法,但是当传递空指针时,它将“加载”主机进程本身,使其导出的符号可用于执行。

    1. ModuleHandle addModule(std::unique_ptr<Module> M) {
    2. // Build our symbol resolver:
    3. // Lambda 1: Look back into the JIT itself to find symbols that are part of
    4. // the same "logical dylib".
    5. // Lambda 2: Search for external symbols in the host process.
    6. if (auto Sym = CompileLayer.findSymbol(Name, false))
    7. return Sym;
    8. return JITSymbol(nullptr);
    9. },
    10. [](const std::string &Name) {
    11. if (auto SymAddr =
    12. RTDyldMemoryManager::getSymbolAddressInProcess(Name))
    13. return JITSymbol(SymAddr, JITSymbolFlags::Exported);
    14. return JITSymbol(nullptr);
    15. });
    16. // Add the set to the JIT with the resolver we created above and a newly
    17. // created SectionMemoryManager.
    18. return cantFail(CompileLayer.addModule(std::move(M),
    19. std::move(Resolver)));
    20. }

    既然我们可以向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层概念。

    下一步:扩展KaleidoscopeJIT

    以下是我们正在运行的示例的完整代码清单。要构建此示例,请使用:

    1. # Compile
    2. clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy
    3. # 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必须能够返回此失败。