Graph algorithms · structuring a method

Dominance and reachability

Where a branch's two arms come back together, and exactly which statements are inside a loop — worked both the slow way and the fast way, on the same small graph, so the difference is visible rather than asserted.

Where this shows up in reverse engineering

Dominance and reachability are the first things a tool needs once it has a control-flow graph and wants to say something about it in English, or in another language's syntax, rather than just a list of edges.

The example

The same diamond as visiting a graph: a branch, two arms of different lengths, a statement both arms reach, then the end. Diagrams below mark the node a step is currently working out in amber.

flowchart TD
    A --> B1
    A --> C
    B1 --> B2
    B2 --> B3
    B3 --> D
    C --> D
    D --> E

Two questions about it, both answered on this page: once A branches, what is the statement both arms are guaranteed to reach? And separately — on a different graph, below — exactly which statements belong to a loop?

Dominators: looking backward

Pick a statement and ask which earlier statements execution is guaranteed to have already passed through to get there, no matter which way it arrived — the iterative dataflow formulation below is Allen and Cocke's.[1] To reach B3: always through B2, always through B1, always through A — no other way in. Each of those dominates B3. C does not — the other arm reaches B3 without it.

statementdominated by
A{A}
B1{A, B1}
B2{A, B1, B2}
B3{A, B1, B2, B3}
C{A, C}
D{A, D}
E{A, D, E}

Of everything dominating B3, the closest — B2 — is its immediate dominator. Chaining immediate dominators (A → B1 → B2 → B3) is the one unavoidable route in.

Post-dominators: looking forward

Same question, mirrored: which statements is execution guaranteed to reach on the way out, no matter which way it leaves? That is a post-dominator, and it is the answer to the branch question — the statement both of A's arms are forced through is A's post-dominator.

statementpost-dominated by
A{A, D, E}
B1{B1, B2, B3, D, E}
B2{B2, B3, D, E}
B3{B3, D, E}
C{C, D, E}
D{D, E}

The closest member of each set, excluding the statement itself, is its immediate post-dominator — the number this page spends the rest of its time computing, two different ways.

statementimmediate post-dominator
AD
B1B2
B2B3
B3D
CD
DE

Notice B1's answer is B2, not D — D is where the branch's arms visibly rejoin, but B2 is closer, and "closest" is the question being asked. Keep that row in mind for what goes wrong.

The slow way: a full set per statement

Give every statement a candidate set, start each one as large as possible, and shrink it to {self} ∪ (intersection of every successor's set), over and over, until nothing shrinks any further. Visited in the same order both times for a fair comparison — D, B3, C, B2, B1, A, each after everything it points to.

Settling every candidate set, round by round (2 rounds, 12 visits)
  1. Before round 1: every statement except E starts holding every statement that can reach E at all — nothing has been ruled out yet. Nothing to mark as current.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    statementcandidate set
    A{A,B1,B2,B3,C,D,E}
    B1{A,B1,B2,B3,C,D,E}
    B2{A,B1,B2,B3,C,D,E}
    B3{A,B1,B2,B3,C,D,E}
    C{A,B1,B2,B3,C,D,E}
    D{A,B1,B2,B3,C,D,E}
  2. Round 1 — D: successor is E = {E}. New set: {D} ∪ {E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class D changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D{D, E}
  3. Round 1 — B3: successor is D, just updated to {D,E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B3 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    B3{B3, D, E}
  4. Round 1 — C: successor is D = {D,E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class C changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    C{C, D, E}
  5. Round 1 — B2: successor is B3, just updated to {B3,D,E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B2 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    B2{B2, B3, D, E}
  6. Round 1 — B1: successor is B2, just updated to {B2,B3,D,E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B1 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    B1{B1, B2, B3, D, E}
  7. Round 1 — A: two successors. Intersect B1's {B1,B2,B3,D,E} with C's {C,D,E} → {D,E}.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    A{A, D, E}
  8. Round 2 — D: recompute from E. Same as before: no change.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    D{D, E}
  9. Round 2 — B3: recompute from D. No change.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    B3{B3, D, E}
  10. Round 2 — C: recompute from D. No change.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    C{C, D, E}
  11. Round 2 — B2, B1, A: each recomputes to exactly what it already held. Nothing in the whole collection of six sets changed anywhere — that comparison, over every set, is how the loop knows to stop.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    B2{B2, B3, D, E}
    B1{B1, B2, B3, D, E}
    A{A, D, E}

Immediate post-dominator, read off at the end, is the member of each set — other than the statement itself — whose own set is largest: for A, the choice is between D (set size 2) and E (size 1), so D wins, matching the table above. Six statements, two rounds, twelve visits, and a whole-collection comparison just to confirm round 2 changed nothing. That confirming round is pure overhead — on this graph, cheap; it is not what makes this approach slow in general.

The fast way: one pointer per statement

Carry one pointer per statement — its best guess at its immediate post-dominator — instead of a set. At each statement, the new guess is the meeting point of its already-visited successors' pointers: walk each successor's pointer-chain forward until two chains land on the same statement.

Settling every pointer, one statement at a time (6 visits, no second round needed)
  1. Nothing settled yet.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
    
    no pointers yet
  2. D: only successor is E, the end. Pointer: E.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class D changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → E
  3. B3: only successor is D, already settled. Pointer: D.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B3 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → EB3 → D
  4. C: only successor is D. Pointer: D.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class C changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → EB3 → DC → D
  5. B2: only successor is B3. Pointer: B3.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B2 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → EB3 → DC → DB2 → B3
  6. B1: only successor is B2. Pointer: B2.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class B1 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → EB3 → DC → DB2 → B3B1 → B2
  7. A: two successors, B1 and C, both settled. Walk B1's chain: B1 → B2 → B3 → D. Walk C's chain: C → D. First statement both reach: D. Pointer: D.

    flowchart TD
        A --> B1
        A --> C
        B1 --> B2
        B2 --> B3
        B3 --> D
        C --> D
        D --> E
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
    D → EB3 → DC → DB2 → B3B1 → B2A → D

Same answers as the slow walk's final row, reached in one pass, no confirming round, and no statement ever held more than one other statement's name at a time.

The two compared

slow way (full set)fast way (one pointer)
held per statementup to 7 namesexactly 1 name
visits to settle12 (2 rounds × 6)6 (1 pass)
work per visitintersect sets of up to 7walk two short chains
answerD, B2, B3, D, D, ED, B2, B3, D, D, E

On six statements the difference is a handful of extra visits — not worth caring about. The per-visit cost is what stops scaling gently: a set of size up to n, compared across all n statements, every round, is n² work per round. A chain-walk is bounded by how far apart two pointers' chains are, which in practice is a handful of hops, not the size of the whole method. Where this is used has the numbers from a method large enough for that difference to be the whole story.

Loops: reachability with a stopping point

A different question needs no sets or pointers at all: which statements belong to a loop — the backward walk below, bounded at the header, is Aho, Sethi and Ullman's.[2] Take the loop from visiting a graph. Below, amber marks the statement a step is currently taking off the worklist; a bordered node marks a statement already confirmed as part of the loop body.

flowchart TD
    T --> X1
    T --> L1
    X1 --> X2
    L1 --> L2
    L2 -.->|back edge| T

L2 → T is the back edge — the arrow DFS found going to a statement still on its own path. T is the loop's header. The loop's body is everything reachable walking backward from L2, with one rule: stop expanding the moment the walk reaches T, rather than following whatever leads into T from outside the loop.

Walking backward from L2, stopping at T (3 steps)
  1. Start the worklist with the back edge's source and the header both already found — the header has to be in the body, it just must not be expanded past.

    flowchart TD
        T --> X1
        T --> L1
        X1 --> X2
        L1 --> L2
        L2 -.->|back edge| T
        class T,L2 member
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
    

    To expand

    L2T

    Body so far

    L2T
  2. Take T off the list. It is the stopping point: do not expand its predecessors.

    flowchart TD
        T --> X1
        T --> L1
        X1 --> X2
        L1 --> L2
        L2 -.->|back edge| T
        class L2 member
        class T changed
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    To expand

    L2

    Body so far

    L2T
  3. Take L2 off the list. Its predecessor is L1, not yet found: add it, both to expand and to the body.

    flowchart TD
        T --> X1
        T --> L1
        X1 --> X2
        L1 --> L2
        L2 -.->|back edge| T
        class T member
        class L1,L2 changed
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    To expand

    L1

    Body so far

    L2TL1
  4. Take L1 off the list. Its predecessor is T — already in the body, and the stopping point besides, so nothing is added. The list is empty: done.

    flowchart TD
        T --> X1
        T --> L1
        X1 --> X2
        L1 --> L2
        L2 -.->|back edge| T
        class T,L2 member
        class L1 changed
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    To expand

    empty

    Body so far

    L2TL1

Loop body: {T, L1, L2}. X1 and X2 are never reached by this walk at all — the other arm out of T, outside the loop — and nothing about them had to be inspected to know that, which is why they never once appear bordered in the diagrams above. The same bounded backward walk, with no stopping point, also answers "what can reach the end" (used above to decide which statements have a post-dominator at all) and, run forward instead of backward, "what is reachable from the start."

What goes wrong

Where this is used

A tool that turns one method's control-flow graph into a nested tree of regions — decisions with two arms and a known end, loops with a known body, everything else a plain sequence — for narrating what the method does, in order. Every decision needs its immediate post-dominator; every loop needs its backward-reachable body.

Most methods in a normal corpus are small enough that either approach — whole candidate sets or a chain of pointers — finishes before anyone would notice the difference. One real method's control-flow graph, though, had on the order of seven thousand statements and eighteen thousand edges — an unusually large single method, the kind a long hand-written dispatcher or a heavily generated chain of conditions produces, but not actually rare in enterprise code once a corpus is big enough. Built the whole-candidate-set way, settling that method's post-dominators alone was doing tens of millions of set comparisons per round, for however many rounds a graph that shape needed — long enough that it looked indistinguishable from a tool that had hung, rather than one that was simply still working. Rebuilt the pointer-chasing way described above, the same method's region tree finished in about a second.

this page's examplea real large method
statements7≈ 7,300
edges7≈ 18,000
slow way12 visits, instanttens of millions of set comparisons per round — looked indistinguishable from a hung tool
fast way6 visits, instantabout one second

Nothing about the fast way is specific to a large method — it is the same six-step walk run on this page, just run on a graph with a thousand times more statements in it.

References

  1. F. E. Allen and J. Cocke. "Graph-Theoretic Constructs for Program Control Flow Analysis." IBM Research Report RC 3923, 1972. The iterative dataflow algorithm for computing dominators.
  2. A. V. Aho, R. Sethi, and J. D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley, 1986, p. 604 (Algorithm 10.1), reprinted 2nd ed. 2006, p. 665 (Algorithm 9.46). Constructing the natural loop of a back edge.