Single Static Assignment Intermediate Representation (or SSA IR) Many examples and pictures taken from Wikipedia
Compiler Structure ● Front End ● Source Code → IR ● Middle End ● IR → IR ● Back End ● IR → Output Form (e.g. machine code, byte code)
Intermediate Representation ● Basic Blocks ● Sequence of linear operations ● Control flow graph ● Connects basic blocks ● Describes one or more functions ● Compiler “middle end” transforms the IR such that the compiler output will ● Perform the same computation ● Run faster on the concrete hardware
SSA Form ● Intermediate Representation is constructed to never re-assign (or mutate) variables. ● (Just add a subscript to each assignment) ● This means that along a given control flow path each variable name represents a value. ● This simplifies a number of optimizations.
Example (1/2) – Rename variables
Example (2/2) – Add “phony” function
Minimal SSA ● We want some basic guarantees ● Minimal number of Φ functions are inserted so that we are sure that: ● Each name is assigned a value exactly once ● Each use of a name in the original program has a unique name to reference ● There is an efficient algorithm to get this
Minimizing SSA – Dominance ● Given an SSA control flow graph, ● We say a node (= basic block) A dominates another node C if ● A and C are the same node OR ● A will always run before C. ● We say that a node B is in the Dominance Frontier of C if ● B does not dominate C AND ● B does dominate an immediate predecessor of C
Dominance Example ● A dominates B ● A dominates C ● B does not dominate C ● B is in the dominance frontier of C ● For block C, how do we tell that we need a Φ func for y but not for x? A BD C
Constant Propagation (1/2) int computify(int x) { int z = 2; x = 14; x = 7 - x / z; return x * (28 / z + 2); } int computify2(int x1) { int z1 = 2; int x2 = 14; int x3 = 7 – x2 / z1; return x3 * (28 / z1 + 2); }
Constant Propagation (2/2) int computify2(int x1) { z1 = 2; x2 = 14; x3 = 7 – x2 / z1; return x3 * (28 / z1 + 2); } int computify3(int x1) { z1 = 2; x2 = 14; x3 = 7 – 14 / 2 = 0; return 0 * (28 / 2 + 2) = 0; }
Dead Code Elimination int computify3(int x1) { z1 = 2; x2 = 14; x3 = 0; return 0; } int computify4(int x1) { return 0; }
Global Value Numbering ● Each expression that produces a value in a graph is numbered. ● Duplicate computations can then be eliminated. a = b * c; d = c; e = b * d; f = e + d + 4; v3 = v1 * v2; v2 = v2; v3 = v3; v4 = v3 + v2 + 4;
Sparse Conditional Constant Propagation ● A more powerful version of constant propagation and dead code elimination. ● Traverse the control flow graph like an interpreter. ● Propagate constant values. ● Variables are unknown ● If a conditional branch can't be resolved, take both ● Any code not reached in the traversal is statically unreachable and can be pruned.
Register Allocation (1/2) ● Modern processors have 8 – 64 registers. ● During a basic block or function, which variables (values) go in which registers when? ● What if there are more variables than registers? ● Spill them to the stack. ● Which variables do we spill?
Register Allocation (2/3) ● Register allocation for one basic block: ● First, calculate the live range of each variable. ● When is it first and last referenced? ● Build a conflict graph. Edges denote variables that are alive at the same time, and thus can't go in the same register. ● If we can color the graph with N colors such that no two neighbors are the same color, that's a register allocation with no spills for N registers.
Register Allocation (3/3) ● The standard algorithm for register allocation, graph coloring, takes worst case exponential time in general. ● Hack and Grund (2005) show that SSA form can reduce the graph coloring problem to polynomial (linear?) time. ● Figuring out the optimal variables to spill to minimize memory access is still NP-complete.