[llvm] [CycleInfo] Identify cycles with a single-pass DFS algorithm (PR #210491)
Fangrui Song via llvm-commits
llvm-commits at lists.llvm.org
Sat Jul 18 01:27:36 PDT 2026
MaskRay wrote:
> For reference, this is the pseudo-code from the paper (which unfortunately is not easily accessible):
>
> ```
> procedure identify_loops(CFG G=(N,E,h0)):
> foreach(Block b in N): // init
> initialize(b); // zeroize flags & properties
> trav_loops_DFS(h0,1);
>
> function trav_loops_DFS(Block b0, int DFSP_pos):
> //return: innermost loop header of b0
> Mark b0 as traversed;
> b0.DFSP_pos := DFSP_pos;//Mark b0’s position in DFSP
> foreach(Block b in Succ(b0)):
> if(b is not traversed):
> // case(A), new
> Block nh := trav_loops_DFS(b, DFSP_pos+1);
> tag_lhead(b0, nh);
> else:
> if(b.DFSP_pos > 0): // b in DFSP(b0)
> // case(B)
> Mark b as a loop header;
> tag_lhead(b0, b);
> else if(b.iloop_header == nil):
> // case(C), do nothing
> else:
> Block h := b.iloop_header;
> if(h.DFSP_pos > 0): // h in DFSP(b0)
> // case(D)
> tag_lhead(b0, h);
> else: // h not in DFSP(b0)
> // case(E), reentry
> Mark b and (b0,b) as re-entry;
> Mark the loop of h as irreducible;
> while(h.iloop_header!=nil):
> h := h.iloop_header;
> if(h.DFSP_pos > 0): // h in DFSP(b0)
> tag_lhead(b0, h);
> break;
> Mark the loop of h as irreducible;
> b0.DFSP_pos := 0; // clear b0’s DFSP position
> return b0.iloop_header;
>
> procedure tag_lhead(Block b, Block h):
> if(b == h or h == nil) return;
> Block cur1 := b, cur2 := h;
> while(cur1.iloop_header!=nil):
> Block ih := cur1.iloop_header;
> if(ih == cur2) return;
> if(ih.DFSP_pos < cur2.DFSP_pos):
> cur1.iloop_header := cur2;
> cur1 := cur2;
> cur2 := ih;
> else:
> cur1 := ih;
> cur1.iloop_header := cur2;
> ```
I use this variant https://gist.github.com/MaskRay/5872ef6af7e78d4329c3e85cc1957638#file-wei-cc , which merges cases (C)(D)(E) :)
> The algorithm has a worst-case complexity of O(N+d*E) (d=max depth), which, although quadratic in the worst case, is acceptable, as the loop nesting depth is typically very small and the two loops in question are very tight and the iteration is only deep for cases that are rare in practice. Also, some transforms have a runtime of O(d^3) already.
I think both Havlak-Tarjan algorithm and Wei's algorithm are O(d*E) on adversarial irreducible input, but Havlak-Tarjan is nearly linear while Wei's has a quadratic on "nested"
gen.py
```
#!/usr/bin/env python3
# Synthesize control-flow-graph-like directed graphs (node 0 = entry) in the
# "n m / u v..." edge format. Deterministic given a seed.
#
# gen.py <family> <n> <seed> -> writes graph to stdout
#
# Families:
# reducible random reducible CFG (DFS tree + forward + back edges to ancestors)
# lowk "realistic" low-unstructuredness CFG (mostly a chain of simple
# loops and branches; a few irreducible edges)
# nested deeply nested loops (stresses loop-nesting depth)
# siblings many small sibling loops in series (wide, shallow)
# irreducible many multi-entry (irreducible) loops
# dense small-ish but high average degree / high unstructuredness
import random
import sys
def emit(n, edges):
# Ensure a spanning structure so every node is reachable from 0.
out = [f"{n} {len(edges)}"]
out.extend(f"{u} {v}" for u, v in edges)
sys.stdout.write("\n".join(out) + "\n")
def reducible(n, rnd):
# Build a DFS tree (each node's parent has a smaller id) plus extra forward
# and cross edges, then add back edges to a random ancestor -> reducible.
edges = []
parent = [0] * n
for v in range(1, n):
parent[v] = rnd.randrange(v)
edges.append((parent[v], v))
# forward/cross edges to already-seen nodes
for _ in range(n // 2):
u = rnd.randrange(n)
v = rnd.randrange(n)
if u != v:
edges.append((u, v))
# back edges to an ancestor (climb the parent chain)
for _ in range(max(1, n // 20)):
v = rnd.randrange(1, n)
a = v
for _ in range(rnd.randrange(1, 6)):
a = parent[a]
edges.append((v, a))
return edges
def lowk(n, rnd):
# A long spine 0->1->...->(n-1) with occasional small self/loop back edges
# and forward skips. Mimics real functions: unstructuredness coefficient ~1.
edges = []
for i in range(n - 1):
edges.append((i, i + 1))
i = 1
while i < n - 2:
r = rnd.random()
if r < 0.30: # simple loop: i+k -> i
k = rnd.randrange(1, min(6, n - i))
edges.append((i + k, i))
elif r < 0.45: # forward branch skip
k = rnd.randrange(2, min(8, n - i))
edges.append((i, i + k))
i += rnd.randrange(2, 6)
# a handful of irreducible edges (jump into the middle of a loop body)
for _ in range(max(0, n // 500)):
u = rnd.randrange(n)
v = rnd.randrange(1, n)
edges.append((u, v))
return edges
def nested(n, rnd):
# Deeply nested loops: header chain h0<h1<...; each level loops back.
# Layout: 0..n-1 as a spine; at the end, back edges from tail to each header
# creating nesting depth ~ number of headers.
edges = []
for i in range(n - 1):
edges.append((i, i + 1))
depth = min(n - 2, max(1, n // 3))
headers = list(range(1, depth + 1))
tail = n - 1
# back edge from a body node to each header, innermost last so the latch of
# an inner loop sits below the outer header -> proper nesting
for idx, h in enumerate(headers):
latch = depth + 1 + idx if depth + 1 + idx < n else tail
edges.append((latch, h))
return edges
def siblings(n, rnd):
# Series of independent 2-3 node loops: 0->a->b->a, b->c->d->c, ...
edges = []
i = 0
prev = 0
while i + 3 < n:
h = i + 1
edges.append((prev, h))
edges.append((h, i + 2))
edges.append((i + 2, h)) # back edge -> loop {h, i+2}
edges.append((i + 2, i + 3))
prev = i + 3
i += 3
return edges
def irreducible(n, rnd):
# Many 2-entry irreducible diamonds: entry branches to a and b; a<->b form a
# 2-entry loop; then exit to the next diamond.
edges = []
prev = 0
i = 1
while i + 3 < n:
a, b, x = i, i + 1, i + 2
edges.append((prev, a))
edges.append((prev, b))
edges.append((a, b))
edges.append((b, a)) # irreducible {a,b}
edges.append((a, x))
edges.append((b, x))
prev = x
i += 3
return edges
def dense(n, rnd):
edges = []
for v in range(1, n):
edges.append((rnd.randrange(v), v))
for _ in range(n * 4):
u = rnd.randrange(n)
v = rnd.randrange(1, n)
edges.append((u, v))
return edges
FAMILIES = {
"reducible": reducible,
"lowk": lowk,
"nested": nested,
"siblings": siblings,
"irreducible": irreducible,
"dense": dense,
}
if __name__ == "__main__":
fam, n, seed = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
rnd = random.Random(seed)
edges = FAMILIES[fam](n, rnd)
# drop self-free? keep self-loops (family may add). clamp node ids.
edges = [(u, v) for (u, v) in edges if 0 <= u < n and 0 <= v < n]
emit(n, edges)
```
https://github.com/llvm/llvm-project/pull/210491
More information about the llvm-commits
mailing list