LLVM's BranchProbabilityInfo assigns every
multi-successor terminator a probability distribution over its
successors. This post describes the estimation used when no profile is
available and reimplements it as a standalone program.
The cascade
BranchProbabilityInfo::calculate tries each source of
information in turn and takes the first that succeeds:
1 |
|
calcMetadataWeights translates !prof branch
weights, so with a PGO profile the distribution comes straight from
metadata. Every later step exists for functions that have none.
The last three heuristics each inspect one condition — a pointer
comparison, a test against a constant, ordered versus unordered floats —
and decide one branch in isolation. They and the loop branch heuristic
behind the LBH_ constants below come from Ball and Larus's
Branch Prediction for Free (PLDI 1993), though nothing in the
tree cites it. Wu and Larus combined such heuristics into probabilities
with Dempster–Shafer evidence; LLVM takes the first that succeeds.
calcEstimatedHeuristics is the odd one out, and the
subject of this post. No paper stands behind it: it arrived in 2020,
unifying what had been separate unreachable, cold-call, loop, and invoke
heuristics. It is a whole-function analysis because branch probability
is not a local property: given
br i1 %c, label %a, label %b, nothing at the terminator
distinguishes the two edges — what distinguishes them is what
%a and %b lead to. So it classifies blocks
first, then reads a branch's probabilities off the classifications of
its successors. The classification is a pure function of the CFG and its
loops: blocks known to be bad — unreachable, noreturn, cold
— pull probability away from the branches that lead to them, and loops
are treated as units so that staying in a loop is far likelier than
leaving it. This is the only step that needs the loop structure, and the
only one expressible over a bare CFG; the others need the instructions.
The program below implements it, omitting the other heuristics and
computeUnlikelySuccessors, a refinement that analyses
induction variables through PHI nodes.
That BlockFrequencyInfo consumes
BranchProbabilityInfo might suggest a circularity, but the
two run in opposite directions: BFI propagates forward from the entry
and needs probabilities to do it, while the estimated heuristic
propagates backward from syntactically bad blocks and needs only the
dominator trees and the loop forest. Nothing it reads comes from a
probability.
Estimating block weights
In outline:
- Seed unreachable,
noreturn, unwinding, and cold blocks with fixed weights. - Propagate each seed up the dominator tree, to every dominator the seeded block post-dominates.
- Weight a loop by the maximum over its exit edges, floored at
LOWEST_NON_ZERO. - Run two worklists to a fixpoint: a block whose successor edges are all known takes their maximum, which propagates like a seed.
- At each branch, divide loop-exiting edges by an assumed trip count, default unknown weights, and normalize.
Weights come from a small fixed scale. Despite the name, a
BlockExecWeight is not an execution estimate but one of
four ordered labels; the magnitudes exist only so a branch can compare
two successors and normalize.
1 | enum class BlockExecWeight : std::uint32_t { |
Blocks not seeded start unweighted, and are treated as
DEFAULT only when a branch needs a number.
DEFAULT is a floor, not a seed: it never propagates.
How far a seed flows backwards is the interesting part. It is not
simply pushed to all predecessors:
propagateEstimatedBlockWeight walks up the dominator
tree from the seeded block and assigns the weight to each dominator
that the seeded block post-dominates. The
post-dominance condition is what makes this meaningful: a branch that
merely can reach a noreturn block may take the
other edge, whereas one that cannot avoid it is genuinely unlikely. The
weight therefore spreads through the region where the bad outcome is
unavoidable, and stops where a bypass exists — or at a loop boundary,
since a loop is weighted as a unit.
The rest is three mutually recursive definitions over edges:
1 | weight(u→v) = weight(L) if u→v enters loop L (L does not contain u) |
An edge entering a loop takes the loop's weight because its target is a header, whose own weight says nothing about how often the loop runs. A loop is as hot as the hottest thing it can fall out to, and entered at least once even if it never exits — hence the maximum and the floor. A block's maximum is all-or-nothing — if any successor edge is still unknown, the block stays unknown — so a block counts as unlikely only when every path out is unlikely; one ordinary escape keeps it ordinary. Each maximum, once known, propagates up the dominator tree like a seed; the recursion settles in a fixpoint over two worklists, the code below enqueuing exactly the blocks and loops each new weight can affect.
From weights to probabilities
A branch takes the weight of each successor edge, applies two adjustments, and normalizes.
The first adjustment: an edge exiting a loop is scaled down by an assumed trip count.
1 | static const uint32_t LBH_TAKEN_WEIGHT = 124; |
TC is 31, so a loop is assumed to run 31 iterations and
leaving it is weighted at 1/31 of staying. This is the single number
behind almost every "the loop body is hot" conclusion LLVM draws without
a profile.
The second: an edge whose weight is still unknown falls back to
DEFAULT, which is what makes DEFAULT a floor
rather than a seed. If no successor has an estimate at all, or every
weight is ZERO, the heuristic declines: it returns false,
the branch is left alone, and the next step of the cascade gets its
turn.
The stored value is a BranchProbability, a rational
whose denominator is a compile-time constant, so only the numerator is
kept:
1 | class BranchProbability { |
A probability is therefore one 32-bit word, and comparing or scaling
two of them is integer arithmetic. D is 2^31 rather than
2^32 so that the numerator of 1.0 still fits in a uint32_t,
which also leaves UINT32_MAX free as the "unknown"
sentinel. Constructing from any other denominator rescales with
rounding, so BranchProbability(1, 3) stores 715827883 — and
a three-way even split shows up in the dumps as that number three times,
not as 0.333.
Why irreducible loops have to be kept
The heuristic asks only three things about the loop structure: is this block in a loop, is this edge entering or exiting that loop, and does one loop contain another. It never asks whether a loop is reducible.
So it wants the full loop-nesting forest — every maximal strongly connected region, nested by containment — not just its single-entry loops. A natural loop is one whose header dominates it, which is exactly the reducible case. Restricting to those loses not just the irreducible loops but the nesting around them, because the reduction has to attribute their blocks somewhere.
An analysis limited to natural loops therefore needs a fallback for irreducible regions, and the obvious one — maximal strongly connected components — is flat. A flat SCC merges an inner loop into its outer one, and the heuristic then scales the wrong set of exit edges.
A note on names, because LLVM's are confusing here.
LoopInfo had already come to mean natural loops,
so when the general notion was added it could not also be called a loop
and was named CycleInfo instead. The new word marks the
different definition, not a different object: a CycleInfo
cycle is a loop in the sense of the previous post. It is also the weaker
name — a cycle is a closed path with no distinguished entry, whereas the
DFS discovers a header and a nesting — so the program below says loop
throughout, keeping cycle for the graph-theoretic sense. Both are now
built by the same single-pass DFS, and LoopInfo then
reduces the irreducible loops to natural-loop subsets — which is why the
dominator tree it takes is, in the words of the source, "needed only for
an irreducible CFG".
Containment is the query that has to be fast, and the flat block layout from the previous post makes it an interval test rather than a set probe.
The program
Three structures feed the analysis; two are the programs already published, so they are not reprinted. The loop-nesting forest comes from the single-pass DFS of irreducible loops — visiting successors last-in-first-out, as LLVM's iterative DFS does, since for irreducible loops the forest depends on that order — and the dominator tree from the semi-NCA routine of natural loops. The post-dominator tree is new and shown below.
Input format, extending the previous posts' by one optional section:
1 | n m |
After parsing, predecessor lists are reversed: LLVM's
predecessors() walks the use list, which is the reverse of
construction order, and a later section shows this order is
observable.
The weight scale, the seeds, and the two sparse weight maps:
1 |
|
Edge classification reduces to containment of the endpoints' innermost loops — here a parent-chain walk rather than the interval test:
1 | static vector<int> loop; |
computeIdom is the semi-NCA routine made generic over
(succ, pred, root), with the reverse post-order falling out
of its DFS; post-dominators are its output on the reverse CFG, rooted at
a virtual exit:
1 | static vector<int> idom, rpo, ipdom; |
The propagation and the worklist fixpoint:
1 | static vector<int> blockWL, loopWL; |
Per-branch normalization:
1 |
|
Examples
The minimal loop first. No block is seeded, so every weight is
DEFAULT and the only signal is the loop itself:
1→3 leaves it, so its weight is divided by 31, and the
branch at the header splits 31:1.
1 | % ./bpi |
(The division is exact: 0xfffff is 2^20−1, and 2^5 ≡ 1
(mod 31) makes 2^20−1 divisible by 31.)
Next, a cold call two blocks deep. Block 3 is cold and block 1 cannot avoid it, so the seed climbs from 3 to 1 — 3 post-dominates it — and stops at 0, which has the bypass through 2.
1 | % ./bpi |
The branch at 0 reads the propagated weight: COLD
against DEFAULT is about 1:16, the gap the enum builds
in.
Nested loops: {2,3} inside {1,2,3,4}. Each
loop scales its own exit — 2→4 leaves only the inner loop,
4→5 only the outer — so both branches split 31:1
independently, even though 4 is still inside the outer loop.
1 | % ./bpi |
Finally, an irreducible dispatch: block 0 can enter at any of 1–5, each arm returns to the dispatcher 5, and 5 branches back to every arm or exits.
1 | % ./bpi |
The forest nests two irreducible loops: the DFS reaches 4 first
(successors are visited LIFO), so 4 heads the outer loop, and removing
the header leaves the strongly connected {1,2,3,5} as an
inner one — the recursion of the previous post. From 5, the edges to 1,
2, 3 stay inside the inner loop, but 5→4 leaves it, so it
is scaled like the true exit 5→6 even though 4 is still
inside the outer loop. The nesting, and therefore these numbers, is what
a flat SCC destroys.
What changed when BPI moved to CycleInfo
Before #210301, the
heuristic used natural loops as its primary structure and a flat Tarjan
SCC pass as the irreducible fallback, glued together by a two-headed
LoopBlock = {natural loop, SCC id}. Reducible loops nested
correctly: a natural loop has a single entry, so containment orders them
unambiguously. Irreducible ones did not: a maximal SCC cannot nest, so
every loop in an irreducible region collapsed into one unit.
With the full loop-nesting forest both cases are handled by the same structure, and irreducible loops nest. On the four-armed dispatch of the last example the difference is visible in the probabilities:
| reducible | irreducible | |
|---|---|---|
full loop-nesting forest (CycleInfo) |
nested | nested — 5→1,2,3 32.63%, 5→4 and
5→6 1.05% |
natural loops + flat SCC (LoopInfo +
SccInfo) |
nested | flat — 5→1..4 24.80%, 5→6 0.80% |
Matching LLVM bit-for-bit
Two details decide exact agreement: one the source mentions only in passing, one it contradicts.
The first is ordering: seeds are applied in reverse post-order and the first weight to reach a block wins, so where two propagations collide the outcome is decided by seed order.
The second is a comment that is wrong.
estimateBlockWeights drives two worklists and says:
1 |
Order is important. Consider two loops where one's exit edge enters
the other: the first loop's weight depends on the second's, and it is
never re-queued when that weight later lands. The pop order of
LoopWorkList therefore shows up in the output, and that
order follows predecessor iteration order — which for LLVM is use-list
order, the reverse of construction order. A model that iterates
predecessors in the natural order matches on almost every CFG and
disagrees on this one shape.
Machine-level branch probability
At the machine level the same information is stored, not computed.
MachineBasicBlock carries the probabilities of its own
out-edges:
1 |
|
so MachineBranchProbabilityInfo is a view rather than an
analysis. Its run does not look at the function at all:
1 | MachineBranchProbabilityAnalysis::Result |
Probabilities are computed once on IR and carried down, rather than re-estimated.