Compiler Design_Unit-4
Compiler Design_Unit-4
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 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 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.
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)
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
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.
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:
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 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>
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 symbol table can be implemented in the unordered list if the compiler is used to handle the small
amount of data.
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.
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.
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.
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.
Disadvantages:
Heap-Based Allocation is slower than Stack-Based in terms of speed.
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.
2. Target program:
The target program is the output of the code generator. The output can be:
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.
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:
Consider the following source code for dot product of two vectors a and b of length 20
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.
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:
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:
2. The label of an interior node with one child is the label of its child.
(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:
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.