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.
Irreducible loops
The dominator tree lets
us identify natural loops:
a back edge T->H whose head H dominates its
tail T defines a loop with the single entry H.
This works only for reducible control flow graphs. Optimized
machine code and decompiler output routinely contain
irreducible loops, which have more than one entry and thus no
dominating header, so the dominator-based method cannot see them.
This post builds a loop-nesting forest for an arbitrary CFG with the single-pass depth-first search of 韦韬、毛剑、邹维、陈宇(Tao Wei, Jian Mao, Wei Zou & Yu Chen) A New Algorithm for Identifying Loops in Decompilation, SAS 2007 (The 14th International Static Analysis Symposium).
Optimizing LLVM's bump allocator
BumpPtrAllocator is LLVM's bump allocator (arena
allocator): each allocation bumps a pointer within a slab, and
everything is freed at once when the allocator dies. It backs Clang's
ASTContext, lld's make<T> object pools,
TableGen records, and many other arenas.
Here is the fast path before three recent changes:
1 | __attribute__((returns_nonnull)) void *Allocate(size_t Size, Align Alignment) { |
A deep dive into SmallVector::push_back
tl;dr This blog post describes a recent
SmallVector::push_back optimization for approximately
trivially copyable element types.
SmallVector is LLVM's most-used container, and
push_back its hot operation. For the trivially-copyable
specialization the fast path should be fast.
1 |
|
clang -S --target=x86_64 -O2 -DNDEBUG a.cc
generates:
1 | push rbp # callee-saved spills + a stack realignment, |
Recent LLVM hash table improvements
LLVM has several hash tables. They used quadratic probing with in-band sentinel keys (empty, tombstone); recent work has been replacing that with linear probing with tombstone key removed.
DenseMap(replacement forstd::unordered_map):DenseMapInfo::getEmptyKey()/getTombstoneKey().DenseSet: implemented usingDenseMapcompiler-rt/lib/sanitizer_common/sanitizer_dense_map.hports the implementation for sanitizers.
SmallPtrSet(replacement forstd::unordered_set<T *>): hard-coded-1(empty) and-2(tombstone).StringMap(replacement forstd::unordered_map<std::string, V>)StringSet: implemented usingStringMap
For the open-addressed DenseMap and
SmallPtrSet, pointers, references, and iterators are
invalidated by insert. StringMap is different: each entry
lives in a heap-allocated StringMapEntry<V> node, so
entry pointers survive grow. std::unordered_map, being
node-based, keeps surviving-element pointers valid across both insert
and erase and only invalidates the erased element's own iterator. LLVM
code rarely needs that stronger contract — callers do not hold
long-lived references into the container across mutation — and that gap
is what gives pass to relocating erase and bit-array occupancy.
Recently,
- Tombstones have been removed from DenseMap and SmallPtrSet.
erase()also invalidates pointers. - DenseMap has also retired its empty-key sentinel, leading to
significant performance improvements. DenseMap with integer keys
(
int/unsigned/size_t) had-1/-2reserved — a footgun, now fixed. - StringMap got Algorithm R deletion too. Its entries are separately
heap-allocated, so erase keeps entry pointers valid but invalidates
iterators; erase-while-iterating moved to
remove_if.
Fighting Hyrum's Law in LLVM
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody. — Hyrum's Law
In a compiler, the most common form of Hyrum's Law is dependence on
unspecified behavior — hash bucket order, the order of equal
elements after std::sort, padding offsets. The same framing
covers a few cases that are technically undefined behavior (use of an
invalidated iterator) or plain incidental properties (ABI struct layout,
ELF section offsets).
When the compiler itself harbors such a dependency, the symptom is
usually output that varies build-to-build: an unstable sort that lands
differently after the standard library changes, a hash map whose
iteration order shifts when the hash function does. Occasionally the
variation is run-to-run within a single build —
DenseMap<void *, X> keys with an ASLR-derived seed
reorder buckets each invocation. Either way, reproducible builds,
bisection, and bug reports all assume same input → same output, and a
stealth Hyrum dependency breaks that.
This post surveys some mechanisms that perturb the contract's blind spots so dependencies cannot quietly form.
Recent lld/ELF performance improvements
Updated in 2026-05.
Since the LLVM 22 branch was cut, I've landed patches that
parallelize more link phases and cut task-runtime overhead. This post
compares current main against lld 22.1, mold, and wild.
Headline: a Release+Asserts clang --gc-sections link is
1.34x as fast as lld 22.1; Chromium debug with --gdb-index
is 1.09x as fast. mold and wild are still ahead — the last section
explains why.
Bit-field layout
The C and C++ standards leave nearly every detail to the implementation. C23 §6.7.3.2:
An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified
C++ is also terse — [class.bit]p1:
Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit.
Call relocation types
Most architectures encode direct branch/call instructions with a PC-relative displacement. This post discusses a specific category of branch relocations: those used for direct function calls and tail calls. Some architectures use two ELF relocation types for a call instruction:
1 | # i386, x86-64 |
lld 22 ELF changes
For those unfamiliar, lld is the LLVM linker, supporting PE/COFF, ELF, Mach-O, and WebAssembly ports. These object file formats differ significantly, and each port must follow the conventions of the platform's system linker. As a result, the ports share limited code (diagnostics, memory allocation, etc) and have largely separate reviewer groups.
With LLVM 22.1 releasing soon, I've added some notes to the https://github.com/llvm/llvm-project/blob/release/22.x/lld/docs/ReleaseNotes.rst as an lld/ELF maintainer. As usual, I've reviewed almost all the patches not authored by me.
For the first time, I used an LLM agent (Claude Code) to help look
through commits
(git log release/21.x..release/22.x -- lld/ELF) and draft
the release notes. Despite my request to only read lld/ELF changes,
Claude Code also crafted notes for other ports, which I retained since
their release notes had been quite sparse for several releases. Changes
back ported to the 21.x release are removed
(git log --oneline llvmorg-22-init..llvmorg-21.1.8 -- lld).
I'll delve into some of the key changes.