Lecture 16 - 480/580 (compiler construction) Algebraic simplification There are three topics we need to deal with for the remainder of the course: * intermediate form generation * optimization * code generation The problem is that these three are intimately tied together; and it is difficult to discuss one without discussing the others. Optimization, in particular, can be viewed at many levels. One popular compiler system (the Amsterdam Compiler Kit, or ACK) performs the following eight steps (ref: CACM 26, 9, Sept 1983): preprocessing front end (parsing) peephole optimizer global optimizer back end (code generation) target optimizer assembler and linker Of these steps, steps one and two covers everything we have done in this course up to now. Three of these steps are optimization, two of which are based on a machine-independent internal form, and one of which is specific to the particular machine the compiler is working on. The plan I have in mind is to continue a more or less chronological tour through a compiler; that is, dicusss topics in the order that they would occur in a real compiler. However, since from now on these three topics would be all mixed up, even in a real compiler, that means that from now on in the course the topics will be mixed up as well. Today I want to talk about algebraic simplification of abstract syntax trees. I do this because (1) it is your next programming assignment, so I might as well get you thinking about it now, and (2) since it is an optimization that is performed on the internal representation (AST's); it would probably be done very early in a real compiler; unlike other optimizations which are specific to machine instructions and thus necessarily come later. In your assignment I give you several different algebraic transformations that could be made to trees. We can think of these as patterns to be applied to trees to give you other trees. Let us take an example. Suppose we have a declaration something like the following: var a : array [ 2 ... 40 ] of record c: integer; b: integer; end; Let us consider the tree that would be generated for something like: a [ 4 + j].b Assume a begins at 8(r6), and let j be a global variable. [ write tree ] How might we optimize this ? rewrite 4 + j as j + 4 rewrite -2 as + (-2) replace j + 4 + -2 with j + 2 replace (j + 2) * 8 with j * 8 + 16 replace j * 8 with j << 3 linearize move shift down, 8 up replace 8 + 16 with 25 replace 24 + 4 with 28 Resulting expression (r6 + (@j << 3)) + 28 This expression is considerably simpler than the original; and will generate many fewer instructions. This problem shows optimization by pattern matching (we are looking for patterns in the original tree, replacing it by other expression). Pattern matching is important in compiler construction; in a way parsing is simply just a matter of pattern matching. We will see the technique of pattern matching occur at least once more before we are done. Before we leave this technique, let me mention some other patterns that often occur and that a compiler might want to look for: (convertoreal integerconstant) => realconstant negate (constant * tree) => ((-constant) * tree) (same with division) t ** 2 => t * t 2 * t => t + t t / 2 => t * 0.5 Some optimizations might seem locally good, but might not be so good when viewed from a more global perspective. For example, suppose we have: if (a-b < 0) then We might want to replace (a-b < 0) with (a>b), which would seem to do the same thing. But if the preceeding statement had been: x := y * (a-b); if (a-b < 0) we might already have the computation of a-b lying around (say in a register) and thus the original might be the better form. Here is another common pattern: adr := adr + constant there is no reason to compute the address twice; nor, on many machines, to even compute the result in registers, we might want to internally represent this as: (adr address constant) Many machines make a special case out of 1, giving us: (incr address) There are literally dozens of other patterns that might be useful or important in a real system.