Register allocation |
|||||
English only |
|||||
The aim of this assignment is to add register spilling and optimise register usage by rearranging expressions in the eins compiler. We propose to do so by implementing the Sethi-Ullman register allocator, but motivated students can choose another technique. The Sethi-Ullman allocator is described at page 241 of Appel's book, but our view of this algorithm is described in detail below, so you do not really need the book. We propose to split the implementation of this algorithm in three stages:
As a starting point, we give you a working implementation of the eins compiler that you had to implement in Prof. Odersky's compilation course. This compiler should be similar to yours. The only notable difference is the use of Java 1.5 collection classes. This helps reduce the number of casts in the parser and code generation bits. However, if you still want to use Java 1.4 (for example, no Java 1.5 is available for your platform) you can download a 1.4-friendly version of the compiler here. You can use either one of them for this assignment. The three stages of the project are described individually below. We suggest that you do them in the order given here. For every stage, we also give a few implementation hints which should hopefully help you, but feel free to ignore them if you have better ideas. The aim of the first stage is to write code to compute the register need of every node in the tree. This need is stored directly in the tree as an integer, like the other attributes (position and type). The register need of a subtree is defined as the number of registers which are needed to compile it. Of course, this need depends on the code generator that is being used, as different implementation techniques can lead to different needs for identical nodes. Therefore, the code which computes the need must precisely follow the code generator. As we will see below, object-oriented techniques can make this relatively easy.
A simple idea to compute the need of the various nodes of a tree
is to run the code generator on the tree while pretending that
infinitely many registers are available, and track register
allocation and deallocation. For example, if during the
compilation of some tree the method
For your implementation, we suggest that you pursue this idea,
by writing a subclass of
The fake code generator also has to use a different version of
class Register spilling is the action of temporarily moving the contents of registers on the stack, in order to increase the number of available registers. This is done by compilers in order to avoid running out of registers during code generation. The Sethi-Ullman approach to spilling is quite simple: when generating code for a node n, we look at its register need. If that need is greater than the number of registers currently available K, we make sure that the situation is not worse for the children of n. That is, we ensure that all children of n get compiled with K available registers too. One can get an intuition of why this works by making the following observations:
To implement spilling, you have two options: either follow the above algorithm faithfully, or try to find an equivalent solution which can easily be introduced in the eins compiler. Following the algorithm faithfully means identifying all the locations in the code where a register is used to hold the value of a left sub-tree while compiling a right sub-tree. In these locations, spilling code should be added to store the contents of this register if the need of the right sub-tree cannot be fulfilled with the currently available registers. There is however an easier solution: as we have seen above, our implementation makes it easy to execute a piece of code just before (and/or after) compiling a node. We can use this trick to implement a slightly modified version of the Sethi-Ullman algorithm, as follows:
The aim of the last stage is to try to lower the register usage of binary operations by rearranging them. The idea is very simple: when generating the code for a binary operation, start with the sub-tree which needs the greatest number of registers. This minimises the need of the whole operation because during the computation of the second operand, at least one register is needed to hold the value of the first operand.
This stage is best implemented by writing a visitor which looks
at all binary operations and reorder the children if the right
one has a greater need in registers than the left one. To
perform this reordering, a simple idea is to add a boolean to
the node When the children are not swapped, code generation works as usual. When they are, the code has to be generated so that the left subtree plays the role of the right operand, and vice versa. In most of the cases, this is trivially done. |
|