for (i = 0; i < n; i++) {
A[i] = m + n; //m+n will not be hoisted out by the PRE algorithm
}
to
if (n > 0) {
i = 0;
t = m + n; //m+n will be hoisted out by the PRE algorithm
do {
A[i] = t;
i++;
} while (i < n);
}
This can be thought-of as a special case of loop peeling (peeling of the first few iterations in the loop header). Here, we peel-off the check of the first iteration. It happens to be a very common optimization in modern compilers as it can be applied to any loop.
Optimizations that make a loop faster are usually very consequential and so much effort has gone into this.
+------+
| BB1 |
+------+
/ \
v v
+------+ +------+
┌───>| BB2 | | BB3 |
│ +------+ +------+
│ \ /
│ \ /
│ v v
│ +------+
│ | BB4 | <-----
│ +------+ |
│ | |
│ v |
│ +------+ |
└────────| BB5 | |
+------+ |
| |
└──────────| BB4
While BB4-BB5 are a natural loop, BB2-BB4-BB5 are not a natural loop (BB3 can jump into the middle of this loop).
An example with nested loops:
+------+
| BB1 | <----------------
+------+ |
/ \ |
v v |
+------+ +------+ |
┌───>| BB2 | | BB3 | |
│ +------+ +------+ |
│ \ / |
│ \ / |
│ v v |
│ +------+ |
│ | BB4 | <----- |
│ +------+ | |
│ | | |
│ v | |
│ +------+ | |
└────────| BB5 | | |
+------+ | |
| | | |
| └──────────| BB4 |
| |
| |
v |
+------+ |
| BB6 | ------------------| BB1
+------+
Most loops in common programs are natural loops.
d dominates node n in a graph (d dom n) if:
n goes through d.d idom n: d dom n, d != n, \not_exists m, s.t., d dom m and m dom n.d dom n)d+ {nodes that can reach n without going through d}.d dominates n in a graph (d dom n) if every path from the start node to
n goes through d.
One way to compute dominators is through a dataflow analysis where the set of values V is the sets of basic blocks. (Show the expected result of such a dataflow analysis on the examples above).
void main()
{
foreach (node n : graph) {
visited[n] = false;
}
c = number of nodes in G;
search(entrynode);
}
void search(node n)
{
visited[n] = true;
for (s : successors(n)) {
if (!visited[s]) {
search(s);
}
}
dfn[n] = c;
c = c - 1;
}
//Output: dfn[] contains the depth-first (aka reverse postorder) numbering of each node
Show the potential results of running the DFN algorithm on the examples above.
An edge m->n is a retreating edge iff dfn[m] > dfn[n] (because this means that during the DFS traversal as in the algorithm above, m was reached through n starting from entry, or time taken by search(m) was a sub-interval of the time taken by search(n)). Notice that there are multiple depth-first orders possible (depending on the order in which the successors are chosen), and each depth-first order may result in a different set of retreating edges.
Example of a non-reducible graph:
+-----+
| BB1 |
+-----+
| \
| \
v v
+-----+ +-----+
| BB2 |->| BB3 |
+-----+ +-----+
^ |
| |
+-------+
Example of non-reducible graph in general: often if we reverse the edges of a regular CFG, we may often end up with a non-reducible flow graph. Intuitively: while typical programs have loops with single entries, those loops sometimes have several exits.
Structured control flow including for, while, do-while,
break, and continue, always produce reducible graphs. However, if we
construct the CFG of the optimized code, e.g., optimized assembly code, it is often not reducible,
i.e., the compiler may have made it irreducible. For example, it may share common code across loops
to reduce code size.
d from the graph, find all predecessors of n.
BB1 --> BB2 --> BB1 |---> BB3 --> BB1Here, the two different back edges (
BB2->BB1 and BB3->BB1) can be either due to a nested loop (i.e., one may be nested as another) or simply a continue statement in the loop body.
BB1->BB2
BB2->BB2
BB2->BB3
BB1: i=0
BB2: i = i+1
if i < n goto BB1
This is a common pattern. Example optimizations possible by analyzing i's behaviour:
sum=sum+i, then the entire loop may be collapsed to sum=sum+n*(n-1)/2.A[j][i] computed through t1=&A; t2 = t1 + j*1000; t3=t2+i*4, then this may be replaced by t4 = t4 + 4 in the loop body, where t4 is initialized to t1=&A; t4 = t1 + j*1000; before the loop is entered. This eliminates the multiplication operation.t4 so that every iteration can proceed in parallel, e.g., the ith processor can compute t2+i*4 independently.How is such an analysis done:
m'(a)=m(a)+1; m'(x)=m(x) for x != am'(a)=m(a)+i; m'(x)=m(x) for x != am'(a)=m(a)+n; m'(x)=m(x) for x != am'(a)=unknown; m'(x)=m(x) for x != aIn the iterative-analysis approach, we create transfer functions for basic blocks and then find the fixedpoint solution by repeated passes over the blocks. Instead of creating transfer functions just for individual blocks, a region-based analysis finds transfer functions that summarize the execution of progressively larger regions of the program. Ultimately, transfer functions for entire procedures are constructed and then applied, to get the desired data-flow values directly.
While a data-flow framework using an iterative algorithm is specified by a semilattice of data-flow values and a family of transfer functions closed under composition, region-based analysis requires more elements. A region-based framework includes both a semilattice of data-flow values and a semilattice of transfer functions that must possess a meet operator, a composition operator, and a closure operator.
A region-based analysis is particularly useful for data-flow problems where paths that have cycles may change the data-flow values. The closure operator allows the effect of a loop to be summarized more effectively than does iterative analysis.
The technique is also useful for interprocedural analysis, where transfer functions associated with a procedure call may be treated like the transfer functions associated with basic blocks. For simplicity, we shall consider only forward data-flow problems in this section. We first illustrate how region-based analysis works by using the familiar example of reaching definitions.
In region-based analysis, a program is viewed as a hierarchy of regions, which are (roughly) portions of a flow graph that have only one point of entry. We should find this concept of viewing code as a hierarchy of regions intuitive, because a block-structured procedure is naturally organized as a hierarchy of regions. Each statement in a block-structured program is a region, as control flow can only enter at the beginning of a statement. Each level of statement nesting corresponds to a level in the region hierarchy.
Formally, a region of a flow graph is a collection of nodes N and edges E such that
Clearly, a natural loop represents a region. Also, a basic block represents a region; similarly a single statement represents a region.
Example of non-region:
B1->B2 B2->B3 B3->B4 B1->B3 B2->B4The subgraph formed by B2-B3-B4 is not a region, but the entire graph is a region.
For the rest of the discussion, let's assume that the CFG is reducible. Every non-reducible graph can be converted to a reducible graph by duplicating subgraphs, but this may increase the code-size exponentially. For our example of a non-reducible graph above
fi(x) = geni \union (x - killi).(f2 o f1)(x) = gen2 \union ((gen1 \union (x-kill1)) - kill2).(f1 ^ f2)(x) = (gen1 \union (x-kill1)) \union (gen2 \union (x-kill2))e:
for (j = ...) {
//A[j] = 0
t1 = 4*j
t2 = &A
t3 = t1+t2
*t3 = 0
}
//t1 = 4j; t3 = &A+4j in iteration j
to
t3 = &A
for (j = ...) {
*t3=0
t3 = t3+4
}
Example2
loop: b = a a = a + 1 c = a + b d = r + b e = f f = read() g = 2 * f a = a + 1 p = p + 1 q = p + aLet
m be a map of var->val at start of loop body.
At the end of each statement:
b = m(a) a = m(a) + 1 c = 2*m(a) + 1 d = m(r) + m(a) e = m(f) f = unknown g = unknown a = m(a) + 2 p = m(p) + 1 q = m(p) + m(a) + 3
Let m' be a map of var->val at start of loop at end of i-1 iteration
m'(r)=m(r) m'(a)=m(a)+2i-2 m'(p)=m(p)+i-1 m'(f)=unknown
At end of each statement in i'th iter:
b=m(a)+2i-2 a=m(a)+2i-1 c=2m(a)+4i-3 d=m(r)+m(a)+2i-2 e=unknown f=unknown g=unknown a=m(a)+2i p=m(p)+i q=m(p)+m(a)+3iWherever possible, represent values as affine expressions on the loop iteration index.
for (i = ....) {
j = c0 + c1*i
... j ...
}
to
j = c0 + c1;
for (i = ...) {
... j ...
j += c1
}
i for each loop, with values 1, 2, ... for 1st, 2nd iterations etc.f be the transfer function of statement: x = c0+c1*y + c2*z
x = c0 + c1*y + c2*zm'(v) =
Summary: region-based analysis is an alternate algorithm for iterative data-flow. It works in transfer function space with composition (o), meet (^) and kleene start (*) operators. Bottom-up analysis summarizes the effect of regions with transfer functions.
Induction variable analysis is useful for strength reduction and for symbolic analysis for parallelization.