pts&alias·dbg
points-to edge heap / address cell may-alias (∩) unsound / miscompile sound / safe

interprocedural analysis · the gate before every memory optimization

What can this pointer actually point to?

Pointer & alias analysis decides whether two memory references can touch the same cell. Get it right and the optimizer is free to reorder, hoist, and forward. Get it wrong by one bit and you ship a miscompile.

This page is a debugger, not a blog post. Step the visualizations: watch points-to sets propagate, nodes coalesce, kills strike old arrows out, and the danger color flash the moment soundness breaks.

§ 01 — first principles

What aliasing is, and why it gates everything

Two expressions alias if they may denote the same storage location at runtime. *p and x alias if p may hold &x. Aliasing is a relation over access paths, derived from a points-to relation over pointers — "what objects can p point to?"

Watch a points-to graph form. Each statement adds a directed pointer edge; after p = q, p copies q's target and gains an arrow to b.

Points-to graph stepperlive memory
1p = &a; // {a} ⊆ pts(p)
2q = &b; // {b} ⊆ pts(q)
3p = q; // pts(q) ⊆ pts(p) [copy]
step 0 / 3
Press Step. pts(p) and pts(q) build up; after line 3, pts(p) = {a, b}.

Two flavors — the difference is the whole game

may-alias the pair might refer to the same location on some execution. An over-approximation; a "yes" can be a false positive. Used to forbid unsafe transforms — if two refs may alias, you cannot reorder them.

must-alias the pair definitely refers to the same location on every reaching execution. An under-approximation; a "yes" is a guarantee. Used to enable transforms — forward a stored value only if the pointers must-alias.

Mnemonic. may-alias is for safety (don't break things); must-alias is for opportunity (rewrite aggressively). Two pointers may-alias iff their points-to sets intersect. Empty intersection ⇒ provably no-alias ⇒ the optimizer is free.
May vs Must aliasset intersection

Why aliasing is the enabling/blocking analysis

Every transform that moves, removes, or reorders a memory op must first prove the locations are independent. Without alias info, the compiler must assume any store may clobber any load — which kills:

OptimizationWhat it needs alias info to prove
Dead store eliminationa store to *p is dead only if no later load may-alias it
Load/store forwardingreplace y=*q with the stored value only if p must-alias q
GVN / CSE over memorytwo loads of *p match only if no intervening store may-alias p
Register promotionpromote *p to SSA only if p doesn't may-alias an escaping access
LICM of a loadhoist *p from a loop only if no store in the loop may-alias p
Vectorizationdependence between a[i] and a[i-k] proven absent or distant

Wrong alias info breaks a transform — the miscompile

This is the most important interaction on the page. The optimizer wants to forward S1's stored 1 into the load at L2. That is legal only if no intervening store may-alias p. Toggle the analysis assumption and the runtime pointer relationship, then play it.

The miscompile demosoundness
S1store 1, *p // write through p
S2store 7, *q // write through q
L2z = load *p // read back through p
step 0 / 3
§ 02 — precision axes

The dimensions of precision

Analyses are classified by which axes of program structure they distinguish. Each axis costs more but buys precision: flow (order), context (callers), field (struct members), heap (object naming). They multiply — which is the scalability wall.

Flow-sensitivity — does statement order matter?

Flow-insensitive computes one global points-to map valid everywhere; an assignment only generates facts, never kills them. Flow-sensitive computes a solution per program point, tracking gen and kill — a re-assignment kills the old target (a strong update). Toggle the mode and watch line 2 strike the old p→a arrow.

Flow-sensitivity & strong update (kill)gen / kill
1p = &a;
2p = &b; // flow-sensitive: KILLS p→a
3x = *p; // what does *p alias?
step 0 / 3

Context-sensitivity — are different calls distinguished?

Context-insensitive analyzes a procedure once; all call sites merge. Context-sensitive specializes per calling context (clones per call site), so facts from caller A don't leak into caller B. With the identity function below, merging makes a spuriously may-alias y and b may-alias x.

Context-sensitivitycall-site cloning
1id(q) { return q; } // identity
2a = id(&x); // call site 1
3b = id(&y); // call site 2

Field & heap, briefly

Field-sensitivity — field-insensitive treats o.f and o.g as one location (so p=o.f ⇒ pts(p)={x,y}); field-sensitive keeps them distinct (pts(p)={x}).

Heap modeling — the heap is unbounded, so objects are named by allocation site: every object from one new shares one abstract name. That merges all objects from a factory into one node; heap cloning (alloc-site + context) separates them.

§ 03 — the canonical tradeoff

Andersen vs Steensgaard

Both are flow-insensitive, whole-program analyses that reduce the program to four constraint forms, then solve. They differ only in the solving discipline: Andersen uses directional inclusion (subset) constraints; Steensgaard uses symmetric unification (equality), merging nodes with union-find.

The four constraint forms

StmtNameConstraint
a = &baddress-of{b} ⊆ pts(a)
a = bcopypts(b) ⊆ pts(a)
a = *bload∀o∈pts(b): pts(o) ⊆ pts(a)
*a = bstore∀o∈pts(a): pts(b) ⊆ pts(o)

The load/store forms are dynamic: new points-to facts create new copy edges, so the solver iterates to a fixpoint. Step each constraint form below to see its dereference hops.

The four constraint formsdereference hops

Side-by-side: inclusion vs unification on five statements

Step both engines in lockstep. Andersen (left) keeps directionality — q never receives anything, so pts(q) stays {b}. Steensgaard (right) unifies on each copy: step 3 merges {p,q}, step 5 merges {p,q,r} into one node that points to {a,b,c} — so q spuriously gains a and c.

Andersen vs Steensgaard — lockstep stepperthe centerpiece
1p = &a; // {a} ⊆ pts(p)
2q = &b; // {b} ⊆ pts(q)
3p = q; // pts(q) ⊆ pts(p) [copy, ONE-WAY]
4r = &c; // {c} ⊆ pts(r)
5r = p; // pts(p) ⊆ pts(r) [copy]
Andersen · inclusion · O(n³)
Steensgaard · unification · O(n·α(n))
step 0 / 5
Takeaway. Andersen distinguishes pts(q)={b} from pts(p)={a,b}; Steensgaard collapses both to {a,b,c}. Same soundness (both over-approximate), wildly different precision and cost. Steensgaard when you need whole-program points-to in near-linear time; Andersen when precision matters and n is moderate.
§ 04 — dragon book framing

Flow-insensitive Datalog + call-string contexts

The Dragon Book (12.4) frames pointer analysis as a flow-insensitive, inclusion-based Datalog program with two IDB relations: pts(V,H) (variable V may point to heap object H) and hpts(H,F,G) (field F of object H points to G — field-sensitive). The four rules, one per statement form, are the Datalog encoding of the inclusion constraints, solved by iterative evaluation.

Interprocedural, context-insensitive (12.5): model parameter passing and returns as copy statements, and discover the call graph on the fly — because you need points-to to compute call targets, but you need call targets to compute points-to. This mutual dependence is iterated to a joint fixpoint.

Context-sensitive (12.6): a context is a call string — the sequence of call-site locations on the stack. Recursion makes call strings infinite, so recursive SCCs are collapsed. Each method is cloned per context; with ~10¹⁴ contexts in real programs, results are stored as BDDs.

§ 05 — why it's hard

The interview thread

This is the perennial follow-up. Four pillars:

PillarThe hard part
UndecidabilityPrecise may-alias with dynamic allocation and ≥2 dereference levels is undecidable (Landi 1992; Ramalingam 1994). Any sound analysis must over-approximate — there is no perfect answer, only a soundness/precision/cost frontier.
Call-graph ↔ points-toYou need points-to to resolve indirect/virtual calls, but the call graph to propagate points-to. Neither comes first; solve jointly to a fixpoint.
Heap / arrays / recursionArrays summarized to one element ⇒ a[i]/a[j] always may-alias unless a separate dependence test proves otherwise. Recursive structures defeat alloc-site naming.
Scalability wallThe product of all four axes is exponential. In practice, production analyses drop flow-sensitivity first (Andersen/Steensgaard are flow-insensitive).

The one-liner: aliasing is hard because precise answers are undecidable, the analysis is mutually recursive with the call graph, the heap/arrays/recursion force lossy abstraction, and the precision axes multiply — so every real analysis is a deliberate point on a soundness/precision/cost surface.

§ 06 — in practice

LLVM AliasAnalysis, TBAA, strict aliasing

LLVM AAResults. The core query alias(LocA, LocB) returns one of NoAlias MayAlias PartialAlias MustAlias. Implementations are chained in an AA pipeline: basic-aa, CFL-AA, globals-aa, scev-aa, and TBAA. Consumers — GVN, LICM, DSE, the loop vectorizer — all gate on these results.

TBAA (type-based). Uses the source language's type rules: in C/C++ an access of type T1 can't alias one of incompatible T2 (modulo the char* escape hatch). Encoded as !tbaa metadata — it proves float* and int* NoAlias without a points-to analysis.

Strict-aliasing UB. The standard makes it UB to access an object through an incompatible-type lvalue, so the compiler may assume type-incompatible pointers never alias — sound only because violating it is UB. Type-punning through a float* may be legally miscompiled. Mitigations: -fno-strict-aliasing, memcpy, char*/std::byte. The restrict qualifier is a programmer NoAlias assertion that enables vectorization on CPU and GPU.

Built as a memory/dataflow debugger. Colors encode meaning — see the legend up top. All semantics traced from the lesson.