- The final phase in our compiler model is the code generator.
- It takes as input the intermediate representation (IR) produced by the front end of the compiler, along with relevant symbol table information, and produces as output a semantically equivalent target program
- Compilers that need to produce efficient target programs, include an optimization phase prior to code generation. The optimizer maps the IR into IR from which more efficient code can be generated. In general, the code optimization and code-generation phases of a compiler, often referred to as the back end , may make multiple passes over the IR before generating the target program.
A code generator has three primary tasks:
- Instruction selection involves choosing appropriate target-machine instructions to implement the IR statements.
- Register allocation and assignment involves deciding what values to keep in which registers.
- Instruction ordering involves deciding in what order to schedule the execution of instructions.
Issues in the Design of a Code Generator
The most important criterion for a code generator is that it produce correct code. Correctness takes on special significance because of the number of special cases that a code generator might face. Given the premium on correctness, designing a code generator so it can be easily implemented, tested, and maintained is an important design goal.
1. Input to the Code Generator
- The input to the code generator is the intermediate representation of the source program produced by the front end, along with information in the symbol table that is used to determine the run-time addresses of the data objects denoted by the names in the IR.
- The many choices for the IR include three-address representations such as quadruples, triples, indirect triples; virtual machine representations such as byte-codes and stack-machine code; linear representations such as post x notation; and graphical representations such as syntax trees and DAG's.
- We assume that the front end has scanned, parsed, and translated the source program into a relatively low-level IR, so that the values of the names appearing in the IR can be represented by quantities that the target machine can directly manipulate, such as integers and floating-point numbers.
- We also assume that all syntactic and static semantic errors have been detected, that the necessary type checking has taken place, and that type- conversion operators have been inserted wherever necessary. The code generator can therefore proceed on the assumption that its input is free of these kinds of errors.
2. The Target Program
- The instruction-set architecture of the target machine has a significant impact on the diffculty of constructing a good code generator that produces high-quality machine code. The most common target-machine architectures are RISC (reduced instruction set computer), CISC (complex instruction set computer), and stack based.
- A RISC machine typically has many registers, three-address instructions, simple addressing modes, and a relatively simple instruction-set architecture.
- In contrast, a CISC machine typically has few registers, two-address instructions, a variety of addressing modes, several register classes, variable-length instructions, and instructions with side effects.
- In a stack-based machine, operations are done by pushing operands onto a stack and then performing the operations on the operands at the top of the stack. To achieve high performance the top of the stack is typically kept in registers.
The target program is the output of the code generator.
- Producing an absolute machine-language program as output has the advantage that it can be placed in a fixed location in memory and immediately executed. Programs can be compiled and executed quickly.
- Producing a relocatable machine-language program (often called an object module ) as output allows subprograms to be compiled separately. A set of relocatable object modules can be linked together and loaded for execution by a linking loader.
- Producing an assembly-language program as output makes the process of code generation somewhat easier. We can generate symbolic instructions and use the macro facilities of the assembler to help generate code. The price paid is the assembly step after code generation.
3. Instruction Selection
- The code generator must map the IR program into a code sequence that can be executed by the target machine.
- The complexity of performing this mapping is determined by factors such as the level of the IR the nature of the instruction-set architecture the desired quality of the generated code.
- If the IR is high level, the code generator may translate each IR statement into a sequence of machine instructions using code templates. Such statement- by-statement code generation, however, often produces poor code that needs further optimization.
- If the IR reflects some of the low-level details of the underlying machine, then the code generator can use this information to generate more ecient code sequences.
- The nature of the instruction set of the target machine has a strong effect on the diculty of instruction selection. For example, the uniformity and completeness of the instruction set are important factors.
- Instruction speeds and machine idioms are other important factors.
- If we do not care about the efficiency of the target program, instruction selection is straightforward. For each type of three-address statement, we can design a code skeleton that defnes the target code to be generated for that construct.
For example, every three-address statement of the form x = y + z , where x , y , and z are statically allocated, can be translated into the code sequence
Here, the fourth statement is redundant since it loads a value that has just been stored, and so is the third if a is not subsequently used. The quality of the generated code is usually determined by its speed and
size.4. Register Allocation
- A key problem in code generation is deciding what values to hold in what registers.
- Registers are the fastest computational unit on the target machine, but we usually do not have enough of them to hold all values. Values not held in registers need to reside in memory.
The use of registers is often subdivided into two sub problems:
1. Register allocation , during which we select the set of variables that will reside in registers at each point in the program.
2. Register assignment , during which we pick the specific register that a variable will reside in.
Finding an optimal assignment of registers to variables is diffcult, even with single-register machines. Mathematically, the problem is NP-complete. The problem is further complicated because the hardware and/or the operating system of the target machine may require that certain register-usage conventions be observed.
Example: Certain machines require register-pairs (an even and next odd- numbered register) for some operands and results. For example, on some machines, integer multiplication and integer division involve register pairs.
- The multiplication instruction is of the form M x, y
where x , the multiplicand, is the odd register of an even/odd register pair and y , the multiplier, can be anywhere. The product occupies the entire even/odd register pair.
- The division instruction is of the form D x, y
where the dividend occupies an even/odd register pair whose even register is x ; the divisor is y . After division, the even register holds the remainder and the odd register the quotient.
5. Evaluation Order
- The order in which computations are performed can a effect the effciency of the target code.
- Some computation orders require fewer registers to hold intermediate results than others.
- However, picking a best order in the general case is a dicult NP-complete problem.