0% found this document useful (0 votes)
26 views

Compiler Design_Unit-4

Uploaded by

tr.anuvarshini
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
26 views

Compiler Design_Unit-4

Uploaded by

tr.anuvarshini
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 42

UNIT IV RUN-TIME ENVIRONMENT AND CODE GENERATION

Runtime Environments – source language issues – Storage organization – Storage Allocation


Strategies: Static, Stack and Heap allocation - Parameter Passing-Symbol Tables - Dynamic Storage
Allocation - Issues in the Design of a code generator – Basic Blocks and Flow graphs - Design of a
simple Code Generator - Optimal Code Generation for Expressions– Dynamic Programming Code
Generation.

Runtime Environments

A program as a source code is merely a collection of text (code, statements etc.) and to make it alive,
it requires actions to be performed on the target machine.

A program needs memory resources to execute instructions.

A program contains names for procedures, identifiers etc., that require mapping with the actual
memory location at runtime.

Runtime environment is a state of the target machine, which may include software libraries,
environment variables, etc., to provide services to the processes running in the system .

Activation Tree

When a sequence of instructions combines into several procedures, it is called a program. A


procedure mainly consists of a start, end delimiter, and everything written inside is considered the
body of the procedure. All the instructions in a procedure execute sequentially.

When a procedure executes, it is called its activation, which consists of all the required information
to call a procedure. The units that may be contained by the activation record are as follows:
When a procedure is called by another procedure, the control stack plays a vital role by storing the
activation record on it. The execution of the caller is suspended until the execution of the called
procedure gets finished, and during this duration, the activation record of the called procedure is
stored on the stack.

To understand this concept, we take a piece of code as an example:

Below is the activation tree of the code given:

SOURCE LANGUAGE ISSUES


Procedure
A procedure definition is a declaration that associates an identifier with a statement. The
identifier is procedure name, and statement is the procedure body.
For example, the following definition of procedure named readarray

When a procedure name appears with in an executable statement, the procedure is said to be
called at that point.
Activation Tree
 Each execution of procedure is referred to as an activation of the procedure. Lifetime of an
activation is the sequence of steps present in the execution of the procedure.
 If ‘a’ and ‘b’ be two procedures, then their activations will be non-overlapping (when one is
called after other) or nested (nested procedures).
 A procedure is recursive if a new activation begins before an earlier activation of the same
procedure has ended. An activation tree shows the way control enters and leaves, activations.
 Properties of activation trees are :-
 Each node represents an activation of a procedure.
 The root shows the activation of the main function.
 The node for procedure ‘x’ is the parent of node for procedure ‘y’ if and only
if the control flows from procedure x to procedure y
 First main function as root then main calls readarray and quicksort.
 Quicksort in turn calls partition and quicksort again. The flow of control in a program
corresponds to the depth first traversal of activation tree which starts at the root.

Control Stack
 Control stack or runtime stack is used to keep track of the live procedure activations
i.e the procedures whose execution have not been completed.
 A procedure name is pushed on to the stack when it is called (activation begins) and it is
popped when it returns (activation ends).
 Information needed by a single execution of a procedure is managed using an activation
record.
 When a procedure is called, an activation record is pushed into the stack and as soon as the
control returns to the caller function the activation record is popped.
 Then the contents of the control stack are related to paths to the root of the activation tree.
When node n is at the top of the control stack, the stack contains the nodes along the path
from n to the root.
 Consider the above activation tree, when quicksort(4,4) gets executed, the contents of control
stack were main() quicksort(1,10) quicksort(1,4), quicksort(4,4)

The Scope Of Declaration


A declaration is a syntactic construct that associates information with a name.
Declaration may be explicit such as
var i : integer;
or may be explicit. The portion of program to which a declaration applies is called the scope of that
declaration.
Binding Of Names
 Even if each name is declared once in a program, the same name may denote different data
object at run time. “Data objects” corresponds to a storage location that hold values.
 The term environment refers to a function that maps a name to a storage location.
 The term state refers to a function that maps a storage location to the value held there.

 When an environment associate’s storage location s with a name x, we say that x is bounds to
s. This association is referred to as a binding of x.

Storage organization
 The executing target program runs in its own logical address space in which each program
value has a location
 The management and organization of this logical address space is shared between the
compiler, operating system and target machine. The operating system maps the logical
address into physical addresses, which are usually spread through memory
Typical subdivision of run time memory:

 Code area: used to store the generated executable instructions, memory locations for the
code are determined at compile time
 Static Data Area: Is the locations of data that can be determined at compile time
 Stack Area: Used to store the data object allocated at runtime. eg. Activation records
 Heap: Used to store other dynamically allocated data objects at runtime ( for ex: malloac)

 This runtime storage can be subdivided to hold the different components of an existing
system

1. Generated executable code


2. Static data objects
3. Dynamic data objects-heap
4. Automatic data objects-stack
Activation Records
 It is LIFO structure used to hold information about each instantiation.
 Procedure calls and returns are usually managed by a run time stack called control stack.
 Each live activation has an activation record on control stack, with the root of the activation
tree at the bottom, the latter activation has its record at the top of the stack
 The contents of the activation record vary with the language being implemented.
 The diagram below shows the contents of an activation record.
 The purpose of the fields of an activation record is as follows, starting from the field for
temporaries.
1. Temporary values, such as those arising in the evaluation of expressions, are
stored in the field for temporaries.
2. The field for local data holds data that is local to an execution of a procedure.
3. The field for saved machine status holds information about the state of the
machine just before the procedure is called. This information includes the values
of the program counter and machine registers that have to be restored when
control returns from the procedure.
4. The optional access link is used to refer to nonlocal data held in other activation
records.
5. The optional control /ink paints to the activation record of the caller
6. The field for actual parameters is used by the calling procedure to supply
parameters to the called procedure.
7. The field for the returned value is used by the called procedure to return a value
to the calling procedure, again, in practice this value is often returned in a
register for greater efficiency.

STORAGE ALLOCATION STRATEGIES


The different storage allocation strategies are:
Static allocation - lays out storage for all data objects at compile time
Stack allocation - manages the run-time storage as a stack.
Heap allocation - allocates and deallocates storage as needed at run time from a data area
known as heap.
Static Allocation:-
 In static allocation, names bound to storage as the program is compiled, so there is no need
for a run-time support package.
 Since the bindings do not change at runtime, every time a procedure activated, its runtime,
names bounded to the same storage location.
 Therefore, values of local names retained across activations of a procedure. That is when
control returns to a procedure the value of the local are the same as they were when control
left the last time.
 From the type of a name, the compiler decides amount of storage for the name and decides
where the activation records go. At compile time, we can fill in the address at which the
target code can find the data it operates on.
Stack Allocation:-
 All compilers for languages that use procedures, functions or methods as units of user
functions define actions manage at least part of their runtime memory as a stack runtime
stack.
 Each time a procedure called, space for its local variables is pushed onto a stack, and when
the procedure terminates, space popped off from the stack.
Calling Sequences:-
 Procedures called implemented in what is called as calling sequence, which consists
of code that allocates an activation record on the stack and enters information into its
fields.
 A return sequence is similar to code to restore the state of a machine so the calling
procedure can continue its execution after the call.
 The code is calling sequence of often divided between the calling procedure (caller)
and a procedure is calls (callee)(callee).
 When designing calling sequences and the layout of activation record, the following
principles are helpful:
1. Value communicated between caller and callee generally placed at the caller
beginning of the callee’s activation record, so they as close as possible to the
caller’s activation record.
2. Fixed length items generally placed in the middle. Such items typically
include the control link, the access link, and the machine status field.
3. Items whose size may not be known early enough placed at the end of the
activation record.
4. We must locate the top of the stack pointer judiciously. A common approach
is to have it point to the end of fixed length fields in the activation is to have it
point to fix the end of fixed length fields in the activation record. Fixed length
data can then be accessed by fixed offsets, known to the intermediate code
generator, relative to the top of the stack pointer.

 The calling sequence and its division between caller and callee are as follows:
1. The caller evaluates the actual parameters.
2. The caller stores a return address and the old value of top_sp into the callee’s
activation record. The caller then increments the top_sp to the respective
positions.
3. The callee-saves the register values and other status information.
4. The callee initializes its local data and begins execution.
 A suitable, corresponding return sequence is:
1. The callee places the return value next to the parameters.
2. Using the information in the machine status field, the callee restores top_sp and
other registers, and then branches to the return address that the caller placed in the
status field.
3. Although top_sp has been decremented, the caller knows where the return
value is, relative to the current value of top_sp; the caller, therefore, may use that
value.
Variable length data on the stack:-
 The run-time memory-management system must deal frequently with the allocation of space
for objects the sizesof which are not known at compile time, but which are local to a
procedure and thus may be allocated on the stack.
 In modern languages, objects whose size cannot be determined at compile time are allocated
space in the heap
 However, it is also possible to allocate objects, arrays, or other structures of unknown size on
the stack.
 We avoid the expense of garbage collecting their space. Note that the stack can be used only
for an object if it is local to a procedure and becomes inaccessible when the procedure
returns.
 A common strategy for allocating variable-length arrays is shown in following figure
Heap Allocation:
 Stack allocation strategy cannot be used if either of the following is possible:
1. The values of local names must be retained when an activation ends.
2. A called activation outlives the caller.
 Heap allocation parcels out pieces of contiguous storage, as needed for activation records or
other objects.
 Pieces may be deallocated in any order, so over the time the heap will consist of alternate
areas that are free and in use.

 The record for an activation of procedure r is retained when the activation ends.
 Therefore, the record for the new activation q(1 , 9) cannot follow that for s physically.
 If the retained activation record forris deallocated, there will be free space in the heap
between the activation records for s and q.
Parameter Passing
The Parameter passing is a way of communication among the variables. The called procedure
gets the value of the variable from the calling procedure through a default mechanism. Now before
proceeding further, we need to understand some basic terminologies as well.
r-value
The value of an expression is called its r-value. The value contained in a single variable also
becomes an r-value if it appears on the right-hand side of the assignment operator. r-values can
always be assigned to some other variable.

l-value
The location of memory (address) where an expression is stored is known as the l-value of that
expression. It always appears at the left hand side of an assignment operator.

For example:
day = 1;
week = day * 7;
month = 1;
year = month * 12;
From this example, we understand that constant values like 1, 7, 12, and variables like day, week,
month and year, all have r-values. Only variables have l-values as they also represent the memory
location assigned to them.
For example:
7 = x + y;
is an l-value error, as the constant 7 does not represent any memory location.

There are two types of parameters:


1. Formal parameters
2. Actual parameters
Formal Parameter:
The formal parameters are the variables declared in the called function’s definition and take
the information passed by the calling process.

Actual Parameters:
The variables that are mentioned in the function are called arguments, and whose address or
values are passed to the called process are known as actual parameters.

Example:

Formal parameters hold the information of the actual parameter, depending upon the parameter
passing technique used. It may be a value or an address.
The most common methods are:
1. Call by value:
 This is the simplest method of parameter passing.
 The actual parameters are evaluated and their r-values are passed to called procedure.
 The operations on formal parameters do not changes the values of actual parameter.

2. Call by reference:
 This method is also called as call by address or call by location.
 The L-value, the address of actual parameter is passed to the called routines activation
record.
 The value of actual parameters can be changed.
 The actual parameter should have an L-value.
Output:

Explanation

As we can see, instead of int x, int y, we used int *x, int y, and instead of giving x,y, we
gave &x,&y in the function call. Due to the usage of pointers as function parameters,
which return the original parameters' address rather than their value, this practice is
called by reference.

The variables' addresses are given using the & operator, and the memory location to
which the pointer is pointing is accessed using the * operator. The modifications done in
the swap() reflect in main(), as shown in the output above because the variable function
points to the same memory address as the original arguments.
3. Copy restore:
 This method is a hybrid between call by value and call by reference.
 This is also known as copy-in-copy-out or values result.
 The calling procedure calculates the value of actual parameters and it is then copied to
activation record for the called procedure.
 During execution of called procedure, the actual parameters value is not affected.
 If the actual parameters have L-value then at return the value of formal parameter is
copied to actual parameter.
Output:
The value of a is : 5

Explanation

Value is passed into the function in the example above, but until the function is complete, it
has no impact on the value of the passed-in variable. At that time, the function variable's final
value is saved in the passed-in variable.
4. Call by name:

1. This is less popular method of parameter passing.


2. Procedure is treated like macro.
3. The procedure body is substituted for call in caller with actual parameters substituted for
formals.
4. The actual parameters can be surrounded by parenthesis to preserve their integrity.
5. The locals name of called procedure and names of calling procedure are distinct.
Output:

Explanation

Here, the evaluation is done based on parameters. In all cases where formal parameters are used in
the technique, actual parameters are used instead of formal ones.

Symbol Tables

Symbol table is an important data structure used in a compiler.

Symbol table is used to store the information about the occurrence of various entities such as objects,
classes, variable name, interface, function name etc. it is used by both the analysis and synthesis
phases.
The symbol table used for following purposes:
 It is used to store the name of all entities in a structured form at one place.
 It is used to verify if a variable has been declared.
 It is used to determine the scope of a name.
 It is used to implement type checking by verifying assignments and expressions in the source
code are semantically correct.
A symbol table is simply a table which can be either linear or a hash table. It maintains an
entry for each name in the following format:
<symbol name, type, attribute>
For example, if a symbol table has to store information about the following variable
declaration:
static int interest;
then it should store the entry such as:
<interest, int, static>

Operations of Symbol Table

Information used by the compiler from Symbol table or Applications of Symbol


Table:
Symbol Identification: The symbol table enables the compiler to apprehend and keep track of
variables, functions, and different symbols in your application.

Data Type Information: It stores data about the data types of variables, like whether they're
numbers, text, or something else.

Variable Scope: It is information wherein variables are declared and where they may be used within
your code.

Function Details: For functions, it stores their names, parameters, and what they do.
Error Detection: The symbol desk aids in detecting mistakes for your code by way of making sure
that variables and functions are used correctly and consistently.

Code Generation: It assists in generating machine code or executable programs primarily based on
the statistics saved, making your code run effectively.

Operations
The symbol table provides the following operations:

Insert ()
 Insert () operation is more frequently used in the analysis phase when the tokens are
identified and names are stored in the table.
 The insert() operation is used to insert the information in the symbol table like the unique
name occurring in the source code.
 In the source code, the attribute for a symbol is the information associated with that symbol.
The information contains the state, value, type and scope about the symbol.
 The insert () function takes the symbol and its value in the form of argument.

For example:
int x; Should be processed by the compiler as:
insert (x, int)

lookup():-In the symbol table, lookup() operation is used to search a name. It is used to determine:

 The existence of symbol in the table.


 The declaration of the symbol before it is used.
 Check whether the name is used in the scope.
 Initialization of the symbol.
 Checking whether the name is declared multiple times.
The basic format of lookup() function is as follows:

lookup (symbol) This format is varies according to the programming language.

Set attribute Operation:-Set_attribute Operation can be performed on a symbol table to associate


an attribute with a given entry.
Get_attribute Operation:-Get_attribute Operation can be performed on a symbol table to get an
attribute associated with a given entry.

Symbol Table Implementation:-

The symbol table can be implemented in the unordered list if the compiler is used to handle the small
amount of data.

A basic example of creating the symbol table of C++ Code


A symbol table can be implemented in one of the following techniques:

 Linear (sorted or unsorted) list


 Hash table
 Binary search tree
Symbol table are mostly implemented as hash table.
1. Linear list or lists
 It is a simplest and easiest to implement data structure.
 We use a single array to store names and their associated information.
 New names are added to the list in the order in which they are encountered.
 To insert a new name, we must scan down the list to make sure that it is not already
there. If not then add it otherwise an error message i.e. multiply declared name.
 When the name is located, the associated information can be found in words following
next.
 To retrieve information about a name, we search from the beginning of the array up to
the position marked by AVAILABLE pointer, which indicates the beginning of the
empty portion of array.
 If we reach AVAILABLE pointer without finding NAME, we have a fault-the use of
an undefined name.
 To find data about the NAME, we shall on the average search N/2 names. So the
cost of an enquiry is proportional to N.
 One advantage of list organization is that the minimum possible space is taken in
simple compiler.

2. Trees
 It is a more efficient approach to symbol table organization. Here we add two link fields
LEFT and RIGHT to each record.
 Following algorithm is used to look for NAME in a binary search tree where p is initially a
pointer to the root.

3. Hash table
Hashing table technique is suitable for searching and hence it is implemented in compiler.
 Here, basic hashing schema is shown in above figure(3). Two tables: hash table and a storage
table are used.
 The hash table consists of k words numbered 0,1,2,…,k-1.These words are pointers into the
storage enable to the heads of k separate linked lists I(some lists may be empty). Each record
in the symbol table appears on one of these lists.
 To determine whether NAME is in the symbol table, we apply NAME as a hash function h
such that h(NAME) is an integer between 0 to k-1.
 Hash function
The mapping between an item and the slot where that item belongs in the hash table is called
the hash function.
 Hash function will take an item in the collection and return an integer in the range of slot
names between 0 to m-1.
Representing Scope Information
A compiler maintains two types of symbol tables: a global symbol table which can be accessed by all
the procedures and scope symbol tables that are created for each scope in the program.

To determine the scope of a name, symbol tables are arranged in hierarchical structure as shown in
the example below:
The above program can be represented in a hierarchical structure of symbol tables:
The global symbol table contains names for one global variable (int value) and two procedure names,
which should be available to all the child nodes shown above. The names mentioned in the pro_one
symbol table (and all its child tables) are not available for pro_two symbols and its child tables.

This symbol table data structure hierarchy is stored in the semantic analyzer and whenever a name
needs to be searched in a symbol table, it is searched using the following algorithm:
 first a symbol will be searched in the current scope, i.e. current symbol table.
 if a name is found, then search is completed, else it will be searched in the parent symbol
table until,
 either the name is found or global symbol table has been searched for the name.

Dynamic Allocation (Stack Allocation)


Dynamic Allocation is a type of Storage Strategies in Compiler Design where the memory is
allocated to the data structures at run-time, not at compile-time. A data structure can be allocated in
memory depending on the program’s need and can be allocated and deallocated at the program’s
execution.

In Dynamic Allocation, we have further two allocation strategies, are Stack-Based Allocation and
Heap-Based Allocation, explained below:
Stack-Based Allocation
In Stack-Based Allocation, A fixed-sized stack data structure is allocated to a memory location
during the run-time execution. A variable is assigned a memory location on the stack when they are
declared and can be freed when the variable’s scope goes out.

C and C++ both support the stack-based allocation through the variables declared in the functions, or
Standard Library functions can be used, such as “alloca()” and “alloca_array()”.
Advantages:
The process of assigning the memory locations is fast.

Memory Deallocation is also fast in stack-based allocation.

Function calls can be efficient using stack-based allocation.

Disadvantages:
The amount of memory is limited for stack-based allocation.

As the size of the stack is limited, issues like stack overflow can happen.

Example for stack-based allocation:

Heap-Based Allocation

In Heap-Based Allocation, Variables that are created and memory will be dynamically allocated in
memory (heap) at run-time execution. The variables that use heap-based allocation are generally
known as dynamic variables that can be changed anytime during the run-time.

C and C++ both support heap-based allocation, and dynamic data structures can be created using
functions like. “malloc()”, “calloc()”, “realloc()”, or “new” keywords.

Java also supports heap-based allocation through the use of new keywords.

Advantages:
Dynamic Data Structures can be handled by heap-based allocation.

Heap-Based Allocation gives more flexibility to a data structure.

It can also handle large amounts of memory as compared to stack-based allocation.

Disadvantages:
Heap-Based Allocation is slower than Stack-Based in terms of speed.

Risk of memory leaks.

Example for Heap-based allocation:


Difference between Static and Dynamic Memory Allocation

Explicit Allocation of Fixed-Sized Blocks


 The simplest form of dynamic allocation involves blocks of a fixed size.
 Allocation and deallocation can be done quickly with little or no storage overhead.
 Suppose that blocks are to be drawn from a contiguous area of storage. Initialization of the
area is done by using a portion of each block for a link to the next block.
 A pointer available points to the first block. Allocation consists of taking a block off the list
and deallocation consists of putting the block back on the list.

Explicit Allocation of Variable-Sized Blocks


 When blocks are allocated and deallocated, storage can become fragmented; that is,
the heap may consist of alternate blocks that are free.
 The situation can occur if a program allocates five blocks and then de- allocates the
second and fourth.
 Fragmentation is of no consequence if blocks are of fixed size, but if they are of
variable size, because we could not allocate a block larger than any one of the free
blocks, even though the space is available.
 First fit, worst fit and best fit are some methods for allocating variable-sized blocks.

Implicit Deallocation
 Implicit deallocation requires cooperation between the user program and the run-time
package, because the latter needs to know when a storage block is no longer in use.
 This cooperation is implemented by fixing the format of storage blocks.

Reference counts:
 We keep track of the number of blocks that point directly to the present block. If this count
ever drops to 0, then the block can be deallocated because it cannot be referred to.
 In other words, the block has become garbage that can be collected. Maintaining reference
counts can be costly in time.
Marking techniques:
 An alternative approach is to suspend temporarily execution of the user program and use the
frozen pointers to determine which blocks are in use.

Code Generator:- Code generator is used to produce the target code for three-address statements.
It uses registers to store the operands of the three address statement.

Issues in the Design of a code generator


In the code generation phase, various issues can arises:

1. Input to the code generator


2. Target program
3. Memory management
4. Instruction selection
5. Register allocation
6. Evaluation order

1. Input to the code generator


The input to the code generator contains the intermediate representation of the source program
and the information of the symbol table. The source program is produced by the front end.
Intermediate representation has the several choices:
a) Postfix notation
b) Syntax tree
c) Three address code
We assume front end produces low-level intermediate representation i.e. values of names in it
can directly manipulated by the machine instructions. The code generation phase needs complete
error-free intermediate code as an input requires.

2. Target program:

The target program is the output of the code generator. The output can be:

a) Assembly language: It allows subprogram to be separately compiled.

b) Relocatable machine language: It makes the process of code generation easier.


c) Absolute machine language: It can be placed in a fixed location in memory and can be executed
immediately.

3. Memory management

o During code generation process the symbol table entries have to be mapped to actual p
addresses and levels have to be mapped to instruction address.
o Mapping name in the source program to address of data is co-operating done by the front end
and code generator.
o Local variables are stack allocation in the activation record while global variables are in static
area.

4. Instruction selection:

o Nature of instruction set of the target machine should be complete and uniform.
o When you consider the efficiency of target machine then the instruction speed and machine
idioms are important factors.
o The quality of the generated code can be determined by its speed and size.

Example:

5. Register allocation
Register can be accessed faster than memory. The instructions involving operands in register are
shorter and faster than those involving in memory operand.

The following sub problems arise when we use registers:

Register allocation: In register allocation, we select the set of variables that will reside in register.

Register assignment: In Register assignment, we pick the register that contains variable.
Certain machine requires even-odd pairs of registers for some operands and result.

For example:
Consider the following division instruction of the form:
D x, y
Where,
x is the dividend even register in even/odd register pair

y is the divisor
Even register is used to hold the remainder.
Odd register is used to hold the quotient.

6. Evaluation order
The efficiency of the target code can be affected by the order in which the computations are
performed. Some computation orders need fewer registers to hold results of intermediate than others.
 Picking the best order is a difficult task.
 Initially avoid this problem by generating code for the three address statements in the order in
which they have been produced by the intermediate code generator.
 It creates schedules for instructions to execute them.
When instructions are independent, their evaluation order can be changed to utilize registers and save
on instruction cost. Consider the following instruction:
a+b-(c+d)*e
The three-address code, the corresponding code and its reordered instruction are given below:

Basic Blocks and Flow graphs


Basic Block Construction:

Consider the following source code for dot product of two vectors a and b of length 20

The three-address code for the above program is given as:


Code sequence for the example is:
Register Allocation and Assignment:
Register allocation is only within a basic block. It follows top-down approach.
Local register allocation
– Register allocation is only within a basic block. It follows top-down approach.
– Assign registers to the most heavily used variables
– Traverse the block
– Count uses
– Use count as a priority function
– Assign registers to higher priority variables first
Need of global register allocation:
 Local allocation does not take into account that some instructions (e.g. those in loops) execute
more frequently. It forces us to store/load at basic block endpoints since each block has no
knowledge of the context of others.
 To find out the live range(s) of each variable and the area(s) where the variable is
used/defined global allocation is needed. Cost of spilling will depend on frequencies and
locations of uses.
Register allocation depends on:
– Size of live range
– Number of uses/definitions
– Frequency of execution
– Number of loads/stores needed.
– Cost of loads/stores needed.
Usage Counts:
 A simple method of determining the savings to be realized by keeping variable x in a register
for the duration of loop L is to recognize that in our machine model we save one unit of cost
for each reference to x if x is in a register.
 An approximate formula for the benefit to be realized from allocating a register to x within a
loop L is:

where,
-use(x, B) is the number of times x is used in B prior to any definition of x;
-live(x,B) is 1 if x is live on exit from B and is assigned a value in B and 0 otherwise.

Example: Consider the basic block in the inner loop in Fig 5.3 where jump and conditional
jumps have been omitted. Assume R0, R1 and R2 are allocated to hold values throughout the
loop. Variables live on entry into and on exit from each block are shown in the figure.

Figure 1: Flow graph of an inner loop


Figure 2 Shows the assembly code generated from Figure 1

Figure 2: Code Sequence using global register assignment


Design of a simple Code Generator
A code generator is expected to have an understanding of the target machine’s runtime environment
and its instruction set. The code generator should take the following things into consideration to
generate the code:

Target language: The code generator has to be aware of the nature of the target language for
which the code is to be transformed. That language may facilitate some machine-specific
instructions to help the compiler generate the code in a more convenient way. The target machine
can have either CISC or RISC processor architecture.
 IR Type: Intermediate representation has various forms. It can be in Abstract Syntax Tree
(AST) structure, Reverse Polish Notation, or 3-address code.
 Selection of instruction: The code generator takes Intermediate Representation as input and
converts (maps) it into target machine’s instruction set. One representation can have many ways
(instructions) to convert it, so it becomes the responsibility of the code generator to choose the
appropriate instructions wisely.
 Register allocation: A program has a number of values to be maintained during the
execution. The target machine’s architecture may not allow all of the values to be kept in the CPU
memory or registers. Code generator decides what values to keep in the registers. Also, it decides
the registers to be used to keep these values.
 Ordering of instructions: At last, the code generator decides the order in which the
instruction will be executed. It creates schedules for instructions to execute them.

Descriptors

The code generator has to track both the registers (for availability) and addresses (location of values)
while generating the code. For both of them, the following two descriptors are used:

 Register descriptor : Register descriptor is used to inform the code generator about the
availability of registers. Register descriptor keeps track of values stored in each register.
Whenever a new register is required during code generation, this descriptor is consulted for
register availability.
 Address descriptor : Values of the names (identifiers) used in the program might be stored
at different locations while in execution. Address descriptors are used to keep track of memory
locations where the values of identifiers are stored. These locations may include CPU registers,
heaps, stacks, memory or a combination of the mentioned locations. Code generator keeps both
the descriptor updated in real-time. For a load statement, LD R1, x, the code generator:

 updates the Register Descriptor R1 that has value of x and


 updates the Address Descriptor (x) to show that one instance of x is in R1.
Code Generation
 Basic blocks comprise of a sequence of three-address instructions. Code generator takes these
sequence of instructions as input.

 Note : If the value of a name is found at more than one place (register, cache, or memory), the
register’s value will be preferred over the cache and main memory. Likewise cache’s value
will be preferred over the main memory. Main memory is barely given any preference.

 getReg : Code generator uses getReg function to determine the status of available registers
and the location of name values. getReg works as follows:

 If variable Y is already in register R, it uses that register. Else if some register R is available,
it uses that register.
 Else if both the above options are not possible, it chooses a register that requires minimal
number of load and store instructions.
 For an instruction x = y OP z, the code generator may perform the following actions. Let us
assume that L is the location (preferably register) where the output of y OP z is to be saved:

 Call function getReg, to decide the location of L.


 Determine the present location (register or memory) of y by consulting the Address
Descriptor of y. If y is not presently in register L, then generate the following instruction to
copy the value of y to L:MOV y’, Lwhere y’ represents the copied value of y.
 Determine the present location of z using the same method used in step 2 for y and generate
the following instruction:OP z’, Lwhere z’ represents the copied value of z.
 Now L contains the value of y OP z, that is intended to be assigned to x. So, if L is a register,
update its descriptor to indicate that it contains the value of x. Update the descriptor of x to
indicate that it is stored at location L.
 If y and z has no further use, they can be given back to the system.

Optimal Code Generation for Expressions

Optimal code generation


– For expressions
– By dynamic programming
1. Ershov Numbers
We begin by assigning to the nodes of an expression tree a number that tells how many registers are
needed to evaluate that node without storing any tem-poraries. These numbers are sometimes
called Ershov numbers, after A. Ershov, who used a similar scheme for machines with a single
arithmetic register. For our machine model, the rules are:
1. Label any leaf 1.

2. The label of an interior node with one child is the label of its child.

3. The label of an interior node with two children is

(a) The larger of the labels of its children, if those labels are different.

(b) One plus the label of its children if the labels are the same.
We can choose registers registers optimally
 If a basic block consists of a single expression, or
 It is sufficient sufficient to generate generate code for a block one expression at a time
Understanding Ershov Numbers
 Ershov variables tell us the minimum number of registers required to evaluate an expression
without requiring extra load/store operations
 The key rule with Ershov numbers happens with binary operators
 If left child requires n registers and right child requires m >= n registers
• Compute right child first, using m registers and store its value
• Computer left child using n registers and store its value - – requires n + 1 registers because
of stored value
• Combine two results and store in 1 register - – Total number of registers required in max(m, n+1)
 Equal to m if m != n
 Otherwise equal to m+1 = n+1

Ershov Number Example - Compute the Ershov numbers for the following

Register Shortages
 If the root's Ershov number k is greater than the number of registers r, then we
 need a different strategy
1.Recursively generate code for the child with larger Ershov number
2.Store the result in memory
3.Recursively generate code for the smaller child
4.Load the stored result from Step 2
5.Generate code for the root
 It is possible to prove that this does the minimum number of possible load/store operations
Ershov Number Example
Generate code on a 2-register machine for the following:

Code Generation by Dynamic Programming


Dynamic Programming

Dynamic Programming Algorithm

Dynamic Programming Example


2. Generating Code from Labeled Expression Trees
It can be proved that, in our machine model, where all operands must be in registers, and registers
can be used by both an operand and the result of an operation, the label of a node is the fewest
registers with which the expression can be evaluated using no stores of temporary results. Since in
this model, we are forced to load each operand, and we are forced to compute the result cor-
responding to each interior node, the only thing that can make the generated code inferior to the
optimal code is if there are unnecessary stores of tempo-raries. The argument for this claim is
embedded in the following algorithm for generating code with no stores of temporaries, using a
number of registers equal to the label of the root.
Dynamic Programming Code-Generation

A dynamic programming algorithm is used to extend the class of machines for which optimal code
can be generated from expressions trees in linear time. This algorithm works for a broad class of
register machines with complex instruction sets.

You might also like