In this lab, you will build a static single-assignment form (SSA) for your Tiger compiler to perform optimizations. SSA is a state-of-the-art IR also used in production compilers such as GCC or LLVM.
This lab consists of two parts: first, in part A, you will design and implement data structures defining the static single-assigment form. You will also implement translations from CFG to SSA as well as translations from SSA back to CFG. Second, in part B, you will implement several classic data-flow analysis and optimizations on SSA.
First check out the source we offered you for lab7:
$ git commit -am 'my solution to lab6' $ git checkout -b lab7 origin/lab7
these commands will first commit your changes to the lab6 branch of your local Git repository, and then create a local lab7 branch and check out the remote lab7 branch into the new local lab7 branch.
Again, you will need to merge your code from lab6 into the new lab7 branch:
$ git merge lab6
Do not forget to resolve any conflicts before commit to the local lab7 branch:
$ git commit -am 'lab7 init'
You should first import the new lab7 code into your favorite IDE, and make sure the code compiles. There are a bunch of new files that you should browse through:
ssa/*: static single-assignment form and its operations
When you finished your lab, zip you code and submit to the online teaching system.
The static single-assignment form (SSA) is an important compiler intermediate representation, in which each variable is assigned (statically) at most once. SSA is more advantageous over other compiler IRs in that its single-assignment property makes program analysis and optimization simpler and more efficient. As a result, SSA has become a de-factor IR in modern optimizing compilers for imperative, OO, or even functional languages.
In this part of the lab, you will build a static single-assigment form for your Tiger compiler, and conduct optimizations based on your SSA IR. To aid in your implementation, we have given some hints for most exercises, but keep in mind that these hints are not mandatory, and you are free (and encouraged) to propose your own ideas.
To better reuse your existing code base, you will implement SSA by leveraging data structures already designed for the control-flow graph. This design decision makes the interfaces cleaner and more elegant hence simplifying subsequent compilation phases.
cfg/Cfg.java
.
A basic block b might contain a list of φ-instructions, which
reside at the top of a block.
Syntactically, a φ-function
x = φ((B_1, x_1), (B_2, x_2), ..., (B_p, x_p))takes a list of tuples
(B_i, x_i), 1≤ i ≤ p
as arguments
and selects one of the variable x_j
from
these tuples as
its value to assign to the left-side x
, when
the control-flow reach the current basic block from its
predecessor B_j
.
Your task is to extend the SSA data structures with
other necessary syntactic features, to support the compilation
of full MiniJava.
cfg/Cfg.java
, to visualize an
SSA graph, that is, to draw the SSA graph
with dot.
You will be constructing the SSA form with the following 5 steps: 1) calculate dominators; 2) build dominator trees; 3) calculate dominance frontiers; 4) place φ-functions; and 5) rename variables.
In this part, you will finish the first two steps, that is, you will first calculate dominators then build a dominator tree for a given CFG.
n
in the control-flow
graph, the dominators DOM[n]
for the node n
.
You should finish this exercise by filling in the
method dominators()
in the file util/Graph.java
.DOM[n] = {n} ⋃ (⋂p∈ pred[n] DOM[p])where
DOM[n0] = {n0}
for
the entry block n0
.O(n2)
due to its use of iterations to reach
the fixpoint. Implement a more efficient algorithm
to calculate dominators. You might consider the
Cooper's
algorithm
as a starting point.
g
, its dominator tree t
.
You should finish this exercise by filling in the
method dominatorTree()
in the file util/Graph.java
.DOM[n]
already calculated
for the node n
, and think carefully about the basic
data structure design to implement this algorithm.
n
in control-flow
graph g
, its immediate dominator IDOM[n]
.
You should finish this exercise by filling in the
method idom()
in the file util/Graph.java
.
In this part of the lab, you will be implementing the
third step to build an SSA, that is, you will calculate
the dominance frontier for each node.
The dominance frontier DF[n]
of a node n
is the
set of all nodes d
such that n
dominates an immediate predecessor of d
, but n
does not strictly dominate d
.
Intuitively, the dominance frontier DF[n]
for a node n
is the set of nodes where n
's
dominance stops.
DF[n]
for any node n
in the CFG.
You should finish this exercise by filling in the
method dominanceFrontier()
in the file util/Graph.java
.dominanceFrontier() foreach(node n ∈ CFG) DF[n] = ∅ // initialized to an empty set foreach(node n ∈ CFG) if(n has multiple predecessors) foreach(predecessor p of n) runner = p while(runner ≠ IDOM[n]) DF[runner] ⋃= {n} runner = IDOM[runner]where
IDOM[n]
represents
the immediate dominator for the node n
.
In this part, you will implement the fourth step to build an SSA, that is, you will be implementing the φ-function placement. The φ-function placement algorithm places φ-functions at the top of corresponding blocks. Specifically, for a definition
x = ...
to a variable
x
in a basic block
n
, this algorithm will place a φ-function
x = φ(x, ..., x)
at the top of each n
's
dominance frontier d ∈ DF[n]
.
placePhiFunctions()
in the file cfg/Cfg.java
.b
, take care not to
place φ-functions more than once
for a same variable x
.x
at a basic block b
also
makes b
a definition block for x
.
In this part of the lab, you will be implementing the fifth and last step to build an SSA, that is, you will rename variables to make its definition unique.
renameVariables()
in the file cfg/Cfg.java
.To this point, your Tiger compiler can convert all legal MiniJava programs to SSA forms. Do regression test your Tiger compiler and fix potential bugs.
Modern machines do not support the φ-functions directly, hence φ-functions must be eliminated before execution. This task is accomplished by the translation out of SSA forms.
outSSA(ssa) splitCriticalEdges(ssa); foreach(basic block b, with p predecessors b1, ..., bp) suppose b contains m φ-functions: x1 = φ(x11, ..., x1p); ... xm = φ(xm1, ..., xmp); generate m fresh variables: t1, ..., tm; foreach(b's predecessor block bi, 1≤ i ≤p) append these assignment statements at the tail of block bi: t1 = x1i ... tm = xmi append these assigment statements at the top of block b: x1 = t1 ... xm = tmwhere the function
splitCriticalEdges()
splits
critical edges in a directed graph.
You should finish this exercise by filling in the
method splitCriticalEdges()
in the file util/Graph.java
,
and by filling in the method outSsa()
in the file cfg/Cfg.java
.
To this point, your Tiger compiler can convert any SSA form back to a corresponding CFG. Do not forget to regression test your Tiger compiler.
SSA makes compiler optimizations not only easier to engineer but also more efficient to execute, largely due to its single-assignment property. This nice property make it much easier to calculate the data-flow information required to perform optimizations.
In this part of the lab, you will re-implement several optimizations that we have done on the CFG on SSA again: dead code-elimination, constant propagation, copy propagation, among others. And you will also implement several novel optimizations that are particularly suitable for SSA: conditional constant propagation, global value numbering, among others.
In SSA, a statement x = e
is dead,
if the variable x
is not used by any other
statement (and e
does not have side effects).
As a result, this statement x = e
can be safely
eliminated.
ssa/DeadCode.java
.
Note that φ-functions are also assignment statements
hence candidates for dead-code elimination.
In SSA, given a statement of the form x = c
in which
c
is a constant, we can propagate the constant c
to any use of variable x
.
ssa/ConstProp.java
.
Note that after the constant propagation optimizations,
the assigment statement x = c
might become dead statement,
manifesting opportunities for dead-code elimination optimizations.
In SSA, given a copy statement x = y
, then
any use of x
can be replaced by y
.
ssa/CopyProp.java
.
In SSA, if a φ-function is of a special form taking a list of same arguments:
x = φ(c, c, ..., c)
where c
is a constant,
then this φ-function can be eliminated by rewriting
it to a normal assignment to x
:
x = c
Note that, often the time,
this special form of φ-function x = φ(c, c, ..., c)
might be generated by constant propagation optimizations,
which substituted φ-function arguments by constants.
Similarly, the φ-function:
x = φ(y, y, ..., y)
can be eliminated by rewriting it to an assignment statement:
x = y
where y
is a variable.
ssa/PhiElim.java
.
Do not forget to test Tiger compiler after finishing all these optimizations.
This completes the lab. Remember to hand in your solution to the online teaching system.