Graph algorithms · structuring a method

Dominance and reachability

Read first: Visiting a graph

An edge list says where each statement can go next, not where a branch ends. A branch's arms rejoin at a statement on every path to the exit, and only the first such statement will do: resuming at a later one copies code into both arms. Proving it with a full candidate set per statement makes every visit cost work in proportion to the method's size.

In the diamond below, A's arms rejoin at D. The set method and the pointer method both find it in 12 visits, 6 to settle and 6 to confirm; the pointer method's visits are cheaper, because each statement holds one name rather than up to seven. In the loop graph further down, a backward walk from L2 that stops at T finds the body {T, L1, L2}.

A statement post-dominates another if every path from that one to the exit passes through it.

The example

The same diamond as visiting a graph: A branches, one arm is B1, B2, B3, the other is C, both reach D, and E is the end.

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

The question: once A branches, what is the first statement both arms are guaranteed to reach? That is A's immediate post-dominator. The table gives it for every statement in the graph above, and A's row, in amber, is the answer.

statementimmediate post-dominator
AD
B1B2
B2B3
B3D
CD
DE

Every other row has one way out: B1's only successor is B2, so that is the next statement it must reach, and the same holds for B2, B3, C and D. Only A's row needs both arms compared. The next two sections compute this table two ways and arrive at the same six rows. A second graph, with a loop, comes in Loops.

The slow way: a full set per statement

Give every statement except E a candidate set, start each one as large as possible, and shrink it by the rule in the box, over and over, until nothing shrinks any further. This is Allen and Cocke's iterative formulation,[1] mirrored to look at successors. Statements are visited in the order D, B3, C, B2, B1, A, each after everything it points to; the fast way uses the same order, for a fair comparison. Diagrams in the walk mark the node a step is working out in amber.

set(E) = {E}
set(s) = {s} ∪ (intersection of set(t) over every successor t of s)
stop when a whole round changes no set

The box's second line does real work in step 6, A, the one visit with two successors: {B1,B2,B3,D,E} ∩ {C,D,E} = {D,E}. The stop line is step 10, where round 2 has changed nothing.

Settling every candidate set, round by round (10 steps · 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 in step 6's set is between D (set size 2) and E (size 1), so D wins, matching the example's table. Six statements, two rounds, twelve visits — though every set was already final after round 1 (steps 1–6), because this graph has no cycles and each statement was visited after everything it points to. Round 2 (steps 7–10), and its whole-collection comparison, only confirms that. The pointer method below needs the same confirming pass on any graph with a loop, so this round is not where the two differ.

The fast way: one pointer per statement

Carry one pointer per statement — its best guess at its immediate post-dominator — instead of a set. This is Cooper, Harvey and Kennedy's algorithm.[2] At each statement, the new guess is the meeting point of its already-visited successors' pointers, found with two "fingers" by the rule in the box. Like the set version, it repeats passes until one changes nothing.

pointer(s) = the meeting point of s's visited successors
meet: one finger on each successor; while they differ,
advance the finger with the lower postorder number to its statement's pointer

For post-dominators the postorder is of the reversed graph, walked from the exit, and a lower number means farther from the exit. Steps 1–5 below have one successor each, so the meeting point is that successor. Step 6, A, is where the fingers move: the B1 finger climbs B2, B3, D, then the C finger steps to D.

Settling every pointer, one statement at a time (6 steps · round 1: 6 visits; a confirming round would add 6 more)
  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. Numbering the reversed graph in postorder from E (visiting B3 before C) gives A 0, B1 1, B2 2, B3 3, C 4, D 5, E 6. Fingers on B1 (1) and C (4): the B1 finger is lower, so it advances — B2, B3, D (5). Now C (4) is lower, so it advances to D. Both fingers on 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, and no statement ever held more than one other statement's name at a time: the chips in step 6 are the whole state. Like the set version, it was settled after one pass only because this graph has no cycles; it still needs a round 2 that changes nothing before it can stop — the same twelve visits. The saving is in what each visit costs.

The two compared

slow way (full set)fast way (one pointer)
held per statementup to 7 namesexactly 1 name
visits12 (6 to settle + 6 to confirm)12 (6 to settle + 6 to confirm)
work per visitintersect sets of up to 7advance two fingers up pointer chains
answerD, B2, B3, D, D, ED, B2, B3, D, D, E

The visits row is identical; the rows above and below it are where the two differ. On six statements the difference is 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.

Post-dominators: looking forward

The slow walk's final sets, gathered into one table, are the answer to a question worth naming: which statements is execution guaranteed to reach on the way out, no matter which way it leaves? Each member of a row is a post-dominator of that row's statement.

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 example's table. Notice A's row has two post-dominators besides itself, D and E — both arms are forced through each of them. D is the immediate one because it comes first: every path from A to E passes through D, so D is closer, and "closest" is the question being asked. Keep that row in mind for what goes wrong.

Dominators: looking backward

The mirror question: 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. To reach B3 in the diamond: always through B2, always through B1, always through A — no other way in. Each of those dominates B3, as B3's row below says. 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. The loop section below needs this direction: a back edge is recognised by its target dominating its source.

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.[3] 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, in both of the senses the word gets used: it is the arrow DFS found going to a statement still on its own path, and its target, T, dominates its source, L2 — every path to L2 goes through T. On a reducible graph the two senses always coincide; where they do not, the graph has a loop with more than one entry (see reducibility). A natural loop needs the second sense, and T is this 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.

body = to-expand = {L2, T} (back edge's source, and the header)
take a statement off to-expand; if it is the header, stop there
otherwise add each predecessor not yet in body to both

The box's "stop there" line is step 1, where T comes off the list and its predecessors are left alone. The last line is step 2, which adds L1, and step 3, where L1's predecessor T is already in the body.

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}, the last step's chips. 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" (which decides which statements have a post-dominator at all — see what goes wrong) and, run forward instead of backward, "what is reachable from the start."

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. Each use below rests on one of this page's answers, gathered here:

A's arms rejoin at Drepeats: T, L1, L2not repeated: X1, X2

What goes wrong

Where this is used

cobol-rekt computes forward dominators the whole-candidate-set way, following Allen and Cocke,[1] and picks each statement's immediate dominator by eliminating the candidates that dominate another candidate. Those dominators are what its DJ graph, loop-body detection and reducibility test are built on. It computes no post-dominators. The elimination, applied to B3's row of the dominator table above:

candidatedominates another candidate?kept
Ayes: B1, B2no
B1yes: B2no
B2noyes: immediate dominator

The post-dominator half of this page comes from a separate Java 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. On this page's graphs those are the example table's D and the loop walk's {T, L1, L2}.

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

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. The table's right-hand column is the exception: one real method's control-flow graph 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. 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. K. D. Cooper, T. J. Harvey, and K. Kennedy. "A Simple, Fast Dominance Algorithm." Software Practice & Experience, 4, 2001. The pointer-per-statement method and its two-finger intersect.
  3. A. V. Aho, R. Sethi, and J. D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley, 1986, p. 604 (Algorithm 10.1); new 2nd edition by A. V. Aho, M. S. Lam, R. Sethi, and J. D. Ullman, 2006, p. 665 (Algorithm 9.46). Constructing the natural loop of a back edge.