330 likes | 482 Views
Lecture #12, May 15, 2007. Basic Blocks, Control flow graphs, Liveness using data flow, dataflow equations, Using fixed-points. Assignments. Reading – chapter 9. Sections 9.1 and 9.2 Liveness analysis. pp 433-452 Possible quiz Wednesday or next Monday. Basic Blocks.
E N D
Lecture #12, May 15, 2007 • Basic Blocks, • Control flow graphs, • Liveness using data flow, • dataflow equations, • Using fixed-points.
Assignments • Reading – • chapter 9. Sections 9.1 and 9.2 Liveness analysis. pp 433-452 • Possible quiz Wednesday or next Monday
Basic Blocks • Extend analysis of register use to program units larger than expressions but still completely analyzable at compile time. • Basic Block = sequence of instructions with single entry & exit. • If first instruction of BB is executed, so is remainder of block (in order).
Calculating Basic Blocks • To calculate basic blocks: • Determine BB leaders (→) : • First statement in routine • Target of any jump (conditional or unconditional). • Statement following any jump. • Basic block extends from leader to (but not including) next leader (or end of routine).
Basic Block Example prod := 0; i := 1; while i <= 20 do prod := prod + a[i] * b[i]; i := i + 1 end →1. prod := 0 2. i := 1 → 3. if i > 20 goto 14 → 4. t1 := i * 4 5. t2 := addr a 6. t3 := *(t2+t1) 7. t4 := i * 4 8. t5 := addr b 9. t6 := *(t5+t4) 10. t7 := t3 * t6 11. prod := prod + t7 12. i := i + 1 13. goto 3 → 14. ---
ML code Strategy – Move code from input to the current block list, until we see a leader. Then add current as a new block to blocks, and reinitialize current to the empty list. fun bb (LABEL n :: ss) current blocks = bb ss [LABEL n] (rev current :: blocks) | bb (JUMP n :: ss) current blocks = bb ss [] (rev (JUMP n :: current) :: blocks) | bb ((s as (CJUMP _)) :: ss) current blocks = bb ss [] (rev (s :: current) :: blocks) | bb ((s as (CALLST _)) :: ss) current blocks = bb ss [] (rev (s :: current) :: blocks) | bb (COMMENT(s,m):: ss) current blocks = bb (s::ss) current blocks | bb (s::ss) current blocks = bb ss (s::current) blocks | bb [] [] blocks = rev blocks | bb [] current blocks = rev((rev current) :: blocks)
Example T1 := 0 L1: T2 := T1 + 1 T3 := T2 + T3 T1 := (2 * T2) if T1 < 1000 GOTO L1 return T3 T1 := 0 L1: T2 := T1 + 1 T3 := T2 + T3 T1 := (2 * T2) if T1 < 1000 GOTO L1 return T3 T1 := 0 L1: T2 := T1 + 1 L1: T3 := T2 + T3 T2 := T1 + 1 L1: T1 := (2 * T2) T3 := T2 + T3 T2 := T1 + 1 L1: The current block grows in reverse order with most recursive calls
BB Code Generation using Liveness • Can combine code generation with ``greedy'' register allocation: • Bring each variable into a register when first needed, and leave it there as long as it's needed (if possible). • Maintain register descriptors saying which variable is in each register, and address descriptors saying where (in memory and/or a register) each variable is.
Algorithm • For each IR instruction x := y op z • If y isn't in a register, load it into a free one, updating descriptors. • Similarly for z . • If y and/or z are no longer live following this instruction, mark their registers as free. • Choose a free register for x , updating descriptors. • Generate instruction MOVE(rx,BINOP(op,ry,rz))
Notes • For the special case x := y , load y into a register, if necessary, and then mark that register as holding x too. • Must now be careful not to free a register unless none of its associated variables is live. • Registers behave like a cache for memory locations. • Nasty problems for source-level debuggers and dump utilities – where is that variable?!?
Example Source code: d := (a-b) + (a-c) + (a-c) Live after inst: a b c IR: t := a - b a c t u := a - c t u v := t + u u v d := v + u d
Algorithm trace Descriptors after Instructions IR Statement Code Regs Addrs - a,b,c:mem t := a - b ld a,r0 r0:a a:r0,mem ld b,r1 r1:t b,c:mem sub r0,r1,r1 t:r1 u := a - c ld c,r2 r0:u a,b,c:mem sub r0,r2,r0 r1:t u:r0 t:r1 v := t + u add r1,r0,r1 r0:u a,b,c:mem r1:v u:r0 v:r1 d := v + u add r0,r1,r0 r0:d a,b,c:mem st r0,d r1:v d:r0,mem
Control Flow Graphs • To assign registers on a per-procedure basis, need to perform liveness analysis on entire procedure, not just basic blocks. • To analyze the properties of entire procedures with multiple basic blocks, we use a control-flow graph. • In simplest form, control flow graph has one node per statement, and an edge from n1 to n2 if control can ever flow directly from statement 1 to statement 2.
We write pred[n] for the set of predecessors of node n, • and succ[n] for the set of successors. • (In practice, usually build control-flow graphs where each node is a basic block, rather than a single statement.) • Example routine: a = 0 L: b = a + 1 c = c + b a = b * 2 if a < N goto L return c
Example | | 1 ▼ .-------. | a = 0 | `-------’ | | .------. | | | 2 C▼ | .-----------. | | b = a + 1 | | `-----------’ | | | | | 3 ▼ | .-----------. | | c = c + b | | `-----------’ | | | | | 4 ▼ | .-----------. | | a = b * 2 | | `-----------’ | | | | | 5 ▼ | .-------. | | a < N | | `-------’ | | T | | F | `-------’ | 6 ▼ .----------. | return c | `----------’ pred[1] = ? pred[2] = {1,5} pred[3] = {2} pred[4] = {3} pred[5] = {4} pred[6] = {5} succ[1] = {2} succ[2] = {3} succ[3] = {4} succ[4] = {5} succ[5] = {6,2} succ[6] = {}
Liveness Analysis using Dataflow • Working from the future to the past, we can determine the edges over which each variable is live. • In the example: • b is live on 2 → 3 and on 3 → 4. • a is live from on 1 → 2, on 4 → 5, and on 5 → 2 (but not on 2 → 3 → 4). • c is live throughout (including on entry → 1). • We can see that two registers suffice to hold a, b and c.
Dataflow equations • We can do liveness analysis (and many other analyses) via dataflow analysis. • A node defines a variable if its corresponding statement assigns to it. • A node uses a variable if its corresponding statement mentions that variable in an expression (e.g., on the rhs of assignment). • Recall our ML function varsOf
Definitions • For any variable v define: • defV[v] = set of graph nodes that define v • useV[v] = set of graph nodes that use v • Similarly, for any node n, define • defN[n] = set of variables defined by node n • useN[n] = set of variables used by node n
Example | | 1 ▼ .-------. | a = 0 | `-------’ | | .------. | | | 2 C▼ | .-----------. | | b = a + 1 | | `-----------’ | | | | | 3 ▼ | .-----------. | | c = c + b | | `-----------’ | | | | | 4 ▼ | .-----------. | | a = b * 2 | | `-----------’ | | | | | 5 ▼ | .-------. | | a < N | | `-------’ | | T | | F | `-------’ | 6 ▼ .----------. | return c | `----------’ defV[a] = {1,4} defV[b] = {2} defV[c] = {?,3} useV[a] = {2,5} useV[b] = {3,4} useV[c] = {3,6} defN[1] = {a} defN[2] = {b} defN[3] = {c} defN[4] = {a} defN[5] = {} defN[6] = {} useN[1] = {} useN[2] = {a} useN[3] = {c,b} useN[4] = {b} useN[5] = {a} useN[6] = {c}
Setting up equations • A variable is live on an edge if there is a directed path from that edge to a use of the variable that does not go through any def. • A variable is live-in at a node if it is live on any in-edge of that node; • It is live-out if it is live on any out-edge. • Then the following equations hold live-in[n] = useN[n] U (live-out[n] – defN[n]) live-out[n] = U s succ(n) live-in[s]
Computing • We want the least fixed point of these equations: the • smallest live-in and live-out sets such that the equations hold. • We can find this solution by iteration: • Start with empty sets for live-in and live-out • Use equations to add variables to sets, one node at a time. • Repeat until sets don't change any more. • Adding additional variables to the sets is safe, as long as the sets still obey the equations, but inaccurately suggests that more live variables exist than actually do.
The Problem • We want to compute: live-in and live-out • We know: • by using: live-in[n] = useN[n] U (live-out[n] – defN[n]) live-out[n] = U s succ(n) live-in[s] succ[1] = {2} succ[2] = {3} succ[3] = {4} succ[4] = {5} succ[5] = {6,2} succ[6] = {} defN[1] = {a} defN[2] = {b} defN[3] = {c} defN[4] = {a} defN[5] = {} defN[6] = {} useN[1] = {} useN[2] = {a} useN[3] = {c,b} useN[4] = {b} useN[5] = {a} useN[6] = {c}
Example live-in[n] = useN[n] U (live-out[n] – defN[n]) live-out[n] = U s succ(n) live-in[s] succ[1] = {2} succ[2] = {3} succ[3] = {4} succ[4] = {5} succ[5] = {6,2} succ[6] = {} defN[1] = {a} defN[2] = {b} defN[3] = {c} defN[4] = {a} defN[5] = {} defN[6] = {} useN[1] = {} useN[2] = {a} useN[3] = {c,b} useN[4] = {b} useN[5] = {a} useN[6] = {c} Lets do node 5 live-out[5] = { } live-in[5]={ } live-out[5] = U s {6,2} live-in[s] so now we need to do live-in[6] and live-in[2]
Solution • For correctness, order in which we take nodes doesn't matter, but it turns out to be fastest to take them in roughly reverse order: • live-in[n] = use[n] U (live-out[n] – def[n]) • live-out[n] = U s succ(n) live-in[s]
Implementation issues • Algorithm always terminates, because each iteration must enlarge at least one set, but sets are limited in size (by total number of variables). • Time complexity is O(N4) worst-case, but between O(N) and O(N2) in practice. • Typically do analysis using entire basic blocks as nodes. • Can compute liveness for all variables in parallel (as here) or independently for each variable, on demand. • Sets can be represented as bit vectors or linked lists; best choice depends on set density.
ML code • First we need operations over sets • union • setMinus • normalization fun union [] ys = ys | union (x::xs) ys = if List.exists (fn z => z=x) ys then union xs ys else x :: (union xs ys)
SetMinus fun remove x [] = [] | remove x (y::ys) = if x=y then ys else y :: remove x ys; fun setMinus xs [] = xs | setMinus xs (y::ys) = setMinus (remove y xs) ys
Normalization fun sort' comp [] ans = ans | sort' comp [x] ans = x :: ans | sort' comp (x::xs) ans = let fun LE x y = case comp(x,y) of GREATER => false | _ => true fun GT x y = case comp(x,y) of GREATER => true | _ => false val small = List.filter (GT x) xs val big = List.filter (LE x) xs in sort' comp small (x::(sort' comp big ans)) end; fun nub [] = [] | nub [x] = [x] | nub (x::y::xs) = if x=y then nub (x::xs) else x::(nub (y::xs)); fun norm x = nub (sort' String.compare x [])
liveness algorithm fun computeInOut succ defN useN live_in live_out range = let open Array fun out n = let val nexts = sub(succ,n) fun getLive x = sub(live_in,x) val listOflists = map getLive nexts val all = norm(List.concat listOflists) in update(live_out,n,all) end fun inF n = let val ans = union (sub(useN,n)) (setMinus (sub(live_out,n)) (sub(defN,n))) in update(live_in,n,norm ans) end fun run i = (out i; inF i) in map run range end; Array access functions x[i] == sub(x,i) x[i] = e == update(x,i,e)
val it = [|[],[],[],[],[],[],[]|] val it = [|[],[],[],[],[],[],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],[],["a"],["b","c"],["b"],[],["c"]|] val it = [|[],[],[],[],[],["a"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],[],["a"],["b","c"],["b"],[],["c"]|] val it = [|[],["a"],["b","c"],["b"],[],["a","c"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],[],["a","c"],["b","c"],["b"],["a","c"],["c"]|] val it = [|[],["a"],["b","c"],["b"],[],["a","c"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],[],["a","c"],["b","c"],["b"],["a","c"],["c"]|] val it = [|[],["a","c"],["b","c"],["b"],["a","c"],["a","c"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],["c"],["a","c"],["b","c"],["b","c"],["a","c"],["c"]|] val it = [|[],["a","c"],["b","c"],["b"],["a","c"],["a","c"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],["c"],["a","c"],["b","c"],["b","c"],["a","c"],["c"]|] val it = [|[],["a","c"],["b","c"],["b","c"],["a","c"],["a","c"],[]|] - computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]; val it = [|[],["c"],["a","c"],["b","c"],["b","c"],["a","c"],["c"]|] val it = [|[],["a","c"],["b","c"],["b","c"],["a","c"],["a","c"],[]|]
Fixed point algorithm • Repeat computeInOut until live_in and live_out remain unchanged after a full iteration. • The comparison is expensive. • Since we never subtract any thing from one of these arrays, we need only detect when we assign a value to a particular index that is different from the one already there. • A full iteration with no changes, means we’ve reached a fixpoint. fun change (array,index,value) = let val old = Array.sub(array,index) in Array.update(array,index,value) ; Bool.not(old=value) end; Returns true only if we’ve made a change
Second try fun computeInOut succ defN useN live_in live_out range = let open Array fun out n = let val nexts = sub(succ,n) fun getLive x = sub(live_in,x) val listOflists = map getLive nexts val all = norm(List.concat listOflists) in change(live_out,n,all) end fun inF n = let val ans = union (sub(useN,n)) (setMinus (sub(live_out,n)) (sub(defN,n))) in change(live_in,n,norm ans) end fun run(i,change) = (out i orelse inF i orelse change) in List.foldr run false range end; returns true only if a change has been made iterates over all n and determines if any change. Note change starts at false.
Keep applying fun try succ defN useN = let val n = Array.length succ val live_in = Array.array(n,[]:string list) val live_out = Array.array(n,[]:string list) fun repeat () = if (computeInOut succ defN useN live_in live_out [6,5,4,3,2,1]) then repeat () else () in repeat(); (live_out,live_in) end;