Consider the following example:
d0: y = 3 d1: x = y + 4 d2: y = 11 d3: if e ... d4: x = 5 d5: y = x + 6 d6: if e2 ...If we do CP for each variable separately, then the order of picking the variables has an effect on the precision of the final solution. In the example above, if we pick
y before x, we get a
more precise solution for the first basic block; on the other hand if we pick
x before y, we get a more precise solution for the
second basic block.
Consider an alternate representation of constant information using sets where all variables are tracked simultaneously. This representation can provide both better precision and better efficiency, than running the single-variable implementation multiple times.
There are two options for the transfer function for d1: either
use the in value to determine that x is a constant
too, or simply represent the transfer function as a GEN/KILL set (as a function
of the statement, but independent of the input value). The former is
more precise than the latter, e.g., the former can have logic that says something
like: if s: z=x+y, and both x,y are constants in in, then
f_s(in) assigns z to a constant --- something like this
cannot be captured in the GEN/KILL set based transfer function as these sets are
only functions of s and have no visibility into in.
This representation allows transfer functions to be written using generate (Gen[s]) and propagate (in[s] - Kill[s]) definitions.
Can summarize a basic block through its own composite transfer function as follows:
Gen[B]: locally exposed constant definitions, definitions available at the end of the basic block B
Kill[B]: the set of constant definitions killed by B
Effects of edges: nodes with multiple predecessor: meet operator is Intersection: in[b] = out[p1] n out[p2] ... n out[pn], for p1..pn predecessors of b
Cyclic graphs: equations still hold:
input: control flow graph CFG = (N, E, Entry, Exit)
//Boundary condition
OUT[Entry] = empty set
//Initialization for iterative algorithm
For each basic block B other than Entry
OUT[B] = {(x,\Top) for all x}
//Iterate
While (changes to any OUT occur) {
For each basic block B other than Entry {
in[B] = intersection over (out[p]), for all preds p of B
out[B] = fB(in[B]) //out[B] = gen[B] u (in[B] - kill[B])
}
}
Summary of constant information analysis
| Constant information | |
| Domain | Sets of constant definitions |
| Transfer function fb(x) | forward: out[b] = fb(in[b]) out[b] = Gen[b] U (x - Kill[b]) |
| Meet operation | in[b] = n out[predecessors] intersection |
| Boundary condition | out[entry] = empty |
| Initial interior points | out[b] = {(x,\top): for all variables x} |
(x,\top) and (x,3) will meet to return an empty set (bottom), which is more imprecise than our previous meet operator that would have resulted in (x,3). It is possible to make the meet operator more precise, by giving special treatment to the \top value, making the analysis more precise and still correct.Some observations:
\top (not live) encoded? By presence in the set.\bot (live) encoded? By absence in the set.Liveness : Iterative algorithm
input: control flow graph CFG = (N, E, Entry, Exit)
//Boundary condition
In[Exit] = empty
//Initialization for iterative algorithm
For each basic block B other than Exit
In[B] = empty
//Iterate
While (changes to any IN occur) {
For each basic block B other than Exit {
out[B] = U (in[s]) for all successors s of B
in[B] = fB(out[B]) //in[B]=Use[B] U (out[B] - Def[B])
}
}
Framework
| Constant information | Live variables | |
| Domain | Sets of constant definitions | Sets of variables |
| Direction and Transfer function fb(x) | forward: out[b] = fb(in[b]) out[b] = Gen[b] U (x - Kill[b]) in[b] = n out[predecessors] | backward: in[B] = fb(out[B]) in[b] = Use[b] U (x - Def[b]) out[B] = U in[succ[B]] |
| Meet operation | intersection | union |
| Boundary condition | out[entry] = empty | in[exit] = empty |
| Initial interior points | out[b] = {(x,\top): for all variables x} | in[b] = empty |
Exercise: must-reach definitions
a is not redefined along any path after last appearance of D and before PExercise: reaching definitions
Would the following be a legal solution to reaching definitions?
Entry --> BB1 BB1 --> BB2 BB2 --> BB2 BB2 --> BB3 BB3 contains (d1: b = 1) definition BB3 --> exitCandidate solution:
in[BB1] = empty
out[BB1] = empty
in[BB2] = {d1}
out[BB2] = {d1}
in[BB3] = {d1}
out[BB3] = {d1}
in[exit] = {d1}
Will our iterative worklist algorithm generate this answer?
Answer: this is a legal fixed-point solution to the system of equations (it satisfies all the rules/constraints of the transfer function and the meet operator). However this is not a maximum fixed-point (later).
Data-flow problems (F, V, ^) are defined by
Example of a semi-lattice diagram: V = { x | x is a subset of {d1,d2,d3}}, ^ = set-union
Draw the semi-lattice with arrows pointing downwards (>= relation)
Some interesting properties
x^y is the first common descendant of x and yx ^ \Top = xx ^ \Bottom = \BottomA meet-operator defines a partial-order and vice-versa
Summary:
x ^ x = xx ^ y = y ^ xx ^ (y ^ z) = (x ^ y) ^ z x ≤ xif x ≤ y and y ≤ x then x = yif x ≤ y and y ≤ z then x ≤ zAnother example: semi-lattice with V = {x | such that x is a subset of {d1, d2, d3}}, ^ = \intersection
Descending chain
x0 > x1 > x2 > ... > ...
Product of semi-lattices. One element at a time
Monotonicity Consider the iterative fixed point algorithm. Recall that we intuitively concluded that the solution keeps descending, and because there is a finite number of steps that can be descended, the algorithm will convert. We used a (co)induction argument to conclude that the solution keeps descending: on a meet, if the current inputs are lower than previous inputs, then the current output is necessarily lower than the previous outputs (because the meet operator is defined as the greatest lower bound). However, what happens on a transfer across a statement: if it is possible that a lower input produces a higher output due to a transfer function f \in F, then we cannot make this argument that the solution keeps descending. To still be able to make this argument, we add a third constraint to F that all functions must be monotone.
f(x) ≤ x.
if (...) { x = 2; y = 3; } else { x = 3; y = 2; }; z = x + y; f(x)^f(y) = 5; f(x^y) = \Bottom.x := 4; y := 3 and BB2 has x := 3; y := 4; whereas BB3 has z := x + y.
v1={(x,4),(y,3)} and v2={(x,3),(y,4)} respectively.v3=v1^v2={} and then apply BB3's transfer function f to obtain out3=f(v3)={}.v4=f(v1)={(x,3),(y,4),(z,7)} and v5=f(v2)={(x,4),(y,3),(z,7)}, and then meet to obtain out4=v4^v5={(z,7)}.For some DFA frameworks, it is true that f(x^y) = f(x)^f(y) for all x,y \in V and f \in F. Can you think of some examples?
Example: reaching definitions: f(x) = Gen \union (x - Kill), ^ = \union
Also liveness. In fact, even for constant propagation, if I use GEN/KILL based
transfer functions, my framework becomes distributive, as: f(v1) = (v1-KILL)\cup GEN and f(v2) = (v2-KILL)\cup GEN, and so
f(v1)^f(v2) = f(v1) \cap f(v2) = ((v1-KILL)\cup GEN) \cap ((v2-KILL)\cup GEN) = ((v1 \cap v2) - KILL) \cup GEN which is the same as f(v1^v2).
For a distributive framework, it does not matter if we meet earlier or later. Notice it is not necessary that a distributive framework is
more precise than a monotone framework for the same problem, e.g., the GEN/KILL based framework for constant propagation is actually less
precise, yet distributive.
Distributivity: A framework (F, V, ^) is distributive if and only if:
Distributivity implies monotonicity, but not vice-versa.
Behaviour of iterative algorithm (intuitively): For each IN/OUT of an interior program point:
Meet over paths MOP solution:
What is the difference between MOP and MFP of data flow equations? Consider two paths F1.F3 and F2.F3. MOP = F3F1(x) ^ F3F2(x). MFP (through iterative algo) = F3(F1(x) ^ F2(x)). Therefore
What should be the order of relaxing the values across CFG edges? If we examine edges in random order, we are likely to encounter nodes where no change has happened. This is tackled through Kildall's worklist algorithm. Every time something changes at a PC p, the nodes neighbouring (successor nods in case of forward DFA, predecessor nodes in case of backward DFA) that PC get added to the worklist. At every step, the fixed point algorithm picks a node from the worklist, and relaxes the value at that node (by considering the constraints at all predecessor/successor nodes); if nothing changed at that node, that node is simply removed from the worklist; if something changed at that node, the neighboring nodes are added to the worklist. This algorithm continues until the worklist becomes empty (i.e., nothing can potentially change). The invariant of the algorithm is that any nodes that can potentially change values are always in the worklist (nodes outside the worklist can never change values). At initialization time, the worklist is initialized with all nodes that can potentially cause some value to change (e.g., only the entry node for constant propagation). In some analyses, it is possible for the worklist to be initialized with all the CFG nodes.
However: given a worklist, should we have a preferred order in which to choose nodes from the worklist? Consider an example where we have "A->B->C" and "A->C". Let's say we have both "B" and "C" in the worklist. For a forward DFA, we should prefer picking B first. This is because B can reach C, but not vice-versa. Such reachability relationships require a topological sort order (for forward DFA), wherever a topological reachability order exists, e.g., it does not exist in the presence of cycles, in which case those nodes may be ordered arbitrarily. One convenient way of generating a topological sort order is the reverse postorder, which involves performing a depth-first search on the graph and numbering the nodes based on the order in which they are last visited. For a backward DFA, the postorder works best for picking a node from the worklist.