Migrating to the LLVM New Pass Manager: Modularizing Your IR Pipeline
Learn how to transition from the LLVM Legacy Pass Manager to the New Pass Manager (NPM) to improve IR pipeline efficiency and modularity.
02 Apr 2026, 01:49 UTC

The Problem: Imperative Bloat in Optimization Pipelines
For years, LLVM's Legacy Pass Manager relied on an imperative style where passes were essentially a sequence of instructions executed on a module. As LLVM grew, this approach created a bottleneck: the legacy manager struggled with efficient analysis caching and lacked a clean way to compose passes without significant boilerplate. If you were writing custom transformations, you likely dealt with a rigid inheritance hierarchy that made it difficult to target specific IR (Intermediate Representation) levels—like a single function—without loading the entire module into memory.
The takeaway is simple: the New Pass Manager (NPM) shifts LLVM from a "list of tasks" model to a functional, modular pipeline. By utilizing the NPM, you can reduce memory overhead and improve the maintainability of your optimization passes through better type safety and granular control.
From Module-Centric to Function-Centric
The legacy manager often treated the Module as the primary unit of work. In large projects, this meant the entire program's IR had to be resident and managed together, even for local optimizations. The NPM introduces a more hierarchical approach.
In the NPM, the pipeline is structured to understand the relationship between the Module, Function, and Loop. This allows LLVM to run a pass on a single function, cache the analysis results for that specific function, and move to the next without invalidating the entire module's state. This granularity is achieved through a functional interface where passes are no longer just "run" but are composed into a pipeline that the PassManager executes efficiently.
Implementing a Pass with PassInfoMixin
In the legacy system, you inherited from FunctionPass or ModulePass. In the NPM, you use the PassInfoMixin template. This template provides the necessary metadata (like the pass name) and allows the PassBuilder to integrate your pass into the standard optimization pipeline.
The primary difference is the run method. Instead of returning a boolean to indicate if the IR was modified, the NPM uses a specific PreservedAnalyses object. This tells the manager exactly which analysis results (like Dominator Trees or Loop Info) are still valid, preventing expensive re-computations.
Example: A Simple IR Logger Pass
To implement a basic pass in a modern LLVM environment (assuming LLVM 14+), you define a class that mixes in PassInfoMixin. This example demonstrates a pass that simply logs the name of every function it visits.
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
struct MyLoggerPass : public PassInfoMixin<MyLoggerPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM)
{
errs() << "Visiting function: " < F.getName() < "\n";
// We didn't change the IR, so all analyses are preserved
return PreservedAnalyses::all();
}
};
To execute this, you would typically register it via the PassBuilder in your plugin or tool. To verify the pipeline sequence without writing a full driver, you can run the following command in your terminal:
# Run this on a sample .c file to see the NPM pipeline in action
clang -O3 -mllvm -print-pipeline-passes input.c > /dev/null
Expected Check: The output will list a nested sequence of passes (e.g., FunctionPassManager containing SimplifyCFG, InstCombine, etc.), confirming that the NPM is managing the execution flow.
Trade-offs and Limitations
The transition to NPM is not without friction. The initialization process is more complex; you can no longer simply instantiate a PassManager and add passes to it. You now need a PassBuilder, a FunctionAnalysisManager, and a ModuleAnalysisManager to coordinate the pipeline.
Additionally, because the legacy manager has been deprecated or removed in recent versions, any existing legacy passes must be completely rewritten. There is no automated migration tool for the C++ logic of a pass; only the structural way the pass is called has changed.
Verifying the Result
To ensure your custom NPM pass is functioning correctly, use the opt tool to compare IR outputs. Run the tool on a bitcode file before and after your pass:
- Generate bitcode:
clang -emit-llvm -c input.c -o input.bc - Run the pass:
opt -load-pass-plugin ./MyPass.so -passes="my-logger-pass" input.bc -S -o output.ll - Check the
output.llfile to ensure the expected transformations occurred.
If the output IR is identical to the input but your pass was intended to modify code, check your PreservedAnalyses return value. Returning PreservedAnalyses::all() when you have actually modified the IR can lead to silent bugs in subsequent passes that rely on outdated analysis data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.