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:
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);
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{});
});
}};
}
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:
Cons:
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.