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

Compiler Note Book

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

Compiler Note Book

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

COMPILER DESIGN

TYPE CHECKING

Surajit Das 4/27/20


Type System:
A type system is a collection of rules that assign types to program constructs (more
constraints added to checking the validity of the programs, violation of such constraints
indicates errors). In other words, a type system is a tractable syntactic framework to
categorize different phrases according to their behaviours and the kind of values they
compute. It uses logical rules to understand the behaviour of a program and associates
types with each compound value and then it rises to prove that no type errors can occur
by analysing the flow of these values. Type systems provide a concise formalization of
the semantic checking rules. Type rules are language specific. To do type checking a
compiler needs to assign a type expression to each component of the source program.
The compiler must then determine that these type expressions conform to a collection
of logical rules that is called the type system for the source language.
For example, the floating-point numbers in C uses floating-point specific operations to
be performed over these numbers such as floating-point addition, subtraction,
multiplication etc.

The language design principle ensures that every expression must have a type that is
known (at run time) and a type system has a set of rules for associating a type to an
expression. Type system allows one to determine whether the operators in an
expression are appropriately used or not.

There are two type systems-


1. Basic Type system: It contains atomic types and has no internal structure. For
example, integer, character, etc.
2. Constructed Type system: It contains arrays, records, sets, and structure types
constructed from basic types and/or from other constructed types.

Type system provides some functions that include:


• Safety: A type system allows a compiler to detect meaningless or invalid code
which dose not make a sense; by doing this it offers more strong typing safety.
A sound type system eliminates the need for dynamic checking for type errors,
because it allows us to determine statically that these errors cannot occur when
the target program runs.
For example, an expression 5/“Hi” is treated as invalid because
arithmetic rules do not specify how to divide an integer by a string.

Compiled by: Surajit Das Page 1|6


• Optimization: For optimization, a type system can use static or dynamic type
checking, where static type checking provides useful compile-time information
and dynamic type-checking verifies and enforces the constraints at runtime.
• Abstraction: Types can help programmers to consider programs as a higher
level of representation than bit or byte by hiding lower level implementation.

Type Checking
Operations in a language may require operands belonging to a specific data type. Some
examples are- indexes of array references should always be integers, conditional
expressions in control-flow statements should be Boolean, pointer values cannot be real
values, and so on.
Therefore, compilers can perform type checking based on the operations involved in the
source code. Some languages even do not permit mixed mode expressions. In such
cases, expressions with operands of different data types are to be considered as semantic
errors. In some other languages, if there are expressions with operands of different but
compatible data types, one of the operands will be converted to the data type of the other
operand by the compiler. This type of conversion is known as coercion. For example,
in C language, when an addition operation involves integer and double values, the
integer value is converted to double. However, if the values are of incompatible types
such as array and integer data types, then conversion is not possible. Therefore, the
compiler has to identify whether coercion is possible. If not, it results in semantic error.

Type Checking is the process of verifying the type correctness of the input
program by using logical rules to check the behaviour of a program either at
compile time or at run time. It allows the programmers to limit the types that can
be used for semantic aspects of compilation.

Though errors can be checked dynamically (at runtime) if the target program contains
both the type of an element and its value, but a sound type system eliminates the need
for dynamic checking for type errors by ensuring that these errors would not arise when
the target program runs.

Rules for Type Checking:


Type checking can take two forms:
1. Type synthesis
2. Type inference

Compiled by: Surajit Das Page 2|6


Type Synthesis:
Type synthesis is used to build the type of an expression from the types of its sub-
expressions. For type synthesis, the names must be declared before they are used. For
example, the type of expression E1 + E2 depends on the types of its sub-expressions E1
and E2. A typical rule is used to perform type synthesis and has the following form:

if expression “f” has a type s→t and expression “x” have a type s,
then expression f(x) will be of type t

Here, s → t represents a function from s to t. This rule can be applied to all functions
with one or more arguments. This rules consider the expressions E1 + E2 as a function,
add (E1, E2 ) and uses E1 and E2 to build the type of E1 + E2 .

Type Inference:
Type inference is the analysis of a program to determine the types of some or all of the
expressions from the way they are used. For example,
public int add (int E1, int E2)
Return E1 + E2
Here, E1 and E2 are defined as integers. So, by type inference, we just need definition of
E1 and E2. Since the resulting expression E1 + E2 uses “+” operation, which would be
taken as integer because it is performed on two integers E1 and E2. Therefore, the return
type of add must be an integer. A typical rule is used to perform type inference and has
the following form:

if f(x) is an expression,
then for some type variables α and β, f is of type α → β and x is of type α

Different types of Type Checking:


Static Type Checking:
A language is statically-typed if the type of a variable is known at compile time instead
of at runtime. Common examples of statically-typed languages include Ada, C, C++,
C#, Java, Fortran, Pascal, and Scala.
The benefits of static type checking are that
▪ It allows many type errors to be caught early in the development cycle that means
as the compiler uses type declarations and determines all types at compile time,
hence catches most of the common errors at compile time.

Compiled by: Surajit Das Page 3|6


▪ Static typing usually results in compiled code that executes more quickly because
when the compiler knows the exact data types that are in use, it can produce
optimized machine code (i.e. faster and/or using less memory) and does not
require any type checking during execution.
The main disadvantage of this method is that it does not provide flexibility to perform
type conversions at runtime. Moreover, static type checking is conservative, i.e. it will
reject some programs that may behave properly at runtime, but they cannot be statically
determined to be well-typed.
Dynamic Type checking:
Dynamic type checking is the process of verifying the type safety of a program at
runtime that means it is performed during the execution of the program and it checks
the type at runtime before the operations are performed on data. Common dynamically-
typed languages include Groovy, JavaScript, Lisp, Objective-C, PHP, Python, Ruby,
Smalltalk etc.
The benefits of static type checking are that
▪ It can determine the type of any data at runtime.
▪ It gives some freedom to the programmer as it is less concerned about types.
▪ In dynamic typing, the variables do not have any types associated with them, i.e.
they an refer to a value of any type at runtime.
▪ It checks the values of all the data types during execution which results in more
robust code.
▪ It is more flexible and can support union types, where the user can convert one
type to another at runtime.
The main disadvantages of this method is that it makes the execution of the program
slower by performing repeated type checks.
Strong Type Checking:
Programming languages in which variables have specific data types are strong typed.
This implies that in strong typed languages, variables are necessarily bound to a
particular data type. Python is strong typed, and so is Java. A type checking which
guarantees that no type errors can occur at runtime is called strong type checking and
the system is called strongly typed.
The strong type checking has certain disadvantages such as:
1. There are some checks like array bounds checking which require dynamic
checking.
2. It can result into performance degradation.

Compiled by: Surajit Das Page 4|6


Type Equivalence:
Type Equivalence is used by the type checking to check whether the two type
expressions are equivalent or not. It can be done by checking the equivalence between
the two types. The rule used for type checking works as follows:

if two type expressions are equivalent


then return a certain type
else return a type_error

When two type expressions are equivalent, we need a precise definition of both the
expressions. When names are given to type expressions, and these names are further
used in subsequent type expressions, it may result in potential ambiguities.
There are two schemes to check type equivalence of expressions:
Structural Equivalence:
When type expressions are represented by graphs, two types are structurally equivalent
if and only if one of the following conditions is true:
• They are the same basic type.
• They are formed by applying the same constructor to structurally equivalent
types.
• One is a type name that denotes the other.
Name Equivalence:
Two type expressions are name equivalent if and only if they are identical, that is if they
can be represented by the same syntax tree, with the same labels.
For example, consider the following few types and variable declarations,
Typedef double Value
….
….
Value var1, var2
Sum var3, var4
In these statements, var1 and var2 are name equivalent, so are var3 and var4, because
their type names are same. However, var1 and var3 are not name equivalent, because
their type names are different.

Compiled by: Surajit Das Page 5|6


Type Conversion:
Consider expressions like “x + i”, where x is of type float and i is of type integer. Since
the representation of integers and floating-point numbers is different within a computer
and different machine instructions are used for operations on integers and floats, the
compiler may need to convert one of the operands of “+” to ensure that both operands
are of the same type when the addition occurs. Suppose that integers are converted to
floats when necessary, using a unary operator (float). For example, the integer 2 is
converted to a float in the code for the expression 2 * 3.14
t1 = (float) 2
t2 = t1 * 3.14

The type conversion can be done implicitly or explicitly. The conversion from one type
to another is called implicit, if it is automatically done by the compiler. Usually, implicit
conversions of constants can be done at compile-time and it results in an improvement
in the execution time of the object program. Implicit type conversion is also known as
coercion.
A conversion is said to be explicit if the programmer must write something to cause the
conversion. Explicit conversion is also known as casts.
Conversion in languages can be considered as widening conversions (which are
intended to preserve information) and narrowing conversions (which can lose
information).

The widening rules are given by the hierarchy in Fig.(a): any type lower in the
hierarchy can be widened to a higher type. Thus, a char can be widened to an int or to
a float, but a char cannot be widened to a short.
The narrowing rules are illustrated by the graph in Fig.(b): a type s can be narrowed
to a type t if there is a path from s to t. Note that char, short, and byte are pairwise
convertible to each other.
Compiled by: Surajit Das Page 6|6
COMPILER DESIGN
INTERMEDIATE CODE GENERATION

Surajit Das 4/9/20


What is Intermediate Code Generation?
During the translation of a source program into the object code for a target machine, a
compiler may generate a middle-level language code, which is known as Intermediate
Code of intermediate text. The complexity of this code lies between the source
language code and the object code. The intermediate code can be represented in the
form of postfix notation, syntax tree, three address code, Quadruples, triples, and
directed acyclic graph.
Benefits of using an Intermediate Code Generation over direct code generation:
➢ Intermediate Code is machine independent, which makes it easy to retarget the
compiler to generate code for newer and different processor.
➢ Intermediate Code is nearer to the target machine as compared to the source
language so it is easier to generate the object code.
➢ The Intermediate code allows the machine-independent optimization of the
code. Several specialized techniques are used to optimize the intermediate code
by the front end of the compiler.
Two representations of intermediate languages are:
➢ High Level Intermediate representation: This representation is closer to the
source program. Thus, it represents the high-level structure of a program, that
is, it depicts the natural hierarchical structure of the source program. The
examples of this representation are directed acyclic graphs (DAG) and syntax
trees. The features of High- Level Intermediate representation are:
• It retains the program structure as it is nearer o the source program.
• It can be constructed easily from the source program.
• It is not possible to break the source program to extract the levels of code
sharing due to which the code optimization in this representation
becomes a bit complex.

➢ Low- Level Intermediate representation: This representation is closer to the


target machine where it represents the low-level structure of a source program.
It is appropriate for machine-dependant tasks like register allocation and
instruction selection. The example of this representation is Three-Address Code.
The features of Low- Level Intermediate representation are:
• It is nearer to the target machine
• It makes easier to generate the object code.
• High effort is required by the source program to generate the low-level
representation.

C o m p i l e d b y : S u r a j i t D a s P a g e 1 | 15
High-level Low-level
Source .. .. . Target (Object)
Intermediate Intermediate
Program Code
representation representation

Postfix Notation:
Generally, we use infix notation to represent an arithmetic expression such as
multiplication of two operands a and b (e.g. x=a*b). But in postfix notation the operator
is shifted to the right end, as x = ab*.
Process of evaluation of postfix expression:
• If the scan symbol is an operand, then it is pushed onto the stack, and scanning
is continued.
• If the scan symbol is binary operator, then the two topmost operands are
popped from the stack. The operator is applied to these operands, and the result
is pushed back to the stack.
• If the scan symbol is a unary operator, it is applied to the top of the stack and
the result is pushed back onto the stack.
▪ The result of a unary operator can be shown within parenthesis.

What is Three-Address Code? What are its types? How it is implemented?


A string of the form X: = Y OP Z, in which OP is a binary operator, Y and Z are the
addresses of the operands, and X is the address of the result of the operation, is known
as Three-Address statement. The operator OP can be fixed or floating-point arithmetic
operator, or logical operator. X, Y, and Z can be considered either as constants or as
predefined names by the programmer or temporary names generated by the compiler.
This statement is named as the “Three-Address statement” because of the usage of
three address, one for the result and two for the operands. The complicated arithmetic
expressions are not allowed in three-Address code because only a single operation is
allowed per statement.
For example, consider the expression A + B * C, this expression contains more than one
operator so the representation of this expression in a single three-address statement is
not possible. Hence, the three-address code of the given expression is as follows:
T1 = B * C
T2 = A + T1
Where, T1 and T2 are the temporary names generated by the compiler.
C o m p i l e d b y : S u r a j i t D a s P a g e 2 | 15
Types of Three-Address Statements:
Different forms of three-address statements are given as follows:
• Assignment Statements: These statements can be represented as:
X: = Y OP Z, where OP is any logical/arithmetic binary operator.
X: = OP Y where OP is a unary such as logical negation, conversion operators,
and shift operators.
X: = Y, where the value of Y is assigned to operand X.
• Indexed Assignment Statements: These statements can be represented as:
X: = Y[I], where X, Y, and I refer to the data objects and are represented by
pointers to the symbol table.
• Address and pointer Assignment Statements: These statements can be
represented as:
X: = addr Y defines that X is assigned the address of Y.
X: = * Y defines that X is assigned the content of location pointed to by Y.
*X: = Y sets the r- value of the object pointed to by X to the r- value of Y.
• Jump Statements: Jump statements are of two types – Conditional and
unconditional that works with relational operators and are represented in the
following forms:
▪ The unconditional jump is represented as goto L, where L being a label.
This instruction means that the Lth three-address statement is the next
to be executed.
▪ The Conditional jumps such as if X relop Y goto L, where relop signifies
the relational operator (<=, =, >=) applied between X and Y. This
instruction implies that if the result of the expression X relop Y is true then
the statement labeled L is executed. Otherwise, the statement
immediately following the if X relop Y goto L is executed.
• Procedure Call/ Return Statements: These statements can be defined in the
following forms:
Param X and call P, n, where they are represented and typically used in the
three-address statement as follows:

Param X1
Param X2
.
.
.
Param Xn
Call P, n

C o m p i l e d b y : S u r a j i t D a s P a g e 3 | 15
Here, the sequence of three-address statements is generated as a part of call of the
procedure P (X1, X2, ……Xn) and n in call P, n is defined as an integer specifying the total
number of actual parameters in the call.
Y = call P, n represents the function call.
Return Y, represents the return statement, where Y is a returned value.
Implementation of Three- Address Statements:
The three-address statement is an abstract form of intermediate code. Hence, the
actual implementation of the three-address statements can be done in the following
ways:
✓ Quadruples
✓ Triples
✓ Indirect triples

Quadruples
Quadruples is defined as a record structure used to represent a three-address
statement. It consists of four fields. The first field contains the operator, the second and
third fields contain the operand 1 and operand 2, respectively, and the last field
contains the result of that three-address statement. For better understanding of
quadruples representation of any statement,
consider a statement, S = - z / a * (x+y)
To represent this statement into quadruples representation, we first construct the
three-address code as follows:
t 1: = x + y
t 2: = a * t 1
t 3: = - z
t4: = t3/t2
S: = t4
The quadruple representation of this three-address code is:
Operator Operand 1 Operand 2 Result
0 + x y t1
1 * a t1 t2
2 - z t3
3 / t3 t2 t4
4 := t4 S
C o m p i l e d b y : S u r a j i t D a s P a g e 4 | 15
Triples
A triple is also defined as a record structure that is used to represent a three-address
statement. In Triples, for representing any three-address statement three fields are
used, namely, operator, operand 1 and operand 2, where operand 1 and operand 2 are
pointers to either symbol table or they are pointers to the records (for temporary
variables) within the triple representation itself. In this representation, the result field
is removed to eliminate the use of temporary names referring to symbol table entries.
Instead, we refer the results by their positions. The pointers to the triple structure are
represented by parenthesized numbers, whereas the symbol-table are represented by
the names themselves.

Operator Operand 1 Operand 2


0 + x y
1 * a (0)
2 - z
3 / (2) (1)
4 := S (3)

In Triple representation, the ternary operation X: = Y[I] is represented by using two


entries in the triple structure. For this operation X: = Y[I], we can write two instructions,
t: = Y[I] and X: = t. Note that instead of referring the temporary t by its name, we refer
it by its position in the triple.

Operator Operand 1 Operand 2


0 =[] Y I
1 := (0) X

Indirect Triples
An indirect triple representation consists of an additional array that contains the
pointers to the triples in the desired order. Let us define an array A that contains
pointers to triples in desired order. Indirect triple representation for the statement S
given in the previous example is:

C o m p i l e d b y : S u r a j i t D a s P a g e 5 | 15
A Operator Operand 1 Operand 2
101 (0) 0 + x y
102 (1) 1 * a (0)
2 - z
103 (2)
3 / (2) (1)
104 (3) 4 := S (3)
105 (4)

The main advantage of indirect triple representation is that an optimizing compiler can
move an instruction by simply reordering the array A, without affecting the triples
themselves.

MAKAUT Year-wise Questions:


(2012)
1. Translate the expression a: = - b * (c + d) / e into quadruples, triple and indirect
triples representation.)
(2015, 2013)
2. Translate the expression X = - (a + b) * (c + d) + (a + b + c) into quadruples and
triples representation.
(2016, 2014)
3. Translate the following expression:
A = b * -c + b* -c
Into Quadruples, Triples and Indirect Triples.

C o m p i l e d b y : S u r a j i t D a s P a g e 6 | 15
Methods of Translating a Boolean Expression into Three-Address Code:
There are two methods available to translate a Boolean expression into three-address
code,
▪ Numerical Representation
▪ Control-Flow Representation

Numerical Representation:
The first method of translating Boolean expression into three-address code comprises
encoding true and false numerically and then evaluating the Boolean expression similar
to an arithmetic expression. True is often denoted by 1 and false by 0. Some other
encodings are also possible where any non-zero or non-negative quantity indicates true
and any negative or zero number indicates false. Expressions will be calculated from
left to right like arithmetic expressions.
Consider a Boolean expression X and Y or Z, the translation of this expression
into three-address code is
t1 = X and Y
t2 = t1 or Z
Now, consider a relational expression if X > Y then 1 else 0, the three-address code
translation for this expression is as follows:
1. If X > Y goto (4)
2. t1: = 0
3. goto (5)
4. t1: = 1
5. Next
Here, t1 is a temporary variable that can have the value 1 or 0 depending on whether
the condition is evaluated to true or false. The label Next represents the statement
immediately following the else part.
Control-Flow Representation:
In the second method, the Boolean expression is translated into three-address code
based on the flow of control. In this method, the value of a Boolean expression is
represented by a position reached in a program. In case of evaluating the Boolean
expressions by their positions in program, we can avoid calculating the entire
expression. This method is useful in implementing the Boolean expressions in control-
flow statements such as if-then-else and while-do statements. For example, we
consider the Boolean expressions in context of conditional statements such as
C o m p i l e d b y : S u r a j i t D a s P a g e 7 | 15
• If X then S1 else S2
• While X do S

In the first statement, if X is true, the control jumps to the first statement of the
code for S1, and if X is false, the control jumps to the first statement of the code for
S2.

Code for X Code for X

True: Code for S1


True Code for S

False: Code for S2 goto

False

In case of second statement, when X is false, the control jumps to the statement immediately following
the while statement, and if X is true, the control jumps to the first statement of the code for S.

Example:
I. Generate the three-address code for the following program segment
while (x < z and y > s) do
If x = 1 then
z=z+1
else
while x <= s do
x = x + 10;
Ans: The three-address code for the given program segment is given below:
1. If x < z goto (3)
2. goto (16)

C o m p i l e d b y : S u r a j i t D a s P a g e 8 | 15
3. if y > s goto (5)
4. goto (16)
5. if x = 1 goto (7)
6. goto (10)
7. t1: = z + 1
8. z: = t1
9. goto (1)
10. if x < = s goto (12)
11. goto (1)
12. t2: =x + 10
13. x: = t2
14. goto (10)
15. goto (1)
16. Next

II. Consider the following code segment and generate the three-address code for
it.

for (k = 1; k <= 12; k++)


if x < y then a = b + c;

Ans: The three-address code for the given program segment is given below:

1. k: = 1
2. if k <= 12 goto (4)
3. goto (11)
4. if x < y goto (6)
5. goto (8)
6. t1: = b + c
7. a: = t1
8. t2: = k + 1
9. k: = t2
10. goto (2)
11. Next.

III. Translate the following statement, which alters the flow of control of
expressions, and generate the three-address code for it.

While (P < Q) do
If (R < S) then a= b + c;

C o m p i l e d b y : S u r a j i t D a s P a g e 9 | 15
Ans: The three-address code for the given statement is as follows:

1. If P < Q goto (3)


2. Goto (8)
3. If R < S goto (5)
4. goto (1)
5. t1: = b + c
6. a: = t1
7. goto (1)
8. Next

IV. Translate the following program segment into three-address statement:

switch (a + b)
{
case 2: {x = y; break;}
case 5: switch x
{
case 0: {a = b + 1; break;}
case 1: {a = b + 3; break;}
default: {a = 2; break;}
}
break;
case 9: {x = y – 1; break;}
default: {a = 2; break;}
}

Ans: The three-address code for the given program segment is given below:

1. t1: = a + b
2. goto (23)
3. x: = y
4. goto (27)
5. goto (14)
6. t3: = b + 1
7. a: = t3
8. goto (27)
9. t4: = b+ 3
10. a: = t4
11. goto (27)
12. a: = 2

C o m p i l e d b y : S u r a j i t D a s P a g e 10 | 15
13. goto (27)
14. if x = 0 goto (6)
15. if x = 1 goto (9)
16. goto (12)
17. goto (27)
18. t5: = y – 1
19. x: = t5
20. goto (27)
21. a: = 2
22. goto (27)
23. if t1: = 2 goto (3)
24. if t1: = 5 goto (5)
25. if t1: = 9 goto (18)
26. goto (21)
27. Next
V. Translate the following program segment into three-address statement:

int a[10], b[10], i, s;


for (i = 0; i<10; i++)
{
s+ = a[i] * b[i];
}

Ans: The three-address code for the given program segment is given below:
1. s = 0
2. i = 0
3. if i > 10 goto (16)
4. t1 = addr(a)
5. t2 = i * 4
6. t3 = t1[t2]
7. t4 = addr(b)
8. t5 = i * 4
9. t6 = t4[t5]
10. t7 = t3 * t6
11. t8 = s + t7
12. s = t8
13. t9 = i + 1
14. i = t9
15. goto (3)
16. Next
C o m p i l e d b y : S u r a j i t D a s P a g e 11 | 15
Exercise:
1. Generate the three-address code for the following program segment:
main ()
{
int k = 1;
int a[5];
while (k <= 5)
{
a[k] = 0;
k++;
}
}

Note:
If the elements of a two-dimensional array X [m][n] are stored in a row-major form, the relative
address of an array element X [i][j] is calculated as follows:
base + (i * n + j) * w
On the other hand, if the elements are stored in a column-major form, the relative address of X
[i][j] is calculated as follows:
base + (i + j * m) * w

Row-Major Column-Major
1. t1 = addr(X) 1. t1 = addr(X)
2. t2= i * n 2. t2= j * m
3. t3 = t2 + j 3. t3 = t2 + i
4. t4 = t3 * w 4. t4 = t3 * w
5. t5 = t1[t4] 5. t5 = t1[t4]

2. Generate the three-address code for the following program segment where x, y
are arrays of size 10 *10, and there are 4 bytes/word.
C o m p i l e d b y : S u r a j i t D a s P a g e 12 | 15
begin
add = 0
a=1
b=1
do
begin
add = add + x [a, b] * y [a, b]
a=a+1
b=b+1
end
while a <=10 and b <=10
end

C o m p i l e d b y : S u r a j i t D a s P a g e 13 | 15
Backpatching:
Backpatching is the process of leaving blank entries for the goto instruction where the
target address is unknown in the forward transfer in the first pass and filling these
unknown addresses in the second pass.
In all control transfer statements such as if, if-else, for, while, do-while, and switch-
case, the transfer of control takes place from one place to another. In all the examples
given in previous sections on control statements, there are goto statements to transfer
the control of the instruction. These control statements are categorized into
unconditional and conditional goto statements, which transfer the control either in
the forward direction or in the backward direction.

Quadruple no. Label Operator Operand 1 Operand 2 Result


1 = 156000 LoanAmount
2 = 0 Balance
3 = 1000 Instalments
4 = 0 noOfInstallments
5 L1 - LoanAmount Installments t1
6 = t1 Balance
7 + noOfInstallments 1 t2
8 = t2 noOfInstallments
9 < balance 0 t3
10 If= t3 0 L2
11 goto L1
12 L2 - balance t4
13 = t4 excess

These sets of quadruples involved both forward and backward control. From
quadruple 10, there is a forward control to quadruple 12, and from quadruple 11,
there is a backward control to quadruple 5. In case of backward control, the location
of the label is known at the point of its reference.

Labels Quadruple appearance


L1 5
L2 12

When a quadruple associated with the labels such as L1 and L2 is created during the
process of IC generation, a function such as

addlabel (Set S, Label L, Quadruple quadapperance)

C o m p i l e d b y : S u r a j i t D a s P a g e 14 | 15
can be called in the semantic action. Here S is the set data type, Label is the string
(such as L1, L2), and quadapperance is the reference of the quadruple associated with
this label.
When L1 is referred in quadruple 11, the location of L1 is known as quadruple
number 5. However, when L2 is referred in quadruple 10, its location is not known at
the point of its reference. Hence the actual location associated with label L2 is deferred
and left blank till the location is found.

Quadruple no. Label Operator Operand 1 Operand 2 Result


5 L1 - LoanAmount Installments t1
10 If= t3 0 __
11 goto 5
12 L2 - balance t4

At the end of the generation of the quadruples by leaving blank for the unknown
quadruple reference, a function
backpatch (LabelList L)
can be called to fill the blank entries with the appropriate quadruple reference from
the Label-Quadruple appearance pair table.

Quadruple no. Label Operator Operand 1 Operand 2 Result


5 L1 - LoanAmount Installments t1
10 If= t3 0 12
11 goto 5
12 L2 - balance t4

MAKAUT Year-wise Questions:


2015:
1. Generate three-address code for the code segment:
While (i<10)
{
x = 0;
y = x+ 2;
i = i + 1;
}
And implement it in quadruples, triples, and indirect triples.

C o m p i l e d b y : S u r a j i t D a s P a g e 15 | 15
COMPILER DESIGN
CODE OPTIMIZATION

Surajit Das 4/21/20


Explanation:
Optimization is a program transformation technique which refers to the reduction of
execution time and/or memory space requirements. The execution time mainly
reduces the energy of the computer so that it can produce better object code (target
code) with high execution efficiency than the input source program.
The main question is, to which part of the compiler can optimization techniques
be applied? Programmers write optimized codes. However, there are some sources of
optimization where programmers do not have direct control. In addition, the
optimization techniques have to be applied from the beginning of the program to the
end. The complexity of an algorithm or program is always written in terms of its run-
time complexity such as O(n), O(nlog2n), and O(n2), where n refers to the problem size.
For example, the number of employees (n) decides a payroll program complexity.
Here, a piece of code (computing the pay of an employee) is executed n times. If this
code is not well written, the performance of the code becomes inferior in terms of its
execution time. In general, on analysis of the program, it is found that most of the
execution time is spent on iterating through such small segments of code. Hence, the
code inside the loop is potential candidate for optimization. Hence, the requirements
for optimization are as follows:
1. To decide which part of the compiler optimization techniques can be applied to
2. To collect information about how variables are used
3. To identify the various controls in the program
4. To apply the required transformation in the appropriate part of the compiler.
5. Reduce the overhead involved in the optimization process.
Most recent compilers come with two modes. One is the development mode, and the
other is the release mode. During the development mode, the focus is on the
development and compilation of the source code for the given applications. After the
successful development of the code, it is released to the customer. At this point, the
source code is optimized so that the released code will run faster. Thus, the intended
objective of optimization is to transform the code so that the complexity of the running
code is reduced.
Objectives of Optimization:
The main objectives of the optimization techniques are as follows:
1. Exploit the fast path in case of multiple paths for a given solution.
2. Reduce redundant instructions.
3. Produce minimum code for maximum work.
4. Trade-off between the size of the code and the speed with which it gets
executed.
Compiled by: Surajit Das P a g e 1 | 17
5. Place code and data together whenever it is required to avoid unnecessary
searching of data/code.
6. Access memory strictly according to the hierarchy (register, cache memory,
main memory).
7. Exploit the features of parallel/ piped line architecture of the given system.
8. Decide on the right instruction to be used.
Construction of Basic Blocks and Processing
To study and analyse the whole set of ICs for optimization purposes, it is preferable to
divide them into set of manageable blocks called Basic Blocks.
A basic block is a sequence of consecutive ICs in which there is an entry and exit
without any branch at the end. Here, the flow of control enters only from the first
statement of the basic block and once entered, the statements of the block are
executed without branching, looping, or jumping except at the last statement. The
control will leave the block only from the last statement of the block. For example,
t1 = b * c
t2 = a + t 1
d = t2

In the above sequence of statements, the control enters from the first statement t1 =
b * c. The second and third statements are executed sequentially without any looping
or branching and the control leaves the block from the last statement. Hence the
above statements form a basic block.
Algorithm for partitioning of three-address instructions into basic block:
A sequence of three-address instructions is taken as input and the following steps are
performed to partition the three-address instructions into basic blocks:
Step 1: Determine the set of leaders (First statement in the basic block is called leader).
[The rules for finding leaders are:
1. The first statement in the intermediate code is leader.
2. The target statement of a conditional and unconditional jump is a leader.
3. The intermediate statement following an unconditional or conditional jump
is a leader.]
Step 2: Construct the basic block for each leader that consists of the leader and all the
instructions till the next leader (excluding the next leader) or the end of the program.
The instructions that are not included in a block are not excluded and may be
removed, if desired.
Consider a program that computes the average of a set of n numbers.

Compiled by: Surajit Das P a g e 2 | 17


double computeAverage (int A[], int n)
{
int i, sum;
double average = 0;
for (i = 0; i < n; i++)
sum = sum + A[i];
average = sum / n;
}
The corresponding three-address code for the above code segment is given as follows:
1. sum = 0
2. i = 0
3. average = 0
4. t1 = 4 * i
5. t2 = A[t1]
6. t3 = sum + t2
7. t4 = i + 1
8. i = t4
9. if i < n goto step (4)
10. t5 = sum/n
11. average = t5
Now, we can determine the basic blocks of the above three-address code by following
the previous algorithm. Considering the rules for finding the leaders, according to rule
(1) statement (1) is a leader. According to rule (2), statement (4) is a leader. According
to rule (3), statement following the 10th statement, is a leader.

1. sum = 0
2. i = 0
3. average = 0 B1

1. t1 = 4 * i
2. t2 = A[t1]
3. t3 = sum + t2 B2
4. t4 = i + 1
5. i = t4
6. if i < n goto step (1)

1. t5 = sum/n
2. average = t5 B3

Compiled by: Surajit Das P a g e 3 | 17


Linking of Basic Blocks:
In general, the source language and in turns its intermediate representation have
some structure such as sequential statement and control flow statement. Normally,
the sequence of statements will be fit into a basic block even if it is large. Owing to the
presence of control flow statement, there is a need for partitioning the statements
into basic blocks and linking them.
In addition, each source program has a set of statements that are
executed once or multiple times. Hence, there is a need for modelling of the repetition
of basic clocks. A directed graph is a suitable model for representing the flow of
statements and in turn the flow of basic block. Thus, a flow graph can be constructed
to study the flow of information across various basic blocks. Here the nodes of the
graph are the basic blocks. The edges of the graph flow from one basic block to
another.

The Basic block Bi precedes Bj, and Bj


Bi Bj
is the successor of Bi.

1. sum = 0
2. i = 0 B1
3. average = 0

1. t1 = 4 * i
2. t2 = A[t1]
3. t3 = sum + t2
4. t4 = i + 1
B2
5. i = t4
6. if i < n goto step (1)

3. t5 = sum/n
4. average = t5 B3

Compiled by: Surajit Das P a g e 4 | 17


Exercise:
Consider the following code:
i. i = 12
ii. j=1
iii. t1 = 10 * i
iv. t2 = t1 + j
v. t3 = 8 * t2
vi. t4 = t3 – 88
vii. a[t4] = 0
viii. j=j+1
ix. if j <=10 goto (iii)
x. i=i+1
xi. if i <=10 goto (ii)
xii. i=1
xiii. t5 = i – 1
xiv. t6 = 88 * t5
xv. a[t6] = 1
xvi. i=i+1
xvii. if i <=10 goto (xiii)

find out the basic block and draw the flow graph for the above code.

Data-Flow Analysis using Flow Graph:


To appropriately apply optimization technique, the status of the variables and their
relationship with other variables play a major role. Data-flow analyses collect
information about the variable parts (blocks) of the program and disseminate the
information to the blocks in the flow. A set of equations is involved in collecting such
information. During the process of data-flow analysis, it should be ensured that the
meaning of the program is retained. Data-flow analysis can be aggressive so that
information can be collected as accurately as possible to explore the maximum
benefits of the optimizations. On the other hand, the flow analysis can be conservative
so that information collected during this process may not be used in the optimization
when it does not retain the semantics of the program.
In general, the objective of the data-flow analysis is to collect information at
the beginning and end of each block. Sometimes, the process has to be iterated
because of the loop associated with the basic block. The information collected in this
process is represented in a set data type. The different sets are Spawn, Destroy, Input
and Output. These set of information are subjected to changes as they go through the

Compiled by: Surajit Das P a g e 5 | 17


basic block. The information about when a variable is defined, when it is redefined,
how it is available, how it is used, and so on are required from the perception of
optimization. For example, if there are n variables in the program, n such kinds of
information are to be maintained. Hence, a collection of n bit vectors could be used to
hold this information, where each bit position corresponds to one definition.

Reachable Definitions
There are many variables- defined and undefined in an IC. For example, consider the
following quadruples:
X = 10;
X = 20;
X = 30;

In these statements (quadruples), the variable X is defined three times. In the first
statement, the variable is defined with the value 10. In the second statement, the
previous value is undefined and a new value 20 is defined for X. Similarly, it is extended
to the third statement. If the values are not used in any right-hand side of the
quadruple, these values can be dead values and the associated code is said to be dead
code. Such dead code can be eliminated. In this example, the last definition, i.e., X =
30, reaches the forthcoming statement. Such definitions are said to live in the basic
block. Hence, it is required to keep track of the set of quadruples that is available at
the input and output of a basic block.
The term spawn is used to define a variable, and the term destroy to kill the previous
definition. Hence, it is preferable to maintain a set of definitions generated out of each
statement and a set of definitions that are destroyed as a result of this statement.

1. Spawn[S] is the set of definitions generated from the statement S.


2. Destroy[S] is the set of definitions that are killed as a result of realizing the
statement S.
3. Input[S] is the set of definitions that are available at the input of the statement
S.
4. Output[S] is the set of definitions that are available at the output of the
statement S.

Hence, the set of definitions that are available at the end of the statement S is
expressed by the following data-flow equation:

Output[S] = Spawn[S] U (Input[S] - Destroy[S])

Compiled by: Surajit Das P a g e 6 | 17


Directed Acyclic Graph (DAG):

DAG is a Directed Acyclic Graph that is used to represent the basic blocks and to
implement transformation on them. For rearranging the ICs in the basic block, DAG is
used. It represents the way in which the value computed by each statement in a basic
block is used in the subsequent statements in the block. Every node in a flow graph
can be represented by DAG. Each node of a DAG is associated with a label. The labels
are assigned by using three rules:
1. The variable names or constants are the leaves of the DAG and are labelled
uniquely. Mostly leaves represent r values.
2. Interior nodes are labelled by the operator symbol.
3. Nodes are also labelled by a sequence of identifiers. The interior nodes are the
computed values and the identifiers labelling the node are said to possess the
value.
Note:
DAGs are different from a flow graph. A DAG represents a basic block, and a flow graph
represents the basic block and its relationship with other basic blocks.
Example:
Consider the following ICs representing the expression a + b * c + d + b * c
t1 = b * c
t2 = b * c
t3 = a + t1
t4 = d + t2
t5 = t3 + t4
t5
+

t3 t4
+ +
t1, t2

a b c d
Compiled by: Surajit Das P a g e 7 | 17
In this example, the first two leaf nodes corresponding to the variables b and c are
created and linked by the interior node, that is, the operator node *, and it is assigned
the label t1. In the next quadruple, a similar right-hand side b * c appears, for which
the nodes are already created and t2 is added to the label. This enables the catching
of the common subexpression b * c. This process is repeated for every quadruple.

Algorithm
// Input: A basic block of ICs
// Output: A DAG
// Cases of ICs: (i) x = y op z, (ii) x = op y, (iii) x = y
for each statement IC of B
{
If (node(y) is not defined)
createLeafNode(y)
defineNode(y)
endif
//Do it for z also
Case (i): n = findNode(node(op), node(y), node(z))
if (n is not found)
n = createNode (op, y, z)
endif
Case (ii): n = findNode (node (op), node(y))
if (n is not found)
n = createNode (op, y)
endif
Case (iii): n = node(y)
Delete X from attached identifier for node(x)
Append x to attached identifier for node(n)
}

Compiled by: Surajit Das P a g e 8 | 17


Advantages of DAG:
✓ Common subexpression can be detected.
✓ It helps in determining the instructions that compute a value which is never
used. It is referred to as dead code elimination.
✓ Identifiers that have their values used in the block can be determined.
✓ Statements that compute values that could be used outside the block can be
determined.
✓ Helps in determining those statements which are independent of one another
and hence, can be reordered.

MAKAUT Previous year questions:

2015, 2017
1. Construct the DAG for the following basic block:
d: = b * c, e: = a + b, b: = b * c, a: = e – d
2014
2. Construct the DAG for the following expression:
A = B * -C + B * -C
2013
3. Construct the DAG for the following basic block:
a = b + c, b = a – d, c = b + c, d = a - d

Dead Code Elimination


Dead code is the portion of the program that will not be executed in any path. Its
presence in the code is redundant, and it can be removed. The possible causes of dead
codes are due to the fact that a value assigned to a variable is never used in the
program and some portion of the code will not be reachable. So that we can delete
from a DAG any root (node with no ancestors) that has no live variable attached.
Repeated application of this transformation will remove all nodes from the DAG that
corresponds to dead code.
Consider the following three-address code: + e
a=b+c
b=b–d a b c
+ - +
c=c+d
e=b+c

b0 c0 d0

Compiled by: Surajit Das P a g e 9 | 17


In the above DAG, a and b are live variables, but c and e are not. So, we can
immediately remove the root labelled e. Then the root labelled c becomes a root and
can be removed. The node labelled a and b remains, since they each have live variable
attached.
DAG from a Code Segment:
Consider the following code segment:
begin
product: = 0
j: = 1
do
begin
product: = product + X[j] * Y[j]
j: = j + 1
end
while j <= 20
end

The corresponding three-address code for the above code segment is:
1. product: = 0
2. j: = 1
3. t1: = 4 * j
4. t2: = x[t1]
5. t3: = 4 * j
6. t4: = y[t3]
7. t5: = t2 * t4
8. t6: = product + t5
9. product: = t6
10. t7: = j + 1
11. j: = t7
12. if j < = 20 goto (3)
The corresponding DAG for the above three-address code is given below:

Compiled by: Surajit Das P a g e 10 | 17


Compiled by: Surajit Das P a g e 11 | 17
The Principle sources of Optimization:
The size of the IC generated by the semantic actions varies with the size of the source
code. Hence, for a large program, the compiler has to manage a large IC. If the ICs are
divided into basic blocks and the flow among the basic blocks is identified, it will be
convenient to apply the optimization techniques on the ICs. The nature of the
transformations from the unoptimized code to optimized code must retain the
meaning of the original code. If the transformations are applied to only one basic
block, it is called local optimization, and if they are applied across the basic blocks,
then it is called global optimization.
There are varieties of transformations available.
1. Identification of the common subexpression and elimination.
2. Copy Propagation
3. Dead Code elimination. (Discussed earlier)

1. Identification of the common subexpression and elimination:


If an IC of the form c = a op b is available in as basic block, it can be verified whether
the definitions for a and b are available at the beginning of the basic block and are not
redefined inside the basic block before this statement. Then it can be checked whether
the same IC is available earlier in any of the basic blocks. This process of identifying
the common subexpression and eliminating it will reduce the number of
computations.
Consider the following C program segment, which copies 10 locations of one array into
another.
for (i = 0; i < 9; i++)
b[i] = a[i];

The intermediate code of above code segment is


1. i = 0
2. if i < 10 goto 4
3. goto 10
The Expression 4 * i is common at both line
4. t1 = 4 * i number 4 and 6 and can be subjected
5. t2 = a[t1] optimization. So, we need to eliminate the
6. t3 = 4 * i duplicate line for getting optimized code.
7. b[t3] = t2
8. I = I + 1
9. goto 2
10. Next

Compiled by: Surajit Das P a g e 12 | 17


The optimized code of the above code segment is:
1. i = 0
2. if i < 10 goto 4
3. goto 11
4. temp = 4 * i
5. t1 = temp
6. t2 = a[t1]
7. t3 = temp
8. b[t3] = t2
9. I = I + 1
10. goto 2
11. Next

2. Copy Propagation:

Copy propagation is a transformation of the IC such that when an assignment a = b


occurs for the variables a and b; every occurrence of a can be replaced by b as long the
values of a and b are not redefined. Copy propagation may reduce a chain of
dependencies.

a=d+e b=d+e t=d+e t=d+e


a=t b=t

c=d+e c=t

(a) (b)

In order to eliminate common-sub expression from the statement c= d + e we must


use a new variable t to hold the value of d + e. The value of variable t, instead of that
of the expression d + e, is assigned to c. Since control may reach c= d + e either after
the assignment to a, or the assignment to b, it would be incorrect to replace c = d + e
either by c= a or c = b.

Compiled by: Surajit Das P a g e 13 | 17


Loops in Flow Graph:
Most of the program execution time is spent on the part of the code inside the loop.
Hence, identification of the loop is mandatory in optimization. A loop is a sequence of
statements with a beginning statement and an ending statement that has a control
statement such that it may go to the beginning once again based on some condition.
The control statement can be placed at the beginning or at the end of the loop in order
to iterate the loop. However, one can obtain the loop with the first statement
(dominator) followed by a sequence of statements, whose last statement gets
transferred to the first statement.
Dominator:
A dominator (dominates a node n) is formally defined as a node d in the flow graph(G),
if every path in G from the initial node to n goes through d and is denoted by d dom n.
From this definition, the following details can be derived:
i) Every node dominates itself.
ii) The initial node dominates all nodes in G.
iii) The node at the beginning of a loop dominates all nodes in the loop.
iv) Each node may have more than one dominator. However, each node n has
a unique immediate dominator m, which is the last dominator of this node
n on any path in G from the initial node to n.
v) The dominator information in a path of a flow graph can be visualized as a
dominator tree.

1
Dom (0) = {1,2,3,4,5,6}
Dom (1) = {2,3,4,5,6}
2 3
Dom (2) = {}
Dom (3) = {}
4 Dom (4) = {5,6}
Dom (5) = {6}
5 Dom (6) = {}

6 Compiled by: Surajit Das P a g e 14 | 17


This figure gives the dominator information. Since node 0 is at the very beginning of
the graph, it dominates all the other nodes. Hence, Dom (0) is {1,2,3,4,5,6}. In other
words, nodes 1,2,3,4,5 and 6 can be processed only after processing node (0). Dom (2)
is empty since nodes 4,5 and 6 can be reached through node 3. Similarly, Dom (3) is
empty. Likewise, the Dom of all the other nodes can be obtained.

Peephole Optimization:
The machine codes that are generated from the ICs are straightforward. There are
many chances that redundant codes may be present in the ICs. Hence, there is a need
for reviewing the codes that are generated. This processing is one of the optimization
techniques. However, there is no guarantee that the produced codes will be optimum.
Instead of looking at the code as a whole and doing the optimization process, local
pieces of the code can be considered for the optimization process called peephole
optimization.
Peephole optimization is a technique, in which a small portion of the code (known as
peephole) is taken into consideration and optimization is done by replacing the code
by the equivalent code with shorter or faster sequence of execution. The statements
within the peephole need not be contiguous, although some of the implementations
require the statement to be contiguous. Each improvement in the code may explore
the opportunities for some other improvements. So, multiple review of the code is
necessary to get maximum benefit from the peephole optimization. The possible
transformations that are applied are as follows:
1. Redundant instruction eliminations:
Consider these machine codes.
MOV R0, a
MOV a, R0
The first instruction can be eliminated since the second instruction ensures that the
value of a is already loaded into register R0. However, it cannot be deleted in a
situation when, it has a label which makes it difficult to identify that whether the first
instruction is always executed before the second. To ensure that this kind of
transformation in the target code would be safe, the two instructions must be in the
same basic block.
2. Removal of unreachable code:
Removing an unlabelled instruction that immediately follows an unconditional jump is
possible. This process eliminates a sequence of instructions when repeated. Consider
the following IC representation:

Compiled by: Surajit Das P a g e 15 | 17


If a == 1 goto L1
Goto L2
L1: Print error information
L2: …….
………
Here, the code is executed only if the variable a is equal to 1. Peephole
optimization allows the elimination of jumps over jumps. Hence, the above
code is replaced as follows irrespective of the value of the variable a.
If a! = 1 goto L2
Print error information
L2: ………
……..
Now, if the value of the variable is set to 0, then the first statement always
evaluates to true. Hence, the statement printing the error information in
unreachable and can be eliminated.
3. Flow of control optimization:
There are chances that the IC or the machine code may have to jump over multiple
places. The peephole optimization helps to eliminate the unnecessary jumps in the
intermediate code. For example, consider the following code sequence:
goto L1
…..
L1: goto L2
…..
L2: goto L3
……
L3: MOV a, R0
Multiple jumps of these machine codes make the code inefficient. These can be
reordered as
goto L3
…..
L1: goto L3
…..
L2: goto L3
……
L3: MOV a, R0

Compiled by: Surajit Das P a g e 16 | 17


4. Reduction in Strength:
It is the process of identifying costly instructions and replacing them with equivalent
cheaper instructions. For example, exponent operations such as x2 can be replaced
with x * x. Multiplication with numbers that are powers of 2, such as 2, 4, 8, …. can be
replaced with shift operations.

5. Use of machine idioms:


This is the process of exploiting the powerful features of the CPU instruction set for a
set of instructions. Some target machines provide hardware instructions to implement
certain operations in a better and efficient way. Thus, identifying the situations that
permit the use of hardware instructions to implement certain operations may reduce
the execution time significantly. For example, some machine provides auto-increment
and auto- decrement addressing modes, which add or subtract one respectively from
an operand. These modes can be used while pushing or popping a stack, or for the
statements of the form X: = X + 1 or X: X – 1. These transformations greatly improve
the quality of the code.

Compiled by: Surajit Das P a g e 17 | 17

You might also like