Pass and Pass Manager

LLVM's Pass infrastructure is one of the many important features of the LLVM system. There are a number of analysis and optimization passes that can be run using this infrastructure. The starting point for LLVM passes is the Pass class, which is a superclass of all the passes. We need to inherit from some predefined subclasses taking into account what our pass is going to implement.

  • ModulePass: This is the most general superclass. By inheriting this class we allow the entire module to be analyzed at once. The functions within the module may not be referred to in a particular order. To use it, write a subclass that inherits from the ModulePass subclass and overloads the runOnModule function.

    Note

    Before going ahead with the discussion of other Pass classes, let's look into the three virtual methods that the Pass classes override:

    • doInitialization: This is meant to do initialization stuff that does not depend on the current function being processed.
    • runOn{Passtype}: This is the method where we should implement our subclass for the functionality of the pass. This will be runOnFunction for FunctionPass, runOnLoop for LoopPass, and so on.
    • doFinalization: This is called when runOn{Passtype} has finished doing the job for every function in the program.
  • FunctionPass: These passes execute on each function present in the module, independent from other functions in the module. There is no defined order in which the functions will be processed. They are not allowed to modify functions other than the one being processed, and any addition or deletion of functions from the current module is also not allowed. To implement FunctionPass we might need to overload the three virtual functions mentioned earlier by implementing in the runOnFunction method.
  • BasicBlockPass: These passes work on basic blocks one at a time, independently of other basic blocks present in the program. They are not allowed to add or delete any new basic block or change the CFG. They are also not allowed to do things that FunctionPass is not allowed to. To implement, they can override the doInitialization and doFinalization methods of FunctionPass, or overload their own virtual methods for the two methods mentioned earlier and the runOnBasicBlock method.
  • LoopPass: These passes work on each loop in the function, independent of all other loops within the function. Loops are processed in such a way that the outermost loop is executed the last. To implement LoopPass we need to overload the doInitialization, doFinalization, and runOnLoop methods.

Now, let's see how to get started with writing a custom pass. Let's write a pass that will print the names of all the functions.

Before getting started with writing the implementation of the pass, we need to make changes in a few places in the code so that the pass is recognized and can be run.

We need to create a directory under the LLVM tree. Let's make a directory, lib/Transforms/FnNamePrint. In this directory, we need to create a Makefile with the following contents, which will allow our pass to be compiled:

LEVEL = ../../..

LIBRARYNAME = FnNamePrint

LOADABLE_MODULE = 1

include $(LEVEL)/Makefile.common

This specifies that all .cpp files should be compiled and linked into a shared object that will be available in the lib folder of the build-folder (build-folder/lib/FnNamePrint.so).

Now, let's get started with writing the actual pass implementation. We need to create the source file for the pass in lib/Transforms/FnNamePrint: let's name it FnNamePrint.cpp. The first step now is to choose the correct subclass. In this case, as we are trying to print names of each function, the FunctionPass class will serve our purpose by processing one function at a time. Also, we are only printing the name of function and not modifying anything within it, so we are choosing FunctionPass for simplicity. We could use ModulePass as well because it is an Immutable Pass.

Now, let's write the source code for the pass implementation, which looks like this:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
  struct FnNamePrint: public FunctionPass {
    static char ID;
    FnNamePrint () : FunctionPass(ID) {}
    bool runOnFunction(Function &F) override {
      errs() << "Function " << F.getName() << '
';
      return false;
    }
  };
}

char FnNamePrint::ID = 0;static RegisterPass< FnNamePrint > X("funcnameprint","Function Name Print", false, false);

In the preceding code we include the necessary headers first and use an llvm namespace:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

We declare our pass as a structure, FnNamePrint, which is a subclass of FunctionPass. In runOnFunction we implement the logic to print the function name. The bool value returned in the end signifies whether we have made any modification within the function. A True value is returned if some modifications was made, otherwise, false is returned. In our case, we are not making any modifications, so we return false.

struct FnNamePrint: public FunctionPass {
  static char ID;
  FnNamePrint () : FunctionPass(ID) {}
  bool runOnFunction(Function &F) override {
    errs() << "Function " << F.getName() << '
';
    return false;
    }
  };
}

Then, we declare the ID for the pass, which is used to identify the pass:

char FnNamePrint::ID = 0;

Finally, we need to register the passes with the Pass Manager. The first argument is the Pass name used by the opt tool to identify this pass. The second argument is the actual Pass name. The third and fourth arguments specify whether the pass modified the cfg and whether it is an analysis pass.

static RegisterPass< FnNamePrint > X("funcnameprint","Function Name Print", false, false);

Note

The implementation of the pass is done. Now, before we use it, we need to build LLVM using the make command, which will build the shared object in the lib folder within the build (build-folder/lib/FnNamePrint.so).

Now, we can run the pass over a test case using the opt tool in the following way:

$ opt -load path-to-llvm/build/lib/FnNamePrint.so -funcnameprint test.ll

The load command line option specifies the path from where to pick the shared object of the pass and –funcnameprint is the option to opt tool to tell it to run the pass we have written. The Pass will print the names of all the function present in the testcase. For the example in the first section it will print out:

Function test
Function caller
Function callercaller

So, we got started with writing a Pass. Now, we will see the significance of the PassManager class in LLVM.

The PassManager class schedules the passes to be run efficiently. The PassManager is used by all LLVM tools that run passes for the execution of these passes. It is the responsibility of the PassManager to make sure the interaction between the passes is correctly done. As it tries to execute the passes in an optimized way, it must have information regarding how the passes interact with each other and what the different dependencies between the passes are.

A pass itself can specify the dependency on other passes, that is, which passes need to be run before the execution of the current pass. Also, it can specify the passes that will be invalidated by the execution of the current pass. The PassManager gets the analysis results before a pass is executed. We will later see how a pass can specify such dependencies.

The main work of the PassManager is to avoid the calculation of analysis results time and again. This is done by keeping track of which analyses are available, which are invalidated, and which analyses are required. The PassManager tracks the lifetimes of the analysis results and frees the memory holding the analysis results when not required, allowing for optimal memory use.

The PassManager pipelines the passes together to get better memory and cache results, improving the cache behavior of the compiler. When a series of consecutive FunctionPass are given, it will execute all the FunctionPass on the first function, then all the FunctionPass on the second function, and so on. This improves cache behavior as it is only dealing with the single function part of the LLVM representation and not the entire program.

The PassManager also specifies the –debug-pass option with which we can see how one pass interacts with other passes. We can see what all passes are run using the –debug-pass=Argument option. We can use the –debug-pass=Structure option to see how the passes had run. It will also give us the names of the passes that ran. Let's take the example of the test code in the first section of this chapter:

$ opt -O2 -S test.ll -debug-pass=Structure
$ opt -load /build-folder/lib/LLVMFnNamePrint.so test.ll -funcnameprint -debug-pass=Structure

Pass Arguments:  -targetlibinfo -tti -funcnameprint -verify
Target Library Information
Target Transform Information
  ModulePass Manager
    FunctionPass Manager
      Function Name Print
      Module Verifier
Function test
Function caller
Function callercaller

In the output, the Pass Arguments gives us the passes that are run and the following list is the structure used to run each pass. The Passes just after ModulePass Manager will show the passes run per module (here, it is empty). The passes in hierarchy of FunctionPass Manager show that these passes were run per function (Function Name Print and Module Verifier), which is the expected result.

The PassManger also provides some other useful flags, some of which are the following:

  • time-passes: This gives time information about the pass along with the other passes that are lined up.
  • stats: This prints statistics about each pass.
  • instcount: This collects the count of all instructions and reports them. –stats must also be Passes to the opt tool so that the results of instcount are visible.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.178.181