Using LLVM’s New Pass Manager to Write, Register, and Run Custom Optimization Passes
Learn how to adopt LLVM 15+’s New Pass Manager (NPM) to create, register, and execute custom optimization passes. Follow a step‑by‑step guide, verify correctness, and handle fallbacks.
27 Apr 2026, 00:45 UTC

Desired Outcome
After completing this guide you will be able to:
- Write a custom LLVM optimization pass that targets the New Pass Manager (NPM).
- Register the pass so that it can be invoked via
optor the compiler driver. - Run, debug, and verify the pass using the NPM‑specific command‑line options.
- Understand how to fall back to the legacy pass manager if necessary.
Prerequisites
- LLVM 15 or newer installed on your system. You can verify the version with
llvm-config --version. - Basic familiarity with C++ and LLVM’s IR.
- Development tools: a C++ compiler (e.g., clang++),
make, andoptfrom the LLVM distribution. - Permission to build and run programs in your environment (root not required).
Step‑by‑Step Procedure
1. Create the Pass Source File
Define a new pass by inheriting from PassInfoMixin and overriding the run method. The following example implements a trivial ModulePass that counts the number of functions in a module.
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Passes/PassPlugin.h>
#include <llvm/IR/Module.h>
#include <llvm/Support/raw_ostream.h>
using namespace llvm;
class FunctionCountPass : public PassInfoMixin<FunctionCountPass> {
public:
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM) {
unsigned count = 0;
for (auto &F : M) {
if (!F.isDeclaration()) ++count;
}
outs() << "FunctionCountPass: " << count << " functions\n";
return PreservedAnalyses::all();
}
};
// Register the pass with the NPM.
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo LLVMGetPassPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "FunctionCountPass", "0.1", [](PassBuilder &PB) {
PB.registerCustomPassCreator([]() {
return std::make_unique<FunctionCountPass>();
});
}};
}
Save this as FunctionCountPass.cpp. The LLVMGetPassPluginInfo function is the NPM hook that injects the pass into the pipeline.
2. Build the Pass as a Shared Library
Compile the source into a shared object that opt can load. Use llvm-config to pull the correct compiler flags.
CXX=$(llvm-config --cxx)
CXXFLAGS=$(llvm-config --cxxflags) $(llvm-config --ldflags)
LIBDIR=$(llvm-config --libdir)
$CXX -fPIC -shared FunctionCountPass.cpp -o FunctionCountPass.so $CXXFLAGS -L$LIBDIR -lLLVM
Ensure the resulting FunctionCountPass.so is in the library search path or set LD_LIBRARY_PATH accordingly.
3. Prepare a Test IR File
Create a minimal LLVM IR file to exercise the pass. For example, test.ll:
; ModuleID = 'test'
source_filename = "test.c"
define i32 @main() {
entry:
ret i32 0
}
4. Run the Pass with NPM Enabled
Invoke opt with the -passes flag to route all passes through the NPM. Use the -passes=custom-pass syntax to run only your pass.
opt -passes=custom-pass -analyze -load FunctionCountPass.so test.ll
Expected output:
FunctionCountPass: 1 functions
The -analyze flag tells opt to run the pass in analysis mode, which is safe for passes that do not modify IR. If you want to apply transformations, omit -analyze and use -S to emit the transformed IR.
5. Verify the Pass Behavior
Use the -S option to generate the post‑pass IR and compare it to the original to confirm no unintended changes:
opt -passes=custom-pass -S test.ll > out.ll
# Diff with original
cmp -s test.ll out.ll && echo "No changes" || echo "IR modified"
Because FunctionCountPass is read‑only, the output should match the input.
6. Benchmark Performance (Optional)
To assess the impact of NPM on compilation time, run a medium‑sized C++ project twice: once with -passes=default (enabling NPM) and once with -passes=legacy (disabling NPM). Measure the elapsed time using time or a build system’s statistics.
# With NPM
clang++ -O3 -passes=default -S -o /dev/null large.cpp
# With Legacy
clang++ -O3 -passes=legacy -S -o /dev/null large.cpp
Compare the durations to identify any regressions.
Verification Checks
- Pass Execution: The console must display the expected output message from the pass.
- IR Integrity: The post‑pass IR should be identical to the input if the pass is read‑only.
- Performance: Compilation time with NPM should be equal to or better than legacy mode for typical workloads.
- Fallback: Running
opt -passes=legacyshould still execute legacy passes, confirming graceful degradation.
Recovery Options
- If the pass fails to load, check that
LD_LIBRARY_PATHincludes the directory containingFunctionCountPass.soand that the library was built with the same LLVM version. - When debugging, enable detailed pass structure output:
opt -passes=custom-pass -debug-pass=Structure. This prints the pass pipeline and can help locate mis‑registers. - For passes that depend on global state, run
opt -passes=custom-pass -analyze -debug-pass=Structureto inspect the analysis results and ensure the pass behaves as expected. - If you encounter undefined behavior after upgrading LLVM, consider recompiling all custom passes against the new API and re‑testing.
Practical Tips
- When writing multiple passes, group them in a single shared library to reduce load overhead.
- Use the
PassBuilderhooks to schedule passes after specific built‑in passes (e.g., aftermem2reg). - Keep the pass interface minimal; return
PreservedAnalyses::all()when no IR changes are made to avoid unnecessary recomputation. - Document each pass’s intended transformation and any assumptions about the input IR to aid future maintenance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.