Ch 9 — Working with PassManager and AnalysisManager

LLVM Techniques, Tips, and Best Practices — Min-Yih Hsu · pages 202–229 · 123 text blocks · 1 figure

Working with PassManager and AnalysisManager In the previous section of this book, Frontend Development, we began with an introduction to the internals of Clang, which is LLVM's official frontend for the C family of programming languages. We went through various projects, involving skills and knowledge, that can help you to deal with problems that are tightly coupled with source code. In this part of the book, we will be working with LLVM IR – a target-independent intermediate representation (IR) for compiler optimization and code generation. Compared to Clang's Abstract Syntax Tree (AST), LLVM IR provides a different level of abstraction by encapsulating extra execution details to enable more powerful program analyses and transformations. In addition to the design of LLVM IR, there is a mature ecosystem around this IR format, which provides countless resources, such as libraries, tools, and algorithm implementations. We will cover a variety of topics in LLVM IR, including the most common LLVM Pass development, using and writing program analysis, and the best practices and tips for working with LLVM IR APIs. Additionally, we will review more advanced skills such as Program Guided Optimization (PGO) and sanitizer development.

184 Working with PassManager and AnalysisManager

In this chapter, we are going to talk about writing a transformation Pass and program analysis for the new PassManager. LLVM Pass is one of the most fundamental, and crucial, concepts within the entire project. It allows developers to encapsulate program processing logic into a modular unit that can be freely composed with other Passes by the PassManager, depending on the situation. In terms of the design of the Pass infrastructure, LLVM has actually gone through an overhaul of both PassManager and AnalysisManager to improve their runtime performance and optimization quality. The new PassManager uses a quite different interface for its enclosing Passes. This new interface, however, is not backward-compatible to the legacy one, meaning you cannot run legacy Passes in the new PassManager and vice versa. What is worse, there aren't many learning resources online that talk about this new interface, even though, now, they are enabled, by default, in both LLVM and Clang. The content of this chapter will fill this gap and provide you with an up-to-date guide to this crucial subsystem in LLVM. In this chapter, we will cover the following topics:

• Writing an LLVM Pass for the new PassManager • Working with the new AnalysisManager • Learning instrumentations in the new PassManager

With the knowledge learned from this chapter, you should be able to write an LLVM Pass, using the new Pass infrastructure, to transform or even optimize your input code. You can also further improve the quality of your Pass by leveraging the analysis data provided by LLVM's program analysis framework.

Technical requirements In this chapter, we will, primarily, use a command-line utility, called opt, to test our Passes. You can build it using a command like this:

$ ninja opt

The code example for this chapter can be found at https://github.com/ PacktPublishing/LLVM-Techniques-Tips-and-Best-Practices-Clangand-Middle-End-Libraries/tree/main/Chapter09.

                                            Writing an LLVM Pass for the new PassManager    185

Writing an LLVM Pass for the new PassManager A Pass in LLVM is the basic unit that is required to perform certain actions against LLVM IR. It is similar to a single production step in a factory, where the products that need to be processed are LLVM IR and the factory workers are the Passes. In the same way that a normal factory usually has multiple manufacturing steps, LLVM also consists of multiple Passes that are executed in sequential order, called the Pass pipeline. Figure 9.1 shows an example of the Pass pipeline:

Figure 9.1 – An example of the LLVM Pass pipeline and its intermediate results In the preceding diagram, multiple Passes are arranged in a straight line. The LLVM IR for the foo function is processed by one Pass after another. Pass B, for instance, performs code optimization on foo and replaces an arithmetic multiplication (mul) by 2 with left shifting (shl) by 1, which is considered easier than multiplication in most hardware architectures. In addition, this figure also illustrates that the code generation steps are modeled as Passes. Code generation in LLVM transforms LLVM IR, which is target independent, into assembly code for certain hardware architecture (for example, x86_64 in Figure 9.1). Each detailed procedure, such as the register allocation, instruction selection, or instruction scheduling, is encapsulated into a single Pass and is executed in a certain order.

        Code generation Passes
        Passes for code generation have a different API than normal LLVM IR Passes.
        Additionally, during the code generation phase, LLVM IR is actually converted
        into another kind of IR, called Machine IR (MIR). However, in this chapter, we
        will only be covering LLVM IR and its Passes.
Figure from page 204
figure · book page 204

186 Working with PassManager and AnalysisManager

This Pass pipeline is conceptually managed by an infrastructure called PassManager. PassManager owns the plan – their execution order, for example – to run these Passes. Conventionally, we actually use the terms Pass pipeline and PassManager interchangeably since they have nearly identical missions. In the Learning instrumentations in the new PassManager section, we will go into more detail about the pipeline itself and discuss how to customize the execution order of these enclosing Passes. Code transformations in modern compilers can be complex. Because of this, multiple transformation Passes might need the same set of program information, which is called analysis in LLVM, in order to do their work. Furthermore, to achieve maximum efficiency, LLVM also caches this analysis data so that it can be reused if possible. However, since a transformation Pass might change the IR, some cached analysis data, which was previously collected, might be outdated after running that Pass. To solve these challenges, in addition to PassManager, LLVM has also created AnalysisManager to manage everything related to program analysis. We will go deeper into AnalysisManager in the Working with the new AnalysisManager section. As mentioned in the introduction of this chapter, LLVM has gone through a series of overhauls on its Pass and PassManager (and AnalysisManager) infrastructure. The new infrastructure runs faster and generates results with better quality. Nevertheless, the new Pass differs in many places from the old one; we will briefly explain these differences along the way. However, aside from that, we will only be discussing the new Pass infrastructure, by default, for the rest of the chapter. In this section, we will show you how to develop a simple Pass for the new PassManager. As usual, we will begin with a description of the sample project we are about to use. Then, we will show you the steps to create a Pass that can be dynamically loaded from a plugin into the Pass pipeline, which was mentioned earlier, using the opt utility.

Project overview In this section, the sample project we are using is called StrictOpt. It is a Pass and Pass plugin that adds a noalias attribute to every function parameter that has a pointer type. Effectively, it adds a restrict keyword to the function parameters in C code. First, let's explain what the restrict keyword does.

        The restrict keyword in C and C++
        The restrict keyword was introduced in C99. However, it doesn't have a
        counterpart in C++. Nevertheless, mainstream compilers such as Clang, GCC,
        and MSVS all support the same functionality in C++. For example, in Clang
        and GCC, you can use __restrict__ or __restrict in C++ code and
        it has the same effect as restrict in C.
                                          Writing an LLVM Pass for the new PassManager      187

The restrict keyword can also be used alongside pointer type variables in C. In the most common cases, it is used with pointer type function parameters. The following is an example:

  int foo(int* restrict x, int* restrict y) {
    *x = *y + 1;
    return *y;

Essentially, this additional attribute tells the compiler that argument x will never point to the same memory region as argument y. In other words, programmers can use this keyword to persuade the compilers that they will never call the foo function, as follows:

  …
  // Programmers will NEVER write the following code
  int main() {
    int V = 1;
    return foo(&V, &V);

The rationale behind this is that if the compiler knows that two pointers – in this case, the two pointer arguments – will never point to the same memory region, it can do more aggressive optimizations. To give you a more concrete understanding of this, if you compare the assembly code of the foo function with and without the restrict keyword, the latter version takes five instructions to execute (on x86_64):

  foo:
         mov     eax, dword ptr [rsi]
         add     eax, 1
         mov     dword ptr [rdi], eax
         mov     eax, dword ptr [rsi]
         ret

The version with the restrict keyword added only takes four instructions:

  foo:
         mov     eax, dword ptr [rsi]
         lea     ecx, [rax + 1]
         mov     dword ptr [rdi], ecx
         ret

188 Working with PassManager and AnalysisManager

Although the difference here seems subtle, in the version without restrict, the compiler needs to insert an extra memory load to assure that the last argument *y (in the original C code) always reads the latest value. This extra cost might gradually accumulate in a more complex code base and, eventually, create a performance bottleneck. Now, you have learned how restrict works and its importance for ensuring good performance. In LLVM IR, there is also a corresponding directive to model the restrict keyword: the noalias attribute. This attribute is attached to the pointer function parameters if hints such as restrict have been given by programmers in the original source code. For example, the foo function (with the restrict keywords) can be translated into the following LLVM IR:

  define i32 @foo(i32* noalias %0, i32* noalias %1) {
    %3 = load i32, i32* %1
    %4 = add i32 %3, 1
    store i32 %4, i32* %0
    ret i32 %3

Furthermore, we can also generate the LLVM IR code of the foo function without restrict in C code, as follows:

  define i32 @foo(i32* %0, i32* %1) {
    %3 = load i32, i32* %1
    %4 = add i32 %3, 1
    store i32 %4, i32* %0
    %5 = load i32, i32* %1
    ret i32 %5

Here, you will find that there is an extra memory load (as shown in the highlighted instruction of the preceding snippet), which is similar to what happened to the assembly examples from earlier. That is, LLVM is unable to perform more aggressive optimization to remove that memory load since it's not sure whether those pointers overlap each other.

                                            Writing an LLVM Pass for the new PassManager     189

In this section, we are going to write a Pass to add a noalias attribute to every pointer argument of a function. The Pass will be built as a plugin, and once it's loaded into opt, users can use the --passes argument to explicitly trigger StrictOpt, as follows:

  $ opt --load-pass-plugin=StrictOpt.so \
        --passes="function(strict-opt)" \
        -S -o – test.ll

Alternatively, we can make StrictOpt run before other optimizations if the optimization level is greater or equal to -O3. The following is an example:

  $ opt -O3 --enable-new-pm \
        --load-pass-plugin=StrictOpt.so \
        -S -o – test.ll

We will show you how to switch between these two modes shortly.

        A demo-only Pass
        Note that StrictOpt is merely a demo-only Pass, and adding noalias to
        every pointer function argument is absolutely not the thing you should do in
        real-world use cases. This is because it might break the correctness of the target
        program.

In the next section, we will show you detailed steps of how to create this Pass.

Writing the StrictOpt Pass The following instructions will take you through the process of developing the core Pass logic before covering how to register StrictOpt into the Pass pipeline dynamically:

  1. We only have two source files this time: StrictOpt.h and StrictOpt.cpp.
     In the former file, we place the skeleton of the StrictOpt Pass:
         #include "llvm/IR/PassManager.h"
         struct StrictOpt : public PassInfoMixin<StrictOpt> {
            PreservedAnalyses run(Function &F,
                                  FunctionAnalysisManager &FAM);
         };

190 Working with PassManager and AnalysisManager

      The Pass we are writing here is a function Pass; namely, it runs on the Function IR
      unit. The run method is the primary entry point for this Pass, which we are going
      to fill in later. It takes two arguments: a Function class that we will work on and
      a FunctionAnalysisManager class that can give you analysis data. It returns a
      PreservedAnalyses instance, which tells PassManager (and AnalysisManager)
      what analysis data was invalidated by this Pass.
      If you have prior experience in writing LLVM Pass for the legacy PassManager, you
      might find several differences between the legacy Pass and the new Pass:
      a) The Pass class no longer derives from one of the FunctionPass, ModulePass,
         or LoopPass. Instead, the Passes running on different IR units are all
         deriving from PassInfoMixin<YOUR_PASS>. In fact, deriving from
         PassInfoMixin is not even a requirement for a functional Pass anymore – we
         will leave this as an exercise for you.
      b) Instead of overriding methods, such as runOnFunction or runOnModule,
         you will define a normal class member method, run (be aware that run does not
         have an override keyword that follows), which operates on the desired IR unit.
      Overall, the new Pass has a cleaner interface compared to the legacy one. This
      difference also allows the new PassManager to have less overhead runtime.
 2. To implement the skeleton from the previous step, we are heading to StrictOpt.
    cpp. In this file, first, we create the following method definition:
         #include "StrictOpt.h"
         using namespace llvm;
         PreservedAnalyses StrictOpt::run(Function &F,
                                   FunctionAnalysisManager &FAM) {
           return PreservedAnalyses::all(); // Just a placeholder
      The returned PreservedAnalyses::all() instance is just a placeholder that
      will be removed later.
 3. Now, we are finally creating the code to add a noalias attribute to the pointer
    function arguments. The logic is simple: for each Argument instance in
    a Function class, attach noalias if it fulfills the criteria:
         // Inside StrictOpt::run…
         bool Modified = false;
         for (auto &Arg : F.args()) {
                                      Writing an LLVM Pass for the new PassManager    191
           if (Arg.getType()->isPointerTy() &&
               !Arg.hasAttribute(Attribute::NoAlias)) {
             Arg.addAttr(Attribute::NoAlias);
             Modified |= true;
    The args() method of the Function class will return a range of Argument
    instances representing all of the formal parameters. We check each of their types to
    make sure there isn't an existing noalias attribute (which is represented by the
    Attribute::NoAlias enum). If everything looks good, we use addAttr to
    attach noalias.
    Here, the Modified flag here records whether any of the arguments were modified
    in this function. We will use this flag shortly.
4. Certain analysis data might become outdated after a transformation Pass since
   the latter might change the program's IR. Therefore, when writing a Pass, we need
   to return a PreservedAnalyses instance to show which analysis was affected
   and should be subject to recalculation. While there are many analyses available in
   LLVM, we don't need to enumerate each of them. Instead, there are some handy
   utility functions to create PreservedAnalyses instances, representing all
   analyses or none of the analyses, such that we only need to subtract or add (un)
   affected analysis from it. Here is what we do in StrictOpt:
       #include "llvm/Analysis/AliasAnalysis.h"
       …
       // Inside StrictOpt::run…
       auto PA = PreservedAnalyses::all();
       if (Modified)
         PA.abandon<AAManager>();
       return PA;
    Here, we first create a PreservedAnalyses instance, PA, which represents all
    analyses. Then, if the Function class we are working on here has been modified,
    we discard the AAManager analysis via the abandon method. AAManager
    represents the alias analysis in LLVM.

192 Working with PassManager and AnalysisManager

      Without going into the details of this, the alias analysis asks whether two pointers
      point to the same memory region, or whether the memory regions they are pointing
      to overlap with each other. The noalias attribute we are discussing here has strong
      relations with this analysis since they're working on a nearly identical problem.
      Therefore, if any new noalias attribute was generated, all the cached alias analysis
      data would be outdated. This is why we invalidate it using abandon.
      Note that you can always return a PreservedAnalyses::none() instance,
      which tells AnalysisManager to mark every analysis as outdated if you are not
      sure what analyses have been affected. This comes at a cost, of course, since
      AnalysisManager then needs to spend extra effort to recalculate the analyses that
      might contain expensive computations.
 5. The core logic of StrictOpt is essentially finished. Now, we are going to show
    you how to dynamically register the Pass into the pipeline. In StrictOpt.cpp,
    we create a special global function, called llvmGetPassPluginInfo, with an
    outline like this:
         extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_
         WEAK
         llvmGetPassPluginInfo() {
           return {
              LLVM_PLUGIN_API_VERSION, "StrictOpt", "v0.1",
             [](PassBuilder &PB) {…}
           };
      This function returns a PassPluginLibraryInfo instance, which contains
      various pieces of information such as the plugin API version (LLVM_PLUGIN_
      API_VERSION) and the Pass name (StrictOpt). One of its most important
      fields is a lambda function that takes a single PassBuilder& argument. In that
      particular function, we are going to insert our StrictOpt into a proper position
      within the Pass pipeline.
                                    Writing an LLVM Pass for the new PassManager     193

PassBuilder, as its name suggests, is an entity LLVM that is used to build the Pass pipeline. In addition to its primary job, which involves configuring the pipeline according to the optimization level, it also allows developers to insert Passes into some of the places in the pipeline. Furthermore, to increase its flexibility, PassBuilder allows you to specify a textual description of the pipeline you want to run by using the --passes argument on opt, as we have seen previously. For instance, the following command will run InstCombine, PromoteMemToReg, and SROA (SROA: Scalar Replacement of Aggregates) in sequential order: $ opt --passes="instcombine,mem2reg,sroa" test.ll -S -o –

What we are going to do in this step is ensure that after the plugin has been loaded, opt will run our Pass if strict-opt appears in the --passes argument, as follows: $ opt --passes="strict-opt" test.ll -S -o –

To do this, we leverage the registerPipelineParsingCallback method in
PassBuilder:
   …
   [](PassBuilder &PB) {
     using PipelineElement = typename
   PassBuilder::PipelineElement;
     PB.registerPipelineParsingCallback(
       [](StringRef Name,
          FunctionPassManager &FPM,
   ArrayRef<PipelineElement>){
         if (Name == "strict-opt") {
           FPM.addPass(StrictOpt());
           return true;
         return false;
       });

194 Working with PassManager and AnalysisManager

      The registerPipelineParsingCallback method takes another lambda
      callback as the argument. This callback is invoked whenever PassBuilder
      encounters an unrecognized Pass name while parsing the textual pipeline
      representation. Therefore, in our implementation, we simply insert our StrictOpt
      pass into the pipeline via FunctionPassManager::addPass when the
      unrecognized Pass name, that is, the Name parameter, is strict-opt.
 6. Alternatively, we also want to trigger our StrictOpt at the beginning of the
    Pass pipeline without using the textual pipeline description, as we described in the
    Project overview section. This means that the Pass will be run before other Passes
    after it is loaded into opt using the following command:
         $ opt -O2 --enable-new-pm \
               --load-pass-plugin=StrictOpt.so test.ll -S -o –
      (The --enable-new-pm flag in the preceding command forced opt to use the
      new PassManager since it's still using the legacy one by default. We haven't used
      this flag before because --passes implicitly enables the new PassManager under
      the hood.)
      To do this, instead of using
      PassBuilder::registerPipelineParsingCallback
      to register a custom (pipeline) parser callback, we are going to use
      registerPipelineStartEPCallback to handle this. Here is the alternative
      version of the code snippet from the previous step:
         …
         [](PassBuilder &PB) {
           using OptimizationLevel
             = typename PassBuilder::OptimizationLevel;
           PB.registerPipelineStartEPCallback(
             [](ModulePassManager &MPM, OptimizationLevel OL) {
               if (OL.getSpeedupLevel() >= 2) {
                 MPM.addPass(
                   createModuleToFunctionPassAdaptor(StrictOpt()));
             });
                                        Writing an LLVM Pass for the new PassManager   195

There are several things worth noting in the preceding snippet:

   • The registerPipelineStartEPCallback method we are using here registers
     a callback that can customize certain places in the Pass pipeline, called extension
     points (EPs). The EP we are going to customize here is one of the earliest points in
     the pipeline.
   • In comparison to the lambda callback we saw in
     registerPipelineParsingCallback, the lambda callback for
     registerPipelineStartEPCallback only provides ModulePassManager,
     rather than FunctionPassManager, to insert our StrictOpt Pass, which is
     a function Pass. We are using ModuleToFunctionPassAdapter to overcome
     this issue.
      ModuleToFunctionPassAdapter is a module Pass that can run a given
      function Pass over a module's enclosing functions. It is suitable for running
      a function Pass in contexts where only ModulePassManager is available,
      such as in this scenario. The createModuleToFunctionPassAdaptor
      function highlighted in the preceding code is used to create a new
      ModuleToFunctionPassAdapter instance from a specific function Pass.
   • Finally, in this version, we are only enabling StrictOpt when the optimization
     level is greater or equal to -O2. Therefore, we leverage the OptimizationLevel
     argument passing into the lambda callback to determine whether we want to insert
     StrictOpt into the pipeline or not.
      With these Pass registration steps, we have also learned how to trigger our
      StrictOpt without explicitly specifying the textual Pass pipeline.
To summarize, in this section, we learned the essentials of the LLVM Pass and Pass
pipeline. Through the StrictOpt project, we have learned how to develop a Pass –
which is also encapsulated as a plugin – for the new PassManager and how to dynamically
register it against the Pass pipeline in opt in two different ways: first, by triggering
the Pass explicitly via a textual description, and second, by running it at a certain time
point (EP) in the pipeline. We also learned how to invalidate analyses depending on the
changes made in the Pass. These skills can help you develop high-quality and modern
LLVM Passes to process IR in a composable fashion with maximum flexibility. In the
next section, we will dive into the program analysis infrastructure of LLVM. This greatly
improves the capability of normal LLVM transformation Passes.

196 Working with PassManager and AnalysisManager

Working with the new AnalysisManager Modern compiler optimizations can be complex. They usually require lots of information from the target program in order to make correct decisions and optimal transformations. For example, in the Writing an LLVM Pass for the new PassManager section, LLVM used the noalias attribute to calculate memory aliasing information, which might eventually be used to remove redundant memory loads. Some of this information – called analysis, in LLVM – is expensive to evaluate. In addition, a single analysis might also depend on other analyses. Therefore, LLVM creates an AnalysisManager component to handle all tasks related to program analysis in LLVM. In this section, we are going to show you how to use AnalysisManager in your own Passes for the sake of writing more powerful and sophisticated program transformations or analyses. We will also use a sample project, HaltAnalyzer, to drive our tutorial here. The next section will provide you with an overview of HaltAnalyzer before moving on to the detailed development steps.

Overview of the project HaltAnalyzer is set up in a scenario where target programs are using a special function, my_halt, that terminates the program execution when it is called. The my_halt function is similar to the std::terminate function, or the assert function when its sanity check fails. The job of HaltAnalyzer is to analyze the program to find basic blocks that are guaranteed to be unreachable because of the my_halt function. To be more specific, let's take the following C code as an example:

  int foo(int x, int y) {
    if (x < 43) {
      my_halt();
      if (y > 45)
        return x + 1;
      else {
        bar();
        return x;
    } else {
      return y;
                                                 Working with the new AnalysisManager    197

Because my_halt was called at the beginning of the true block for the if (x < 43) statement, the code highlighted in the preceding snippet will never be executed (that is, my_halt stopped all of the program executions before even getting to those lines). HaltAnalyzer should identify these basic blocks and print out warning messages to stderr. Just like the sample project from the previous section, HaltAnalyzer is also a function Pass wrapped inside a plugin. Therefore, if we use the preceding snippet as the input to our HaltAnalyzer Pass, it should print out the following messages:

$ opt --enable-new-pm --load-pass-plugin ./HaltAnalyzer.so \ --disable-output ./test.ll [WARNING] Unreachable BB: label %if.else [WARNING] Unreachable BB: label %if.then2 $

The %if.else and %if.then2 strings are just names for the basic blocks in the if (y > 45) statement (you might see different names on your side). Another thing worth noting is the --disable-output command-line flag. By default, the opt utility will print out the binary form of LLVM IR (that is, the LLVM bitcode) anyway unless users redirect the output to other places via the -o flag. Using the aforementioned flag is merely to tell opt not to do that since we are not interested in the final content of LLVM IR (because we are not going to modify it) this time. Although the algorithm of HaltAnalyzer seems pretty simple, writing it from scratch might be a pain. That's why we are leveraging one of the analyses provided by LLVM: the Dominator Tree (DT). The concept of Control Flow Graph (CFG) domination has been taught in most entry-level compiler classes, so we are not going to explain it in depth here. Simply speaking, if we say a basic block dominates another block, every execution flow that arrives at the latter is guaranteed to go through the former first. A DT is one of the most important and commonly used analyses in LLVM; most control flow-related transformations cannot live without it. Putting this idea into HaltAnalyzer, we are simply looking for all of the basic blocks that are dominated by the basic blocks that contain a function call to my_halt (we are excluding the basic blocks that contain the my_halt call sites from the warning messages). In the next section, we will show you detailed instructions on how to write HaltAnalyzer.

198 Working with PassManager and AnalysisManager

Writing the HaltAnalyzer Pass In this project, we will only create a single source file, HaltAnalyzer.cpp. Most of the infrastructure, including CMakeListst.txt, can be reused from the StrictOpt project in the previous section:

 1. Inside HaltAnalyzer.cpp, first, we create the following Pass skeleton:
         class HaltAnalyzer : public PassInfoMixin<HaltAnalyzer> {
           static constexpr const char* HaltFuncName = "my_halt";
           // All the call sites to "my_halt"
           SmallVector<Instruction*, 2> Calls;
           void findHaltCalls(Function &F);
         public:
            PreservedAnalyses run(Function &F,
                                  FunctionAnalysisManager &FAM);
         };
      In addition to the run method that we saw in the previous section, we are
      creating an additional method, findHaltCalls, which will collect all of the
      Instruction calls to my_halt in the current function and store them inside the
      Calls vector.
 2. Let's implement findHaltCalls first:
         void HaltAnalyzer::findHaltCalls(Function &F) {
           Calls.clear();
           for (auto &I : instructions(F)) {
             if (auto *CI = dyn_cast<CallInst>(&I)) {
               if (CI->getCalledFunction()->getName() ==
                   HaltFuncName)
                 Calls.push_back(&I);
      This method uses llvm::instructions to iterate through every
      Instruction call in the current function and check them one by one. If the
      Instruction call is a CallInst – representing a typical function call site – and
      the callee name is my_halt, we will push it into the Calls vector for later use.
                                               Working with the new AnalysisManager   199
     Function name mangling
     Be aware that when a line of C++ code is compiled into LLVM IR or native
     code, the name of any symbol – including the function name – will be different
     from what you saw in the original source code. For example, a simple function
     that has the name of foo and takes no argument might have _Z3foov as its name
     in LLVM IR. We call such a transformation in C++ name mangling. Different
     platforms also adopt different name mangling schemes. For example, in Visual
     Studio, the same function name becomes ?foo@@YAHH@Z in LLVM IR.
3. Now, let's go back to the HaltAnalyzer::run method. There are two things we
   are going to do. We will collect the call sites to my_halt via findHaltCalls,
   which we just wrote, and then retrieve the DT analysis data:
      #include "llvm/IR/Dominators.h"
      …
      PreservedAnalyses
      HaltAnalyzer::run(Function &F, FunctionAnalysisManager
      &FAM) {
        findHaltCalls(F);
        DominatorTree &DT = FAM.
          getResult<DominatorTreeAnalysis>(F);
        …
   The highlighted line in the preceding snippet is the main character of this section.
   It shows us how to leverage the provided FunctionAnalysisManager type
   argument to retrieve specific analysis data (in this case, DominatorTree) for
   a specific Function class.
   Although, so far, we have (kind of) used the words analysis and analysis data
   interchangeably, in a real LLVM implementation, they are actually two different
   entities. Take the DT that we are using here as an example:
   a) D
       ominatorTreeAnalysis is a C++ class that evaluates dominating relationships
      from the given Function. In other words, it is the one that performs the
      analysis.
   b) D
       ominatorTree is a C++ class that represents the result generated from
      DominatorTreeAnalysis. This is just static data that will be cached by
      AnalysisManager until it is invalidated.

200 Working with PassManager and AnalysisManager

      Furthermore, LLVM asks every analysis to clarify its affiliated result type via the
      Result member type. For example, DominatorTreeAnalysis::Result is
      equal to DominatorTree.
      To make this even more formal, to associate the analysis data of an analysis class, T,
      with a Function variable, F, we can use the following snippet:
         // `FAM` is a FunctionAnalysisManager
         typename T::Result &Data = FAM.getResult<T>(F);
 4. After we retrieve DominatorTree, it's time to find all of the basic blocks
    dominated by the Instruction call sites that we collected earlier:
         PreservedAnalyses
         HaltAnalyzer::run(Function &F, FunctionAnalysisManager
         &FAM) {
           …
           SmallVector<BasicBlock*, 4> DomBBs;
           for (auto *I : Calls) {
             auto *BB = I->getParent();
             DomBBs.clear();
             DT.getDescendants(BB, DomBBs);
               for (auto *DomBB : DomBBs) {
               // excluding the block containing `my_halt` call site
                 if (DomBB != BB) {
                   DomBB->printAsOperand(
                          errs() << "[WARNING] Unreachable BB: ");
                   errs() << "\n";
             return PreservedAnalyses::all();
                                              Working with the new AnalysisManager   201
    By using the DominatorTree::getDescendants method, we can retrieve all
    of the basic blocks dominated by a my_halt call site. Note that the results from
    getDescendants will also contain the block you put into the query (in this
    case, the block containing the my_halt call sites), so we need to exclude it before
    printing the basic block name using the BasicBlock::printAsOperand
    method.
    With the ending of the returning PreservedAnalyses::all(), which tells
    AnalysisManager that this Pass does not invalidate any analysis since we don't
    modify the IR at all, we will wrap up the HaltAnalyzer::run method here.
5. Finally, we need to dynamically insert our HaltAnalyzer Pass into the Pass pipeline.
   We are using the same method that we did in the last section, by implementing the
   llvmGetPassPluginInfo function and using PassBuilder to put our Pass at
   one of the EPs in the pipeline:
       extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_
       WEAK
       llvmGetPassPluginInfo() {
         return {
            LLVM_PLUGIN_API_VERSION, "HaltAnalyzer", "v0.1",
            [](PassBuilder &PB) {
              using OptimizationLevel
                = typename PassBuilder::OptimizationLevel;
              PB.registerOptimizerLastEPCallback(
                [](ModulePassManager &MPM, OptimizationLevel OL)
       {
                  MPM.addPass(createModuleToFunctionPassAdaptor
                   (HaltAnalyzer()));
                });
         };

202 Working with PassManager and AnalysisManager

      In comparison to StrictOpt in the previous section, we are using
      registerOptimizerLastEPCallback to insert HaltAnalyzer after
      all of the other optimization Passes. The rationale behind this is that some
      optimizations might move basic blocks around, so prompting warnings
      too early might not be very useful. Nevertheless, we are still leveraging
      ModuletoFunctionPassAdaptor to wrap around our Pass; this is because
      registerOptimizerLastEPCallback only provides ModulePassManager
      for us to add our Pass, which is a function Pass.
These are all the necessary steps to implement our HaltAnalyzer. Now you have learned
how to use LLVM's program analysis infrastructure to obtain more information about the
target program in an LLVM Pass. These skills can provide you with more insight into IR
when you are developing a Pass. In addition, this infrastructure allows you to reuse high-
quality, off-the-shelf program analysis algorithms from LLVM instead of recreating the
wheels by yourself. To browse all of the available analyses provided by LLVM, the llvm/
include/llvm/Analysis folder in the source tree is a good starting point. Most of
the header files within this folder are standalone analysis data files that you can use.
In the final section of this chapter, we will show you some diagnosis techniques that are
useful for debugging an LLVM Pass.

Learning instrumentations in the new PassManager PassManager and AnalysisManager in LLVM are complicated pieces of software. They manage interactions between hundreds of Passes and analyses, and it can be a challenge when we try to diagnose a problem caused by them. In addition, it's really common for a compiler engineer to fix crashes in the compiler or miscompilation bugs. In those scenarios, useful instrumentation tools that provide insights to Passes and the Pass pipeline can greatly improve the productivity of fixing those problems. Fortunately, LLVM has already provided many of those tools.

        Miscompilation
        Miscompilation bugs usually refer to logical issues in the compiled program,
        which were introduced by compilers. For example, an overly aggressive
        compiler optimization removes certain loops that shouldn't be removed,
        causing the compiled software to malfunction, or mistakenly reorder memory
        barriers and create race conditions in the generated code.
                                       Learning instrumentations in the new PassManager     203

We will introduce a single tool at a time in each of the following sections. Here is the list of them:

• Printing Pass pipeline details • Printing changes to the IR after each Pass • Bisecting the Pass pipeline

These tools can interact purely in the command-line interface of opt. In fact, you can also create your own instrumentation tools (without even changing the LLVM source tree!); we will leave this as an exercise for you.

Printing Pass pipeline details There are many different optimization levels in LLVM, that is, the -O1, -O2, or -Oz flags we are familiar with when using clang (or opt). Each optimization level is running a different set of Passes and arranging them in different orders. In some cases, this might greatly affect the generated code, in terms of performance or correctness. Therefore, sometimes, it's crucial to know these configurations in order to gain a clear understanding of the problems we are going to deal with. To print out all the Passes and the order they are currently running in inside opt, we can use the --debug-pass-manager flag. For instance, given the following C code, test.c, we will see the following:

  int bar(int x) {
    int y = x;
    return y * 4;
  int foo(int z) {
    return z + z * 2;

We first generate the IR for it using the following command:

$ clang -O0 -Xclang -disable-O0-optnone -emit-llvm -S test.c

204 Working with PassManager and AnalysisManager

        The -disable-O0-optnone flag
        By default, clang will attach a special attribute, optnone, to each
        function under the -O0 optimization level. This attribute will prevent any
        further optimization on the attached functions. Here, the -disable-
        O0-optnone (frontend) flag is preventing clang from attaching to this
        attribute.

Then, we use the following command to print out all of the Passes running under the optimization level of -O2:

$ opt -O2 --disable-output --debug-pass-manager test.ll Starting llvm::Module pass manager run. … Running pass: Annotation2MetadataPass on ./test.ll Running pass: ForceFunctionAttrsPass on ./test.ll … Starting llvm::Function pass manager run. Running pass: SimplifyCFGPass on bar Running pass: SROA on bar Running analysis: DominatorTreeAnalysis on bar Running pass: EarlyCSEPass on bar … Finished llvm::Function pass manager run. … Starting llvm::Function pass manager run. Running pass: SimplifyCFGPass on foo … Finished llvm::Function pass manager run. Invalidating analysis: VerifierAnalysis on ./test.ll … $

                                      Learning instrumentations in the new PassManager   205

The preceding command-line output tells us that opt first runs a set of module-level optimizations; the ordering of those Passes (for example, Annotation2MetadataPass and ForceFunctionAttrsPass) are also listed. After that, a sequence of functionlevel optimizations is performed on the bar function (for example, SROA) before running those optimizations on the foo function. Furthermore, it also shows the analyses used in the pipeline (for example, DominatorTreeAnalysis), as well as prompting us with a message regarding they became invalidated (by a certain Pass). To sum up, --debug-pass-manager is a useful tool to peek into the Passes and their ordering run by the Pass pipeline at a certain optimization level. Knowing this information can give you a big picture of how Passes and analyses interact with the input IR.

Printing changes to the IR after each Pass To understand the effects of a particular transformation Pass on your target program, one of the most straightforward ways is to compare the IR before and after it is processed by that Pass. To be more specific, in most cases, we are interested in the changes made by a particular transformation Pass. For instance, if LLVM mistakenly removes a loop that it shouldn't do, we want to know what Pass did that, and when the removal happened in the Pass pipeline. By using the --print-changed flag (and some other supported flags that we will introduce shortly) with opt, we can print out the IR after each Pass if it was ever modified by that Pass. Using the test.c (and its IR file, test.ll) example code from the previous paragraph, we can use the following command to print changes, if there are any, made by each Pass:

  $ opt -O2 --disable-output --print-changed ./test.ll
  *** IR Dump At Start: ***
  define dso_local i32 @bar(i32 %x) #0 {
  entry:
    %x.addr = alloca i32, align 4
    %y = alloca i32, align 4
    …
    %1 = load i32, i32* %y, align 4
    %mul = mul nsw i32 %1, 4
    ret i32 %mul

206 Working with PassManager and AnalysisManager

*** IR Dump After VerifierPass (module) omitted because no change *** … *** IR Dump After SROA *** (function: bar) ; Function Attrs: noinline nounwind uwtable define dso_local i32 @bar(i32 %x) #0 { entry: %mul = mul nsw i32 %x, 4 ret i32 %mul $

Here, we have only shown a small amount of output. However, in the highlighted part of the snippet, we can see that this tool will first print out the original IR (IR Dump At Start), then show the IR after it is processed by each Pass. For example, the preceding snippet shows that the bar function has become much shorter after the SROA Pass. If a Pass didn't modify the IR at all, it will omit the IR dump to reduce the amount of noise. Sometimes, we are only interested in the changes that have happened on a particular set of functions, say, the foo function, in this case. Instead of printing the change log of the entire module, we can add the --filter-print-funcs=<function names> flag to only print IR changes for a subset of functions. For example, to only print IR changes for the foo function, you can use the following command:

  $ opt -O2 --disable-output \
            --print-changed --filter-print-funcs=foo ./test.ll

Just like --filter-print-funcs, sometimes, we only want to see changes made by a particular set of Passes, say, the SROA and InstCombine Passes. In that case, we can add the --filter-passes=<Pass names> flag. For example, to view only the content that is relevant to SROA and InstCombine, we can use the following command:

  $ opt -O2 --disable-output \
            --print-changed \
            --filter-passes=SROA,InstCombinePass ./test.ll
                                       Learning instrumentations in the new PassManager   207

Now you have learned how to print the IR differences among all the Passes in the pipeline, with additional filters that can further focus on a specific function or Pass. In other words, this tool can help you to easily observe the progression of changes throughout the Pass pipeline and quickly spot any traces that you might be interested in. In the next section, we will learn how to debug problems raised in the code optimization by bisecting the Pass pipeline.

Bisecting the Pass pipeline In the previous section, we introduced the --print-changed flag, which prints out the IR change log throughout the Pass pipeline. We also mentioned that it is useful to call out changes that we are interested in; for instance, an invalid code transformation that caused miscompilation bugs. Alternatively, we can also bisect the Pass pipeline to achieve a similar goal. To be more specific, the --opt-bisect-limit=<N> flag in opt bisects the Pass pipeline by disabling all Passes except the first N ones. The following command shows an example of this:

$ opt -O2 --opt-bisect-limit=5 -S -o – test.ll BISECT: running pass (1) Annotation2MetadataPass on module (./ test.ll) BISECT: running pass (2) ForceFunctionAttrsPass on module (./ test.ll) BISECT: running pass (3) InferFunctionAttrsPass on module (./ test.ll) BISECT: running pass (4) SimplifyCFGPass on function (bar) BISECT: running pass (5) SROA on function (bar) BISECT: NOT running pass (6) EarlyCSEPass on function (bar) BISECT: NOT running pass (7) LowerExpectIntrinsicPass on function (bar) BISECT: NOT running pass (8) SimplifyCFGPass on function (foo) BISECT: NOT running pass (9) SROA on function (foo) BISECT: NOT running pass (10) EarlyCSEPass on function (foo) define dso_local i32 @bar(i32 %x) #0 { entry: %mul = mul nsw i32 %x, 4 ret i32 %mul define dso_local i32 @foo(i32 %y) #0 {

208 Working with PassManager and AnalysisManager

  entry:
    %y.addr = alloca i32, align 4
    store i32 %y, i32* %y.addr, align 4
    %0 = load i32, i32* %y.addr, align 4
    %1 = load i32, i32* %y.addr, align 4
    %mul = mul nsw i32 %1, 2
    %add = add nsw i32 %0, %mul
    ret i32 %add
  $

(Note that this is different from examples shown in the previous sections; the preceding command has printed both messages from --opt-bisect-limit and the final textual IR.) Since we implemented the --opt-bisect-limit=5 flag, the Pass pipeline only ran the first five Passes. As you can see from the diagnostic messages, SROA was applied on bar but not the foo function, leaving the final IR of foo less optimal. By changing the number that follows --opt-bisect-limit, we can adjust the cut point until certain code changes appear or a certain bug is triggered (for example, a crash). This is particularly useful as an early filtering step to narrow down the original problem to a smaller range of Passes in the pipeline. Furthermore, since it uses a numeric value as the parameter, this feature fits perfectly to automating environments such as automatic crash reporting tools or performance regression tracking tools. In this section, we introduced several useful instrumentation tools in opt for debugging and diagnosing the Pass pipeline. These tools can greatly improve your productivity when it comes to fixing problems, such as compiler crashes, performance regressions (on the target program), and miscompilation bugs.

Summary In this chapter, we learned how to write an LLVM Pass for the new PassManager and how to use program analysis data within a Pass via the AnalysisManager. We also learned how to leverage various instrumentation tools to improve the development experiences while working with the Pass pipeline. With the skills gained from this chapter, you can now write a Pass to process LLVM IR, which can be used to transform or even optimize a program.

                                                                             Questions      209

These topics are some of the most fundamental and crucial skills to learn before starting on any IR level transformation or analysis task. If you have been working with the legacy PassManager, these skills can also help you to migrate your code to the new PassManager system, which has now been enabled by default. In the next chapter, we will show you various tips along with the best practices that you should know when using the APIs of LLVM IR.

Questions
  1. In the StrictOpt example of the Writing an LLVM Pass for the new PassManager
     section, how can you write a Pass without deriving the PassInfoMixin class?
  2. How can you develop a custom instrumentation for the new PassManager?
     Additionally, how can you do this without modifying the LLVM source tree?
     (Hint: think about the Pass plugin that we learned about in this chapter.)
← / → change chapter. Esc returns to the main menu. Click a figure to zoom.