| # Building a JIT: Adding Optimizations -- An introduction to ORC Layers | |
| **This tutorial is under active development. It is incomplete and details may | |
| change frequently.** Nonetheless we invite you to try it out as it stands, and | |
| we welcome any feedback. | |
| ## Chapter 2 Introduction | |
| **Warning: This tutorial is currently being updated to account for ORC API | |
| changes. Only Chapters 1 and 2 are up-to-date.** | |
| **Example code from Chapters 3 to 5 will compile and run, but has not been | |
| updated** | |
| Welcome to Chapter 2 of the "Building an ORC-based JIT in LLVM" tutorial. In | |
| [Chapter 1](BuildingAJIT1.md) of this series we examined a basic JIT | |
| class, KaleidoscopeJIT, that could take LLVM IR modules as input and produce | |
| executable code in memory. KaleidoscopeJIT was able to do this with relatively | |
| little code by composing two off-the-shelf *ORC layers*: IRCompileLayer and | |
| ObjectLinkingLayer, to do much of the heavy lifting. | |
| In this layer we'll learn more about the ORC layer concept by using a new layer, | |
| IRTransformLayer, to add IR optimization support to KaleidoscopeJIT. | |
| ## Optimizing Modules using the IRTransformLayer | |
| In [Chapter 4](LangImpl04.md) of the "Implementing a language with LLVM" | |
| tutorial series the llvm *FunctionPassManager* is introduced as a means for | |
| optimizing LLVM IR. Interested readers may read that chapter for details, but | |
| in short: to optimize a Module we create an llvm::FunctionPassManager | |
| instance, configure it with a set of optimizations, then run the PassManager on | |
| a Module to mutate it into a (hopefully) more optimized but semantically | |
| equivalent form. In the original tutorial series the FunctionPassManager was | |
| created outside the KaleidoscopeJIT and modules were optimized before being | |
| added to it. In this Chapter we will make optimization a phase of our JIT | |
| instead. For now this will provide us a motivation to learn more about ORC | |
| layers, but in the long term making optimization part of our JIT will yield an | |
| important benefit: When we begin lazily compiling code (i.e. deferring | |
| compilation of each function until the first time it's run) having | |
| optimization managed by our JIT will allow us to optimize lazily too, rather | |
| than having to do all our optimization up-front. | |
| To add optimization support to our JIT we will take the KaleidoscopeJIT from | |
| Chapter 1 and compose an ORC *IRTransformLayer* on top. We will look at how the | |
| IRTransformLayer works in more detail below, but the interface is simple: the | |
| constructor for this layer takes a reference to the execution session and the | |
| layer below (as all layers do) plus an *IR optimization function* that it will | |
| apply to each Module that is added via addModule: | |
| ```c++ | |
| class KaleidoscopeJIT { | |
| private: | |
| ExecutionSession ES; | |
| RTDyldObjectLinkingLayer ObjectLayer; | |
| IRCompileLayer CompileLayer; | |
| IRTransformLayer TransformLayer; | |
| DataLayout DL; | |
| MangleAndInterner Mangle; | |
| ThreadSafeContext Ctx; | |
| public: | |
| KaleidoscopeJIT(JITTargetMachineBuilder JTMB, DataLayout DL) | |
| : ObjectLayer(ES, | |
| []() { return std::make_unique<SectionMemoryManager>(); }), | |
| CompileLayer(ES, ObjectLayer, ConcurrentIRCompiler(std::move(JTMB))), | |
| TransformLayer(ES, CompileLayer, optimizeModule), | |
| DL(std::move(DL)), Mangle(ES, this->DL), | |
| Ctx(std::make_unique<LLVMContext>()) { | |
| ES.getMainJITDylib().addGenerator( | |
| cantFail(DynamicLibrarySearchGenerator::GetForCurrentProcess(DL.getGlobalPrefix()))); | |
| } | |
| ``` | |
| Our extended KaleidoscopeJIT class starts out the same as it did in Chapter 1, | |
| but after the CompileLayer we introduce a new member, TransformLayer, which sits | |
| on top of our CompileLayer. We initialize our OptimizeLayer with a reference to | |
| the ExecutionSession and output layer (standard practice for layers), along with | |
| a *transform function*. For our transform function we supply our classes | |
| optimizeModule static method. | |
| ```c++ | |
| // ... | |
| return cantFail(OptimizeLayer.addModule(std::move(M), | |
| std::move(Resolver))); | |
| // ... | |
| ``` | |
| Next we need to update our addModule method to replace the call to | |
| `CompileLayer::add` with a call to `OptimizeLayer::add` instead. | |
| ```c++ | |
| static Expected<ThreadSafeModule> | |
| optimizeModule(ThreadSafeModule M, const MaterializationResponsibility &R) { | |
| // Create a function pass manager. | |
| auto FPM = std::make_unique<legacy::FunctionPassManager>(M.get()); | |
| // Add some optimizations. | |
| FPM->add(createInstructionCombiningPass()); | |
| FPM->add(createReassociatePass()); | |
| FPM->add(createGVNPass()); | |
| FPM->add(createCFGSimplificationPass()); | |
| FPM->doInitialization(); | |
| // Run the optimizations over all functions in the module being added to | |
| // the JIT. | |
| for (auto &F : *M) | |
| FPM->run(F); | |
| return M; | |
| } | |
| ``` | |
| At the bottom of our JIT we add a private method to do the actual optimization: | |
| *optimizeModule*. This function takes the module to be transformed as input (as | |
| a ThreadSafeModule) along with a reference to a reference to a new class: | |
| `MaterializationResponsibility`. The MaterializationResponsibility argument | |
| can be used to query JIT state for the module being transformed, such as the set | |
| of definitions in the module that JIT'd code is actively trying to call/access. | |
| For now we will ignore this argument and use a standard optimization | |
| pipeline. To do this we set up a FunctionPassManager, add some passes to it, run | |
| it over every function in the module, and then return the mutated module. The | |
| specific optimizations are the same ones used in [Chapter 4](LangImpl04.md) | |
| of the "Implementing a language with LLVM" tutorial series. Readers may visit | |
| that chapter for a more in-depth discussion of these, and of IR optimization in | |
| general. | |
| And that's it in terms of changes to KaleidoscopeJIT: When a module is added via | |
| addModule the OptimizeLayer will call our optimizeModule function before passing | |
| the transformed module on to the CompileLayer below. Of course, we could have | |
| called optimizeModule directly in our addModule function and not gone to the | |
| bother of using the IRTransformLayer, but doing so gives us another opportunity | |
| to see how layers compose. It also provides a neat entry point to the *layer* | |
| concept itself, because IRTransformLayer is one of the simplest layers that | |
| can be implemented. | |
| ```c++ | |
| // From IRTransformLayer.h: | |
| class IRTransformLayer : public IRLayer { | |
| public: | |
| using TransformFunction = std::function<Expected<ThreadSafeModule>( | |
| ThreadSafeModule, const MaterializationResponsibility &R)>; | |
| IRTransformLayer(ExecutionSession &ES, IRLayer &BaseLayer, | |
| TransformFunction Transform = identityTransform); | |
| void setTransform(TransformFunction Transform) { | |
| this->Transform = std::move(Transform); | |
| } | |
| static ThreadSafeModule | |
| identityTransform(ThreadSafeModule TSM, | |
| const MaterializationResponsibility &R) { | |
| return TSM; | |
| } | |
| void emit(MaterializationResponsibility R, ThreadSafeModule TSM) override; | |
| private: | |
| IRLayer &BaseLayer; | |
| TransformFunction Transform; | |
| }; | |
| // From IRTransformLayer.cpp: | |
| IRTransformLayer::IRTransformLayer(ExecutionSession &ES, | |
| IRLayer &BaseLayer, | |
| TransformFunction Transform) | |
| : IRLayer(ES), BaseLayer(BaseLayer), Transform(std::move(Transform)) {} | |
| void IRTransformLayer::emit(MaterializationResponsibility R, | |
| ThreadSafeModule TSM) { | |
| assert(TSM.getModule() && "Module must not be null"); | |
| if (auto TransformedTSM = Transform(std::move(TSM), R)) | |
| BaseLayer.emit(std::move(R), std::move(*TransformedTSM)); | |
| else { | |
| R.failMaterialization(); | |
| getExecutionSession().reportError(TransformedTSM.takeError()); | |
| } | |
| } | |
| ``` | |
| This is the whole definition of IRTransformLayer, from | |
| `llvm/include/llvm/ExecutionEngine/Orc/IRTransformLayer.h` and | |
| `llvm/lib/ExecutionEngine/Orc/IRTransformLayer.cpp`. This class is concerned | |
| with two very simple jobs: (1) Running every IR Module that is emitted via this | |
| layer through the transform function object, and (2) implementing the ORC | |
| `IRLayer` interface (which itself conforms to the general ORC Layer concept, | |
| more on that below). Most of the class is straightforward: a typedef for the | |
| transform function, a constructor to initialize the members, a setter for the | |
| transform function value, and a default no-op transform. The most important | |
| method is `emit` as this is half of our IRLayer interface. The emit method | |
| applies our transform to each module that it is called on and, if the transform | |
| succeeds, passes the transformed module to the base layer. If the transform | |
| fails, our emit function calls | |
| `MaterializationResponsibility::failMaterialization` (this JIT clients who | |
| may be waiting on other threads know that the code they were waiting for has | |
| failed to compile) and logs the error with the execution session before bailing | |
| out. | |
| The other half of the IRLayer interface we inherit unmodified from the IRLayer | |
| class: | |
| ```c++ | |
| Error IRLayer::add(JITDylib &JD, ThreadSafeModule TSM, VModuleKey K) { | |
| return JD.define(std::make_unique<BasicIRLayerMaterializationUnit>( | |
| *this, std::move(K), std::move(TSM))); | |
| } | |
| ``` | |
| This code, from `llvm/lib/ExecutionEngine/Orc/Layer.cpp`, adds a | |
| ThreadSafeModule to a given JITDylib by wrapping it up in a | |
| `MaterializationUnit` (in this case a `BasicIRLayerMaterializationUnit`). | |
| Most layers that derived from IRLayer can rely on this default implementation | |
| of the `add` method. | |
| These two operations, `add` and `emit`, together constitute the layer | |
| concept: A layer is a way to wrap a part of a compiler pipeline (in this case | |
| the "opt" phase of an LLVM compiler) whose API is opaque to ORC with an | |
| interface that ORC can call as needed. The add method takes an | |
| module in some input program representation (in this case an LLVM IR module) | |
| and stores it in the target `JITDylib`, arranging for it to be passed back | |
| to the layer's emit method when any symbol defined by that module is requested. | |
| Each layer can complete its own work by calling the `emit` method of its base | |
| layer. For example, in this tutorial our IRTransformLayer calls through to | |
| our IRCompileLayer to compile the transformed IR, and our IRCompileLayer in | |
| turn calls our ObjectLayer to link the object file produced by our compiler. | |
| So far we have learned how to optimize and compile our LLVM IR, but we have | |
| not focused on when compilation happens. Our current REPL optimizes and | |
| compiles each function as soon as it is referenced by any other code, | |
| regardless of whether it is ever called at runtime. In the next chapter we | |
| will introduce a fully lazy compilation, in which functions are not compiled | |
| until they are first called at run-time. At this point the trade-offs get much | |
| more interesting: the lazier we are, the quicker we can start executing the | |
| first function, but the more often we will have to pause to compile newly | |
| encountered functions. If we only code-gen lazily, but optimize eagerly, we | |
| will have a longer startup time (as everything is optimized at that time) but | |
| relatively short pauses as each function just passes through code-gen. If we | |
| both optimize and code-gen lazily we can start executing the first function | |
| more quickly, but we will have longer pauses as each function has to be both | |
| optimized and code-gen'd when it is first executed. Things become even more | |
| interesting if we consider interprocedural optimizations like inlining, which | |
| must be performed eagerly. These are complex trade-offs, and there is no | |
| one-size-fits all solution to them, but by providing composable layers we leave | |
| the decisions to the person implementing the JIT, and make it easy for them to | |
| experiment with different configurations. | |
| [Next: Adding Per-function Lazy Compilation](BuildingAJIT3.md) | |
| ## Full Code Listing | |
| Here is the complete code listing for our running example with an | |
| IRTransformLayer added to enable optimization. To build this example, use: | |
| ```bash | |
| # Compile | |
| clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy | |
| # Run | |
| ./toy | |
| ``` | |
| Here is the code: | |
| ```{literalinclude} ../../examples/Kaleidoscope/BuildingAJIT/Chapter2/KaleidoscopeJIT.h | |
| :language: c++ | |
| ``` | |