Loops
Given a CFG, formally define a *loop* as a set of CFG nodes S with
header node h, such that
(1) for all n in S, there is a path from n to h.
(2) for all n in S, there is a path from h to n.
(3) if n is in S and m not in S, there is no path from m to n that doesn't include h.
A loop can have multiple exits, but only one entry (the header).
Dominators
Assume the CFG has a distinguished start node S, and has no disconnected subgraphs (nodes unreachable from S).
Then we define that node d *dominates* node n if all paths from S to n include d.
In particular, for all n, n dominates n.
Fact: d dominates n iff d = n or d dominates all predecessors of n.
So, the set D[n] of nodes that dominate n can be defined as:
D[S] = { S }
D[n] = { n } U (intersection over all p in pred[n] of D[p])
(where pred[n] = set of predecessors on n in CFG)
Define the *immediate dominator* of n, idom(n) as follows:
(1) idom(n) dominates n
(2) idom(n) is not n
(3) idom(n) does not dominate any other dominator of n (except n itself)
Fact: every node (except S) has a unique immediate dominator.
Hence the immediate dominator relation defined a tree, called the *dominator tree*,
whose nodes are the nodes of the CFG, where the parent of a node is its immediate dominator.
Have D[n] = {n} U (descendents of n in dominator tree)
Fact: The dominator tree of a CFG can be computed in almost-linear time.
Define a *back edge* of the CFG to be an edge whose source is dominated by its target
(i.e., an edge whose source is a descendent of its target in the dominator tree).
Every back edge n -> h defines a *natural loop* with header h, consisting of all nodes x
dominated by h such that there is a path from x to n that doesn't include h.
Note that a node h can be the header of more than one natural loop.
For convenience, we alter the control flow graph to insert a *preheader* node above
each loop header, with a single out-edge to the header node.
Pointers that originally targeted the header node from outside the loop now target
the pre-header node instead; pointers from inside the loop are unchanged.
A CFG is *reducible* (or "well-structured") if, after we remove all back edges, the remaining
graph is acyclic. This is true iff all cycles in the graph are natural loops.
Loop Invariant Compuatations
Consider a quad
d: t <- a1 binop a2
that lies inside a loop.
We say d is a *loop invariant* if, for each of i = 1 and 2 we have:
(1) ai is a constant; or
(2) all the definitions of ai that reach d are outside the loop; or
(3) the sole definition of ai that reaches d is a loop invariant.
(Note this is a recursive definition.)
When is it safe to hoist such a loop invariant into the loop header (assuming that binop has no side-effects) ?
(1) d dominates all loop exits at which t is live.
(2) there is only one definition of t in the loop.
(3) t is not live-out of the loop pre-header.
Static Single Assignment (SSA) Form.
- Every variable has one (static) definition (though defn. may be executed many times).
- For straightline code, this is just like value numbering:
Original Code:
v <- 4
w <- v + 5
v <- 6
w <- v + 7
Code in SSA:
v1 <- 4
w1 <- v1 + 5
v2 <- 6
w2 <- v2 + 6
- For general flow, must introduce phi-nodes. These are fictional operations, (usually) not
intended to have exeuction significance. To interpret these nodes, must view code as CFG,
with the in-edges to any node have well-defined order.
Example 1:
Original CFG:
|--------|
| P? |
|--------|
/ \
/ \
V V
|------| |------|
| v<-4 | | v<-5 |
|------| |------|
\ /
\ /
V V
|------------|
| w <- v + v |
|------------|
CFG in SSA form:
|--------|
| P? |
|--------|
/ \
/ \
V V
|-------| |-------|
| v1<-4 | | v2<-5 |
|-------| |-------|
\ /
\ /
V V
|------------------|
| v3 <- phi(v1,v2) |
| w1 <- v3 + v3 |
|------------------|
Example 2:
Original CFG:
|-----------|
| i <- 0 |
| j <- 0 |
-------------
| ----------------
| | |
V V |
|-----------| |
| i > N ? | |
|-----------| |
/\ |
/ \ |
EXIT \ |
\ |
|------------| |
| j <- j + i | |
| i <- j + 1 | |
|------------| |
| |
|-----------
CFG in SSA form:
|-----------|
| i1 <- 0 |
| j1 <- 0 |
-------------
| ----------------
1 | | 2 |
V V |
|-----------------| |
| i2 = phi(i1,i3) | |
| j2 = phi(j1,j3) | |
| i2 > N ? | |
|-----------------| |
/\ |
/ \ |
EXIT \ |
\ |
|--------------| |
| j3 <- j2 + i | |
| i3 <- j2 + 1 | |
|--------------| |
| |
|-----------
Where should we put phi assignments, and for which variables? Simple answer: in every join node,
for every variable in the program. Too expensive!
Suffices to put a phi assignment for x in join nodes that aren't dominated by a single definition of x.
More next time...