[!IMPORTANT]
TL;DR — what you must remember: The New Pass Manager replaces the legacy one with value-semantics passes, explicit analysis dependencies via AnalysisManager, and proxies that thread module/function/loop analyses across IR-unit boundaries. Analyses are cached and invalidated precisely; pipelines are composed declaratively (-passes=...). The reason NPM exists: cheaper analysis reuse and faster pipelines than the legacy manager's implicit, re-computed dependencies.
LLVM's transformation and analysis logic is built on two primary engines:
PassManager<IRUnitT>: The executor. It holds a sequence of passes and runs them one by one on a given IR unit (Module, Function, etc.).AnalysisManager<IRUnitT>: The state provider. It runs analysis passes, caches the results, and hands them to transformation passes when requested.IRUnitT)The Pass Manager is templated over the IR unit it operates on. The most common are:
Module: The entire translation unit.Function: A single function.Loop: A single loop within a function.CGSCC: Call Graph Strongly Connected Components.PassManager?Think of a PassManager as a composite pass. It implements the same interface as a single pass (run() method), but its implementation iterates through a std::vector of "Pass Concepts."
PassConcept: An internal polymorphic wrapper (Type Erasure) that allows the PassManager to store different types of passes in the same container.run() method: When you call MPM.run(Module, AM), it:
AnalysisManager for any required analyses.run() method.The AnalysisManager (AM) is much more than a list. It is a sophisticated cache that ensures an analysis (like DominatorTree) is only computed when absolutely necessary.
AM.getResult<MyAnalysis>(IR).MyAnalysis has already been run on this IR unit and if the result is still valid.MyAnalysis::run() method, stores the result, and returns it.PreservedAnalyses CatastropheThis is the #1 correctness concern for LLVM developers.
When a Transformation Pass finishes, it returns a PreservedAnalyses object. This tells the PassManager which analyses are still "valid" (i.e., the pass didn't change the IR in a way that breaks the analysis).
If you incorrectly claim an analysis is preserved when it's not:
AnalysisManager keeps the old, now-incorrect analysis result in its cache.DominatorTree that says Block A dominates Block B, even though you just added a branch that bypasses A).[!CAUTION]
No Crash, No Assertion. The compiler finishes successfully. The only way you find out is when your production code starts returning 4 instead of 5.
To add a new pass, you typically follow these steps:
Most optimizations happen at the function level.
#include "llvm/IR/PassManager.h" #include "llvm/Analysis/DominatorTree.h" class MyFunctionPass : public PassInfoMixin<MyFunctionPass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { // 1. Request an analysis auto &DT = AM.getResult<DominatorTreeAnalysis>(F); bool Changed = false; // ... perform transformations using DT ... if (!Changed) return PreservedAnalyses::all(); // Everything is still valid // 2. Be honest about what we broke PreservedAnalyses PA; PA.preserve<DominatorTreeAnalysis>(); // We kept the DT valid // Everything else is invalidated by default return PA; } };
Used for IPO (Interprocedural Optimization) or global analysis.
class MyModulePass : public PassInfoMixin<MyModulePass> { public: PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM) { // Accessing function-level analyses from a module pass: auto &FAM = AM.getResult<FunctionAnalysisManagerModuleProxy>(M).getManager(); for (auto &F : M) { if (F.isDeclaration()) continue; auto &DT = FAM.getResult<DominatorTreeAnalysis>(F); // ... analyze ... } return PreservedAnalyses::all(); } };
Requires a LoopAnalysisManager.
class MyLoopPass : public PassInfoMixin<MyLoopPass> { public: PreservedAnalyses run(Loop &L, LoopAnalysisManager &AM, LoopStandardAnalysisResults &AR, LPMUpdater &U) { // LoopStandardAnalysisResults (AR) provides DT, LI, SE, etc. easily DominatorTree &DT = AR.DT; // ... loop optimizations ... return PreservedAnalyses::all(); } };
You don't just "run" a pass; you add it to a pipeline via PassBuilder.
PassBuilder about your custom analyses.-passes='my-pass') to build the pipeline.// In your Tool or Plugin: PassBuilder PB(TM); // 1. Register Analysis Managers ModuleAnalysisManager MAM; FunctionAnalysisManager FAM; LoopAnalysisManager LAM; CGSCCAnalysisManager CGAM; // 2. Register Proxies (The "Glue" between managers) PB.registerModuleAnalyses(MAM); PB.registerCGSCCAnalyses(CGAM); PB.registerFunctionAnalyses(FAM); PB.registerLoopAnalyses(LAM); PB.crossRegisterProxies(LAM, FAM, CGAM, MAM); // 3. Create the Pass Manager ModulePassManager MPM; FunctionPassManager FPM; // Add passes FPM.addPass(MyFunctionPass()); MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM))); // 4. Run! MPM.run(MyModule, MAM);
PassInfoMixin.AM.getResult<T>(IR) to get data.PreservedAnalyses::all() if no changes were made.DominatorTree.PassBuilder if you want to use it via opt.Answer: Cytron's algorithm produces Minimal SSA form in two phases: φ-insertion and Variable Renaming.
X is the set of blocks where X's dominance "stops" (i.e., X dominates a predecessor of Y, but does not strictly dominate Y).v defined in block B, any block in DF(B) needs a φ-function for v, because DF(B) is the first place where a path from B merges with a path from somewhere else.DF(B). But wait: adding a φ-function is a new definition of v! So, we must now compute the DF of the block where we just added the φ-function, and place more φ-functions there if needed. This recursive closure is the Iterated Dominance Frontier (IDF).v. We pop B, place φ for v in all Y ∈ DF(B), and push Y to the worklist if it hasn't been processed.v (e.g., stack[v]).A = ..., we push a new version (e.g., A_1) onto stack[A]. Uses of A are replaced by top(stack[A]).Answer:
A loop header H has at least two incoming edges: one from outside the loop (the pre-header) and one from inside the loop (the backedge from a latch block L).
Because H dominates L, and L has an edge to H, H dominates a predecessor of H (which is L). But H does not strictly dominate itself (by definition, strict dominance requires A != B). Thus, H satisfies the exact definition of being in DF(H).
This fundamental property is why loop variables (induction variables) elegantly and automatically get φ-functions placed exactly at the loop header in SSA form.
Answer:
The Iterated Dominance Frontier (IDF) accounts for the fact that a φ-function acts as a new definition.
If block A defines x, we place a φ-function in B ∈ DF(A). Block B now defines x (via the φ-function). If there's another join point C ∈ DF(B), C needs a φ-function bridging the value from B and some other path. The standard DF only calculates the immediate frontier. The IDF takes the transitive closure IDF(S) = DF(S ∪ DF(S)), ensuring φ-functions propagate through chains of control-flow merges.
mem2reg work in LLVM? What does it actually do to alloca instructions?Answer:
mem2reg is the LLVM pass that promotes memory allocations (alloca) to SSA registers.
Front-ends (like Clang) emit alloca for all local variables, generating loads and stores. This avoids complex SSA construction in the front-end.
mem2reg looks for alloca instructions that meet specific criteria (only accessed via direct load and store, address never taken/escaped).
alloca is stored to (defining blocks).alloca'd value.alloca based on the sequence of stores and φ-functions, and replaces subsequent loads with direct SSA values.alloca, load, and store instructions. The memory-bound variable is purely virtual registers now.Answer:
A φ-function has exactly one argument for every incoming CFG edge to the basic block. It has >2 arguments when a block has >2 predecessors. A common example is a switch statement where multiple cases break into the same exit block, or a sequence of if-else if-else statements merging at a single downstream block.
Answer:
CSSA (Conventional SSA) is a specific flavor of SSA where all variables related by a φ-function (both the definition and all its operands) have non-overlapping, "compatible" live ranges.
When doing out-of-SSA translation (getting rid of φ-functions to generate machine code), we usually insert copies on predecessor edges. But these copies can interfere. CSSA ensures that for any φ-function v = φ(a, b), v, a, b can be safely coalesced into the exact same physical register without conflicts. If they overlap in standard SSA, a CSSA-building pass inserts extra dummy copies to split the live ranges, resolving the "Lost Copy" and "Swap" problems before Register Allocation.
An instruction is loop invariant if its operands are all either constants, defined outside the loop, or themselves loop invariant.
Licm is entirely bound by Alias Analysis. To hoist x = load p, the compiler asks AA: "Does any store in this loop alias p?". If AA returns MayAlias for *q = y inside the loop, LICM is blocked, because p and q might overlap, changing p's value per iteration.
Local CSE (Common Subexpression Elimination) only eliminates redundancies within a single Basic Block. GVN works globally across the entire CFG using dominator trees to prove values are equivalent across branches.
A "value number" is a unique ID (often a hash) representing a specific computation (Opcode + ValueNum(Op1) + ValueNum(Op2)). If a = x + y gets Value #5, later when evaluating b = x + y, GVN sees the operands have the same value numbers, calculates the same hash #5, and replaces b with a.
GVN tries to number memory states. Using MemorySSA, GVN assigns a value number to a load based on the pointer and the current memory version (MemoryUse token). What can go wrong is aliasing. If of an intervening opaque store occurs, GVN must pessimistically assume the memory changed, increment the memory version, and fail to optimize the redundant load.
In SSA, standard Dead Code Elimination is trivial O(N). Because every variable has one definition and explicit use-lists, a pass iterates over instructions: if use_count == 0 and the instruction has no side-effects (not a volatile load, store, or call), delete it. Done.
Standard DCE only deletes instructions whose outputs are clearly unused. But what about a dead cycle? a = b + 1; b = a + 1; (with no outside uses). Standard DCE sees use_count > 0 for both and leaves them.
Aggressive DCE (ADCE) works backwards. It assumes everything is dead initially, marks only "roots" (returns, side-effects) as live, and recursively marks their operands as live. Anything left unmarked at the end is deleted. This naturally kills dead cycles.
SCCP propagates constants through a CFG while simultaneously figuring out which branches of CFG execution are actually reachable (dead branch elimination).
Simple folding just looks at x = 2 + 3 -> x = 5. SCCP evaluates a branch if (x == 5) as True, completely skips visiting the False branch, and propagates constants assuming the False branch never executes.
"Sparse" means it doesn't propagate values linearly block-by-block. It uses SSA def-use chains (the sparse graph) to directly jump from a definition to its uses, regardless of where they are in the CFG, skipping over instructions that don't depend on the value. This makes it incredibly fast.
SCEV mathematically analyzes how loop variables change. Instead of seeing i = i + 1, it models i as a polynomial or recurrence relation over loop iterations, like {0, +, 1} (starts at 0, adds 1 per iteration). It is used to compute exact loop trip counts and array access patterns.
Replacing expensive operations inside a loop with cheaper ones.
Example: for(i=0; i<N; ++i) { a[i] = i * 4; }
Multiplier * 4 is expensive. Strength reduction transforms this into an accumulator using addition:
int temp = 0; for(i=0; i<N; ++i) { a[i] = temp; temp += 4; }
Loop interchange (swapping inner and outer loops to improve cache locality) is legal if it preserves all data dependencies (true, anti, and output dependencies) defined by the loop distance vectors. No iteration (i, j) can read/write data that was meant to be written/read by a later/earlier iteration in the original order.
Hoisting a loop-invariant conditional check outside the loop, and duplicating the loop body for the True and False paths. Profitable because it removes a branch from the inner loop, enabling better vectorization and software pipelining, at the cost of code size.
TBAA uses the source language's type system rules (like C++ Strict Aliasing) to prove pointers don't alias. In C++, an int* and a float* are disallowed from pointing to the same memory. LLVM generates TBAA metadata nodes on loads/stores. The TBAA pass reads this metadata; if the types are fundamentally distinct on the TBAA hierarchy tree, it returns NoAlias.
p and q ever alias anywhere in the entire program. (Fast, less precise).p and q might alias at line 10, but after q = new int() at line 15, they definitely NoAlias at line 20. (Slower, highly precise). LLVM's basic AA is mostly flow-insensitive, augmented by MemorySSA for flow-sensitive queries.It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.