Blog

Figuring out how to run a pass with LLVM feels needlessly complicated. This short post will briefly lay out two different methods of doing so using the new Pass Manager.

One simple method of running a compiler pass is to directly invoke it via Clang as outlined in this blog post.

This method is fairly straightforward and super easy, we just need to:

  1. Create a class that inherits from PassInfoMixin, as described here.

    struct MyPass : public PassInfoMixin<MyPass> {
        PreservedAnalyses run(Module& M, ModuleAnalysisManager& AM)
        {
            outs() << "Hello World\n";
            // Your code here
    
            return PreservedAnalyses::all();
        };
    }
    

    If you’re unfamiliar, this bit of inheriting from a template where the derived type is the template parameter is known as the Curiously Recurring Template Pattern (CRTP).

    Note that this example is a module pass, if you wanted a function pass you can just change the signature of run to

    PreservedAnalyses run(Function& F, FunctionAnalysisManager& AM);
    

    Or for a loop pass:

    PreservedAnalyses run(Loop &L, LoopAnalysisManager &AM, 
        LoopStandardAnalysisResults &AR, LPMUpdater &U);
    
  2. Register your pass by adding the following:

    extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
    llvmGetPassPluginInfo()
    {
        return {.APIVersion = LLVM_PLUGIN_API_VERSION,
                .PluginName = "MyPass",
                .PluginVersion = "v0.1",
                .RegisterPassBuilderCallbacks = [](PassBuilder& PB) {
                    PB.registerOptimizerEarlyEPCallback(
                        [](ModulePassManager& PM, OptimizationLevel /* Level */) {
                            PM.addPass(MyPass{});
                        });
                }};
    }
    
  3. Compile the pass into a pass plugin library and run your pass with Clang by giving it the argument -fpass-plugin=PATH_TO_LIB.

For more details, you can look at this repo.

Pros:

  • Single command line argument to Clang

Cons:

  • No custom command line arguments for your pass
  • Not much control over when your pass is scheduled

The PassBuilder callbacks provide you with some control over when your pass is scheduled, but it’s at a very coarse granularity. The registerOptimizerEarlyEPCallback will schedule the pass before pretty much every other pass. For example, if you’d like mem2reg to have been run first, then this extension point is not for you. The registerOptimizerLateEPCallback will schedule the pass after almost all other LLVM passes. If your pass enables more optimizations for other standard LLVM passes, then this extension point is also not for you. There are a few other extension points, but they all have similar flavors like registering your pass at the end of the loop optimization pipeline. If you need precise control over when you’re pass is run, you’re probably going to have to run the pass using opt.

Opt is LLVM’s modular optimizer and it gives you full control over which passes are run and how they are ordered. Step 1 in the previous section does not change. The first difference is in step 2. Now we need to register a pipeline parsing callback like so:

extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo()
{
    return {.APIVersion = LLVM_PLUGIN_API_VERSION,
            .PluginName = "MyPass",
            .PluginVersion = "v0.1",
            .RegisterPassBuilderCallbacks = [](PassBuilder& PB) {
                // for usage with opt
                PB.registerPipelineParsingCallback(
                    [](auto Name, ModulePassManager& PM,
                       auto /* PipelineElement*/) {
                        if (Name == "my-pass") {
                            // if opt command line argument matches "my-pass"
                            // add the pass
                            PM.addPass(MyPass{});
                            return true;
                        }
                        return false;
                    });
                // for usage with clang
                PB.registerOptimizerEarlyEPCallback(
                    [](ModulePassManager& PM, OptimizationLevel /* Level */) {
                        PM.addPass(MyPass{});
                    });
            }};
}

The callback will return true when the pass has been added and false otherwise. When Name matches the given string, that’s when we’ll know to add our pass. The string we choose will be the string we specify on the command line to run our pass with opt. It does not have to match the PluginName field we registered the pass plugin with.

Now opt operates on LLVM IR. So to run a pass with opt we’re going to have to generate LLVM from Clang. This can be done with clang++ file.c -S -emit-llvm.

But there’s a catch, when Clang generates code with optimizations disabled, it adds the optnone LLVM attribute to all functions. The LLVM middle-end will not optimize such functions, so what we need to do is enable optimizations with clang and then pass a flag through clang directly to the optimizer to prevent any LLVM optimizations from occurring.

clang++ -O1 -S -emit-llvm -mllvm -disable-llvm-optzns file.c -o file.ll

The -mllvm flag will tell Clang to pass the next flag or option directly to LLVM. In this case, we want it to pass -disable-llvm-optzns.

The result of this command is the program in LLVM IR textual format. opt accepts both the IR textual format and the IR as bitcode. opt will output the optimized program as LLVM bitcode.

Now we can run our pass like so:

opt-17 -load-pass-plugin PATH_TO_PASS_LIB -passes="my-pass" file.ll -o file.bc

We can turn a bitcode file back into the textual format with llvm-dis file.bc -o file.ll and we can compile the IR (bitcode or textual) into assembly with llc FILE_NAME -o file.s.

From here, we now need to run an assembler and linker to produce an executable. We can use both of these directly, or, just use Clang like so:

clang++-17 file.s -o file -no-pie && ./file

The method I just discussed does not allow for position-independent executables (PIE), so we need to tell this to the linker via the -no-pie flag.

Here’s one whole shell command to do this entire pipeline:

clang++-17 -O1 -mllvm -disable-llvm-optzns {filename} -emit-llvm -S -o /dev/stdout \
    | opt-17 -load-pass-plugin PATH_TO_LIB -passes="my-pass" \
    | llc-17 -o {filebasename}.s && clang++-17 {filebasename}.s -no-pie -o a.out

Now to run some pass, say mem2reg, before our pass, we can just specify it first in the -passes argument of opt like opt-17 -load-pass-plugin PATH_TO_LIB -passes="mem2reg,my-pass".

This works fine if my-pass is a function pass like mem2reg, but this won’t work if my-pass is a module pass because opt expects passes in order of the LLVM hierarchy. In other words, it expects the module passes before the function passes, and the function passes before the loop passes. So if my-pass was a function or loop pass the above command would work. Similarly, if mem2reg was a module pass then the above command would also work.

So what to do? Well we can chain together multiple invocations of opt, or we can specify the granularity of our passes like so:

opt-17 -load-pass-plugin PATH_TO_LIB -passes="function(mem2reg,sroa),module(my-pass)"

Using either method, we can create the desired orderings of different passes regardless of their granularity.

Another benefit of using opt is that we can register command line arguments to customize our pass.

To do this, we simply create static global constants in the same file we registered our pass plugin in:

static cl::opt<bool> EnablePrint(
    "ive-print" /* option name */, 
    cl::desc("Enable printing of detected induction variables") /*option description*/,
    cl::init(false) /*default value*/);

static cl::opt<bool> EnableDebug(
    "ive-debug", cl::desc("Enable printing of debug information"),
    cl::init(false));

Then, when running opt we can just specify our options like any other command line flag or option.

opt-17 -load-pass-plugin PATH_TO_LIB -ive-print -passes="ive"

LLVM’s command line utility library supports more than just boolean flags, you can read more here.

Hopefully, this quick post on scheduling passes was helpful. In general, if you need precise control of what optimizations are run and when or if you want custom command line arguments, then using opt is likely your best bet. Otherwise, if you want something quick and coarse, it’s probably easier just to invoke your pass with Clang.


Source