640 likes | 795 Views
Control Flow Analyses. The material in these slides have been taken from Chapter 3 of the book "Principles of Program Analysis", by Nielson et al . Warm up Example. int arith(int b , int e ) { int sum = 0; int counter = b ; while (counter < e ) { sum += counter; counter++;
E N D
Control Flow Analyses The material in these slides have been taken from Chapter 3 of the book "Principles of Program Analysis", by Nielson et al.
Warm up Example intarith(intb, inte) { int sum = 0; int counter = b; while (counter < e) { sum += counter; counter++; } return sum; } int main() { int N0 = 0, N1 = 10; int N3 = arith(N0, N1); } How could we optimize this program? Which knowledge would we need to optimize this program? How can we obtain this knowledge? How would be the CFG of this example, considering that we have two functions?
Inter-Procedural CFG What is the glue between arith and main? How could we analyze this code, using our data-flow framework?
Inter-Procedural Analysis Once we have built the inter-procedural CFG, we can just run our ordinary data-flow analysis on it.
Making it a bit Harder intarith(intb, inte) { int sum = 0; int counter = b; while (counter < e) { sum += counter; counter++; } return sum; } int (*f)(int, int) = &arith; int main() { int N0 = 0, N1 = 10; int N3 = (*f)(N0, N1); } Which optimization would speed up the program on the left? But this case is trickier: how can we find out the inter-procedural control flow graph of this program?
Programs with Dynamic Control Flow • The analyses that we have seen so far assume that the program has a well-defined shape. • Each instruction is succeeded by a small number of next instructions. • Sometimes we do not know the shape of the control flow graph of a program. • This shortcoming is typical in programming languages with dynamic dispatch. • What is dynamic dispatch? • Can you give examples of programming languages with this capacity?
Programs with Dynamic Control Flow • Dynamic dispatch is typical in functional and object oriented languages. • As an example, consider the SML program below: • What is the value of this program? • What is the control flow graph of this program? • Could similar difficulties surface in object oriented languages? let valf = fn x => x 1; valg = fn y => y + 2; valh = fn z => z + 3 in (fg) + (fh) end
Programs with Dynamic Control Flow • The program on the right suffer from similar problems as our previous example, in SML • We cannot tell precisely, at compilation time, what is the control flow graph of this program. • The last method invocation, e.g., o.add(p) will be defined only at execution time. class A: def __init__(self): self.list = [] def add(self, n): self.list.append(n) def __str__(self): return str(self.list) class B: def __init__(self, n): self.n = n def add(self, n): self.n += n def __str__(self): return str(self.n) o = A(); p = int(raw_input("p = ")) if p % 2 == 1: o = B(p) o.add(p) print o
Function Inlining • Once we know the target of a function call, we can inline it. • Inlining is a whole program optimization, in which the call to a function is replaced by the function body itself. What is the main benefit of inlining?
Function Inlining • Once we know the target of a function call, we can inline it. • Inlining is a whole program optimization, in which the call to a function is replaced by the function body itself. • The key benefit of inlining is speed: we save the cost of passing parameters and reading return values. • But there is another benefit: we can optimize the program more aggressively! intarith(intb, inte) { int sum = 0; int counter = b; while (counter < e) { sum += counter; counter++; } return sum; } int main() { int N1 = 0, N2 = 10; int N3 = 11, N4 = 20; arith(N1, N2); arith(N3, N4); } Can we do any constant propagation in the program on the right?
Context • The context of a function call is sequence of calls that have been called before it. • In this example, we have two contexts: • main, and arith at line 4 • main, and arith at line 5 • Each context passes different information to arith; thus, we cannot assume that any parameter is constant. How can inlining fix this problem for us?
Function Inlining Inlining creates new opportunities for optimization, because it makes them context sensitive. By context sensitive, we mean that the optimizer might disambiguate different calling sites. int main() { int N1 = 0, N2 = 10; int N3 = 11, N4 = 20; int sum1 = 0, sum2 = 0; int counter1 = N1; int counter2 = N3; while (counter1 < N2) { sum1 += counter1; counter1++; } while (counter2 < N4) { sum2 += counter2; counter2++; } } intarith(intb, inte) { int sum = 0; int counter = b; while (counter < e) { sum += counter; counter++; } return sum; } int main() { int N1 = 0, N2 = 10; int N3 = 11, N4 = 20; arith(N1, N2); arith(N3, N4); }
Which Function Can I inline? Discovering which function we can inline is not always an easy problem, depending on the programming language. Which inlining can we perform in the program on the left? Which information do we need to perform inlining in this kind of situation? Any idea on how we can discover this kind of information? fun end_point s = s ^ "." fun inc x = x + 1 fun square y = y * y fun map _ nil = nil | map f (h::t) = f h :: map f t val l0 = [0, 1, 2] fun imap f = map f l0 val points = map end_point ["hi", "there"] val y = if true the Imapinc else Imap square
Control Flow Analysis • There exists a general technique to determine the control flow of programs with dynamic dispatch. • This technique is called control flow analysis. • We shall explain it in three steps: • We will specify the problem formally, providing a definition of a correct solution to the control flow analysis problem. • We will extract constraints from this specification. A solution to these constraints is a solution to the formal specification of the problem. • We will provide an efficient, graph-based, algorithm to solve these constraints.
The Subject Language • We will be working with a subset of SML to illustrate the Control Flow Analysis. • Other languages, e.g., object oriented, would be analyzed in the same way. e ::= tl t ::= c | x | fnx => e0 | e1 e2 | ife0thene1elsee2 | letx = e1ine2 | e1 op e2 Could you explain what each one of these terms is all about?
An Example in SML e ::= tl t ::= c | x | fnx => e0 | e1 e2 | ife0thene1elsee2 | letx = e1ine2 | e1 op e2 let val f = fn x => x + 1 val g = fn y => y * y val max = fn a => fn b => if a > b then a else b in max (f 1) (g 1) end What does this program above do?
Variables and Labels • Our goal is to find a valid set of expressions bound to each variable and each syntactic term of a program. • Variables are determined by their names. • Let's assume that each variable name is unique! • Terms are determined by labels. e ::= tl t ::= c | x | fnx => e0 | e1 e2 | ife0thene1elsee2 | letx = e1ine2 | e1 op e2 • Below we have an example of input program: ((fn x => x1)2 (fn y => y3)4)5 Which functions can be bound to variables x and y? What about the terms labeled 1, 2, 3, 4 and 5?
Cache and Environment • In our context, a value is the syntactical representation of a function, e.g., fnx => x + 1 • The cache is a map of labels to values. • The environment is a map of variable names to values. • A solution to the control flow analysis is a tuple (C, R), in which C is a cache, and R is an environment.
Cache and Environment • We can accept conservative solutions, but we cannot accept incomplete solutions. ((fn x => x1)2 (fn y => y3)4)5 Which solution is too conservative, which solution is wrong, and which solution is just about right?
Conservative Solutions ((fn x => x1)2 (fn y => y3)4)5 We want to know which functions might be bound to variables or terms. So, if we say that any function is bound to anything, we are not wrong (but this is the solution of a coward!). However, (C1, R1) is wrong, because clearly the parameter x can assume fn y => y. This is a false negative, which we must avoid.
The Specification of a Solution • We say that (C, R) ⊨ e if the bindings in the cache C and in the environment R correctly represent the program e. • We must specify correctness rules for each syntactic category in our programming language. e ::= tl t ::= c | x | fnx => e0 | e1 e2 | ife0thene1elsee2 | letx = e1ine2 | e1 op e2 • Some rules are easy, e.g.: • (C, R) ⊨cl always • When does (C, R)⊨xl ?
Variables and Functions • We say that (C, R) ⊨ xl, if, and only if, R(x) ⊆ C(l) • When does (C, R) ⊨ (fnx => e0)l ?
Functions and Applications • When does (C, R) ⊨ xl ? • (C, R) ⊨ xl, if, and only if, R(x) ⊆ C(l) • When does (C, R) ⊨ (fnx => e0)l ? • (C, R) ⊨ (fnx => e0)l, if, and only if, {fnx => e0} ⊆ C(l) • When does (C, R) ⊨ (t1l1 t2l2)l ?
Stop and Breath • When does (C, R) ⊨ (t1l1 t2l2)l ? • (C, R) ⊨ (t1l1 t2l2)l, if, and only if, (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 and (∀(fnx => t0l0) ∈ C(l1) : (C, R) ⊨ t0l0 and C(l2) ⊆ R(x) and C(l0) ⊆ C(l)) Ok, now is when you start banging your head against the first solid thing around, decide to become a hippie, let the beard grow and give up… But not so quickly, if you look at these constraints, the do make some sense… If we have something like t1l1 t2l2, then we must analyze each label recursively. It is also true that any fnx => t0l0 that makes its way into t1l1 will receive t2l2 into x. That is why we have that second part. And, by the way: When does (C, R) ⊨ (ift0l0thent1l1elset2l2) l?
Let Bindings and Binary Operators • When does (C, R) ⊨ (ift0l0thent1l1elset2l2) l? • (C, R) ⊨ (ift0l0thent1l1elset2l2) l, if, and only if, (C, R) ⊨ t0l0 and (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 and C(l1) ⊆ C(l) and C(l2) ⊆ C(l) • When does (C, R) ⊨ (letx=t1l1int2l2) l? • (C, R) ⊨ (letx=t1l1int2l2) l, if, and only if, (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 and C(l1) ⊆ R(x) and C(l2) ⊆ C(l) • When does (C, R) ⊨ (t1l1 op t2l2) l?
Binary Operators and we are done! • When does (C, R) ⊨ (ift0l0thent1l1elset2l2) l? • (C, R) ⊨ (ift0l0thent1l1elset2l2) l, if, and only if, (C, R) ⊨ t0l0 and (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 and C(l1) ⊆ C(l) and C(l2) ⊆ C(l) • When does (C, R) ⊨ (letx=t1l1int2l2) l? • (C, R) ⊨ (letx=t1l1int2l2) l, if, and only if, (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 and C(l1) ⊆ R(x) and C(l2) ⊆ C(l) • When does (C, R) ⊨ (t1l1 op t2l2) l? • (C, R) ⊨ (t1l1 op t2l2) l, if, and only if, (C, R) ⊨ t1l1 and (C, R) ⊨ t2l2 Why don't we have that (t1l1 op t2l2) ⊆ C(l) here?
Why all this notation? • Notation is not bad: if we are fluent, then it is easier to read than natural language. • A good notation may be a key success factor. • Today, we use Leibnitz version of the calculus, because he had a very good notation. • In computer science, many programming languages, such as Prolog and SML, already resemble formal notation. • Thus, mapping from executable code to formal latex specification is not too hard.
0-CFA Analysis • We will be working only with control flow analyses that do not distinguish between different calling context. • These analyses are less precise • But they are more efficient (much more indeed!) • Let's illustrate this imprecision with an example: (let f = (fn x => x1)2 in ((f3 f4)5(fn y => y6)7)8)9 • Which term(s) can be bound to the variable x? • Which term(s) can be bound to the whole thing, e.g., label 9?
0-CFA Analysis • Our analysis will give the result below to the program: (let f = (fn x => x1)2 in ((f3 f4)5(fn y => y6)7)8)9 C(1) = {fn x => x1, fn y => y6} R(f) = {fn x => x1} C(2) = {fn x => x1} R(x) = {fn x => x1, fn y => y6} C(3) = {fn x => x1} R(y) = {fn y => y6} C(4) = {fn x => x1} C(5) = {fn x => x1, fn y => y6} C(6) = {fn y => y6} C(7) = {fn y => y6} C(8) = {fn x => x1, fn y => y6} C(9) = {fn x => x1, fn y => y6} So, in the end we conclude that the entire program can be either fn x => x or fn y => y, yet, it is easy to see that this program can only be fn y => y
0-CFA Analysis • We are imprecise because we are not context sensitive. (let f = (fn x => x1)2 in ((f3 f4)5(fn y => y6)7)8)9 • In other words, we cannot distinguish between the call sites of function f at label 3 and 4. • Indeed, in this program we have two different realizations of variable x, the first is only bound to fn x => x. The second is only bound to fn y => y. • The equivalent program below would be precisely analyzed. What is the value of this program? let f1 = (fn x1 => x1) in let f2 = (fn x2 => x2) in (f1 f2)(fn y => y)
Constraint Based 0-CFA Analysis • We will extract constraints from a program. • These constraints have one of two forms: • lhs ⊆ rhs • {t} ⊆ rhs' ⇒ lhs ⊆ rhs • rhs can be either C(l) or R(x) • lhs can be either C(l), R(x) or {t} • We will see efficient algorithms to solve these constraints. • And it is not too hard to show that they satisfy the correctness relations that we have defined before♣. • Given a term t, we will denote the constraints that we extract from t by X[t] ♣: see "Principles of Program Analysis", by Nielson et al, section 3.4.1
Constants and Variables • What are the constraints in X[cl]? • X[cl] = {} • What are the constraints in X[xl]?
Variables and Functions • What are the constraints in X[cl]? • X[cl] = {} • What are the constraints in X[xl]? • X[xl] = {R(x) ⊆ C(l)} • What are the constraints in X[(fnx => e0)l]?
Functions and Applications • What are the constraints in X[cl]? • X[cl] = {} • What are the constraints in X[xl]? • X[xl] = {R(x) ⊆ C(l)} • What are the constraints in X[(fnx => e0)l]? • X[(fnx => e0)l] = {{fnx => e0} ⊆ C(l)} ∪ X[e0] • What are the constraints in X[(t1l1 t2l2)l]?
Applications and Conditionals • What are the constraints in X[(t1l1 t2l2)l]? • X[(t1l1 t2l2)l] = X[t1l1] ∪ X[t2l2] ∪ {{t} ⊆ C(l1) ⇒ C(l2) ⊆ R(x) | t = (fnx => t0l0) ∈ Prog} ∪ {{t} ⊆ C(l1) ⇒ C(l0) ⊆ C(l) | t = (fnx => t0l0) ∈ Prog} • What are the constraints in X[ift0l0thent1l1elset2l2)l]? We are traversing the program, extracting constraints. Upon finding an application, we need to determine this "t". How do we find all the elements that t can be? Ok, ok… no complains… let’s all behave.
Conditionals and Let Bindings • What are the constraints in X[(t1l1 t2l2)l]? • X[(t1l1 t2l2)l] = X[t1l1] ∪ X[t2l2] ∪ {{t} ⊆ C(l1) ⇒ C(l2) ⊆ R(x) | t = (fnx => t0l0) in Prog} ∪ {{t} ⊆ C(l1) ⇒ C(l0) ⊆ C(l) | t = (fnx => t0l0) in Prog} • What are the constraints in X[ift0l0thent1l1elset2l2)l]? • X[ift0l0thent1l1elset2l2)l] = X[t0l0] ∪ X[t1l1] ∪ X[t2l2] ∪ {C(l1) ⊆ C(l)} ∪ {C(l2) ⊆ C(l)} • What are the constraints in X[letx=t1l1int2l2)l]?
Let Bindings and Binary Operators • What are the constraints in X[letx=t1l1int2l2)l]? • X[letx=t1l1int2l2)l] = X[t1l1] ∪ X[t2l2] ∪ {C(l1) ⊆ R(x)} ∪ {C(l2) ⊆ C(l)} • What are the constraints in X[t1l1 op t2l2)l]?
Let Bindings and Binary Operators • What are the constraints in X[letx=t1l1int2l2)l]? • X[letx=t1l1int2l2)l] = X[t1l1] ∪ X[t2l2] ∪ {C(l1) ⊆ R(x)} ∪ {C(l2) ⊆ C(l)} • What are the constraints in X[t1l1 op t2l2)l]? • X[t1l1 op t2l2)l] = X[t1l1] ∪ X[t2l2] Which constraints do we have for X[((fnx => x1)2 (fn y => y3)4)5]?
Example of Constraints X[((fnx => x1)2 (fn y => y3)4)5] = { {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(3) ⊆ C(5), } Can you put bounds on the number of constraints that we can extract from a program?
On the Number of Constraints • If we assume a program of size n, then we could have, in principle: • O(n2) constraints like lhs ⊆ rhs • O(n4) constraints like {t} ⊆ rhs' ⇒ lhs ⊆ rhs • However, if we look into the constructs that we may have in our programming language, then we can conclude that we have much less constraints. Can you explain these bounds? Can you revise the bounds found above?
On the Number of Constraints • If we assume a program of size n, then we could have, in principle: • O(n2) constraints like lhs ⊆ rhs • O(n4) constraints like {t} ⊆ rhs' ⇒ lhs ⊆ rhs • However, if we look into the constructs that we may have in our programming language, then we can conclude that we have much less constraints. • Every construct, but applications, e.g, (e1 e2), gives us only one constraint per se. Applications may gives us a linear number of constraints. In the end, how many constraints can we have, considering a program of size n?
Comparison with Data-Flow Constraints • We had seen constraint systems before, as a part of the data-flow monotone framework: IN[x1] = {} IN[x2] = OUT[x1] ∪ OUT[x3] IN[x3] = OUT[x2] IN[x4] = OUT[x1] ∪ OUT[x5] IN[x5] = OUT[x4] IN[x6] = OUT[x2] ∪ OUT[x4] OUT[x1] = IN[x1] OUT[x2] = IN[x2] OUT[x3] = (IN[x3]\{3,5,6}) ∪ {3} OUT[x4] = IN[x4] OUT[x5] = (IN[x5]\{3,5,6}) ∪ {5} OUT[x6] = (IN[x6]\{3,5,6}) ∪ {6} {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(3) ⊆ C(5) Control Flow Analysis What are the main differences between these two constraint systems? Data-Flow Analysis
Solving the Constraints • We can visualize a constraint system as a constraint graph: • The constraint graph has a node C(l) for each label l, and a node R(x) for each variable x. • Each constraint p1 ⊆ p2 adds an initial edge (p1, p2) to the graph. • Each constraint {t} ⊆ p ⇒ p1 ⊆ p2 adds acandidate edge (p1, p2), with trigger{t} ⊆ p. X[((fnx => x1)2 (fn y => y3)4)5] = { {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5), } How would be the constraint graph for the system above?
Solving the Constraints with the Constraint Graph • We can visualize a constraint system as a constraint graph: X[((fnx => x1)2 (fn y => y3)4)5] = { {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5), } • The constraint graph has a node C(l) for each label l, and a node R(x) for each variable x. • Each constraint p1 ⊆ p2 adds an initial edge (p1, p2) to the graph. • Candidate edges are dashed • Triggers are in squared boxes ∨
Zooming In Can you make sure that you understand how each constraint edge, candidate edge and trigger has been created? {fn x => x1} ⊆ C(2) R(x) ⊆ C(1) {fn y => y3} ⊆ C(4) R(y) ⊆ C(3) {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x) {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5) {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y) {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5) ∨ By the way: fn x => x1 = idx, and fn y => y3 = idy
Value Sets • Each node p of the constraint graph is associated with a set D[p] of values. • Just to remember: here a value is the syntactical representation of a function, e.g., fnx => x + 1 • Our goal is to find the value set of every node • Initially, we have that D[p] = {t | ({t} ⊆ p) ∈ X[e]} X[((fnx => x1)2 (fn y => y3)4)5] = { {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5), } What are the initial value sets in our example? ∨
Value Sets • Each node p of the constraint graph is associated with a set D[p] of values. • Just to remember: here a value is the syntactical representation of a function, e.g., fnx => x + 1 • Our goal is to find the value set of every node • Initially, we have that D[p] = {t | ({t} ⊆ p) ∈ X[e]} X[((fnx => x1)2 (fn y => y3)4)5] = { {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5), } ∨ Again: idx = {fn x => x1} idy = {fn y => y3}
The Sliding Behavior • If we have an edge (p1, p2) in our graph, then the value set of p1 must flow into the value set of p2. Why is this property true? X[((fnx => x1)2 (fn y => y3)4)5] = {fn x => x1} ⊆ C(2), R(x) ⊆ C(1), {fn y => y3} ⊆ C(4), R(y) ⊆ C(3), {fn x => x1} ⊆ C(2) ⇒ C(4) ⊆ R(x), {fn x => x1} ⊆ C(2) ⇒ C(1) ⊆ C(5), {fn y => y3} ⊆ C(2) ⇒ C(4) ⊆ R(y), {fn y => y3} ⊆ C(2) ⇒ C(1) ⊆ C(5), We can think about these solid edges as an “slide”. If our constraint graph contains an edge (i, j), then every value stored in i slides into j.