Unit IV-1
Unit IV-1
If we generate machine code directly from source code then for n target machine we
will have optimizers and n code generator but if we will have a machine-
independent intermediate code, we will have only one optimizer. Intermediate code
can be either language-specific (e.g., Bytecode for Java) or language. independent
(three-address code).
Department of Computer Science and Engineering
Creating a syntax tree involves strategically placing parentheses within the expression.
This technique contributes to a more intuitive representation, making it easier to discern
the sequence in which operands should be processed.
The syntax tree not only condenses the parse tree but also offers an improved visual
representation of the program’s syntactic structure,
Example: x = (a + b * c) / (a – b * c)
Easier to implement: Intermediate code generation can simplify the code generation
process by reducing the complexity of the input code, making it easier to implement.
Facilitates code optimization: Intermediate code generation can enable the use of
various code optimization techniques, leading to improved performance and efficiency
of the generated code.
Platform independence: Intermediate code is platform-independent, meaning that it
can be translated into machine code or bytecode for any platform.
Code reuse: Intermediate code can be reused in the future to generate code for other
platforms or languages.
Easier debugging: Intermediate code can be easier to debug than machine code or
bytecode, as it is closer to the original source code.
the compilation time, making it less suitable for real-time or time-critical applications.
Additional memory usage: Intermediate code generation requires additional memory
to store the intermediate representation, which can be a concern for memory-limited
systems.
Increased complexity: Intermediate code generation can increase the complexity of the
compiler design, making it harder to implement and maintain.
Reduced performance: The process of generating intermediate code can result in code
that executes slower than code generated directly from the source code.
Three address code in Compiler
Three address code is a type of intermediate code which is easy to generate and can be easily
converted to machine code. It makes use of at most three addresses and one operator to
represent an expression and the value computed at each instruction is stored in temporary
variable generated by compiler. The compiler decides the order of operation given by three
address code.
Three Address Code is Used in Compiler Applications
1. Optimization: Three address code is often used as an intermediate representation of code
during optimization phases of the compilation process. The three address code allows the
compiler to analyze the code and perform optimizations that can improve the performance
of the generated code.
2. Code generation: Three address code can also be used as an intermediate representation of
code during the code generation phase of the compilation process. The three address code
allows the compiler to generate code that is specific to the target platform, while also
ensuring that the generated code is correct and efficient.
3. Debugging: Three address code can be helpful in debugging the code generated by the
compiler. Since three address code is a low-level language, it is often easier to read and
understand than the final generated code. Developers can use the three address code to
trace the execution of the program and identify errors or issues that may be present.
4. Language translation: Three address code can also be used to translate code from one
programming language to another. By translating code to a common intermediate
representation, it becomes easier to translate the code to multiple target languages.
General Representation
a = b op c
Where a, b or c represents operands like names, constants or compiler generated temporaries
and op represents the operator.
Triples – This representation doesn’t make use of extra temporary variable to represent a
single operation instead when a reference to another triple’s value is needed, a pointer to that
triple is used. So, it consist of only three fields namely op, arg1 and arg2.
Disadvantage –
Temporaries are implicit and difficult to rearrange code.
It is difficult to optimize because optimization involves moving intermediate code. When a
triple is moved, any other triple referring to it must be updated also. With help of pointer
one can directly access symbol table entry.
Example – Consider expression a = b * – c + b * – c
Indirect Triples – This representation makes use of pointer to the listing of all references to
computations which is made separately and stored. Its similar in utility as compared to
quadruple representation but requires less space than it. Temporaries are implicit and easier to
rearrange code.
Example – Consider expression a = b * – c + b * – c
This distinguishes abstract syntax trees from concrete syntax trees, traditionally designated parse
trees. Parse trees are typically built by a parser during the source code translation
and compiling process. Once built, additional information is added to the AST by means of
subsequent processing, e.g., contextual analysis.
Example:
while b ≠ 0:
if a > b:
a := a - b
else:
b := b - a
return a
Syntax Directed Translation has augmented rules to the grammar that facilitate semantic
analysis. SDT involves passing information bottom-up and/or top-down to the parse tree in form of
attributes attached to the nodes. Syntax-directed translation rules use 1) lexical values of nodes,
2) constants & 3) attributes associated with the non-terminals in their definitions.
In syntax directed translation, every non-terminal can get one or more than one attribute or
sometimes 0 attribute depending on the type of the attribute. The value of these attributes is
evaluated by the semantic rules associated with the production rule.
In the semantic rule, attribute is VAL and an attribute may hold anything like a string, a
number, a memory location and a complex record. In Syntax directed translation, whenever a
construct encounters in the programming language then it is translated according to the semantic
rules define in that particular programming language.
Example
E -> E+T | T
T -> T*F | F
F -> INTLIT
This is a grammar to syntactically validate an expression having additions and
multiplications in it. Now, to carry out semantic analysis we will augment SDT rules to this
grammar, in order to pass some information up the parse tree and check for semantic errors.
For understanding translation rules further, we take the first SDT augmented to [ E -> E+T ]
production rule. The translation rule in consideration has val as an attribute for both the
non-terminals – E & T. Right-hand side of the translation rule corresponds to attribute values of the
right-side nodes of the production rule and vice-versa.
Generalizing, SDT are augmented rules to a CFG that associate 1) set of attributes to every
node of the grammar and 2) a set of translation rules to every production rule using attributes,
constants, and lexical values.
Let’s take a string to see how semantic analysis happens – S = 2+3*4. Parse tree
corresponding to S would be
To evaluate translation rules, we can employ one depth-first search traversal on the parse
tree. For better understanding, we will move bottom-up in the left to right fashion for computing the
translation rules of our example.
The above diagram shows how semantic analysis could happen. The flow of information
happens bottom-up and all the children’s attributes are computed before parents
Types of attributes – Attributes may be of two types – Synthesized or Inherited.
1. Synthesized attributes – A Synthesized attribute is an attribute of the non-terminal on the
left-hand side of a production. Synthesized attributes represent information that is being
passed up the parse tree. The attribute can take value only from its children (Variables in
the RHS of the production). The non-terminal concerned must be in the head (LHS) of
production. For e.g. let’s say A -> BC is a production of a grammar, and A’s attribute is
dependent on B’s attributes or C’s attributes then it will be synthesized attribute.
2. Inherited attributes – An attribute of a nonterminal on the right-hand side of a production
is called an inherited attribute. The attribute can take value either from its parent or from its
siblings (variables in the LHS or RHS of the production). The non-terminal concerned must
be in the body (RHS) of production. For example, let’s say A -> BC is a production of a
grammar and B’s attribute is dependent on A’s attributes or C’s attributes then it will be
inherited attribute because A is a parent here, and C is a sibling.
S – attributed and L – attributed SDTs in Syntax directed translation
1. S-attributed SDT :
If an SDT uses only synthesized attributes, it is called as S-attributed SDT.
S-attributed SDTs are evaluated in bottom-up parsing, as the values of the parent nodes
depend upon the values of the child nodes.
Semantic actions are placed in rightmost place of RHS.
2. L-attributed SDT:
If an SDT uses both synthesized attributes and inherited attributes with a restriction that
inherited attribute can inherit values from left siblings only, it is called as L-attributed
SDT.
Attributes in L-attributed SDTs are evaluated by depth-first and left-to-right parsing
manner.
Semantic actions are placed anywhere in RHS.
Example : S->ABC, Here attribute B can only obtain its value either from the parent –
S or its left sibling A but It can’t inherit from its right sibling C. Same goes for A & C –
A can only get its value from its parent & C can get its value from S, A, & B as well
because C is the rightmost attribute in the given production.
Annotated Parse Tree – The parse tree containing the values of attributes at each node for
given input string is called annotated or decorated parse tree.
The annotated parse tree is generated and attribute values are computed in bottom up manner.
Let us assume an input string 4 * 5 + 6 for computing synthesized attributes. The annotated
parse tree for the input string is
For computation of attributes we start from leftmost bottom node. The rule F –> digit is used
to reduce digit to F and the value of digit is obtained from lexical analyzer which becomes value of
F i.e. from semantic action F.val = digit.lexval. Hence, F.val = 4 and since T is parent node of F so,
we get T.val = 4 from semantic action T.val = F.val. Then, for T –> T1 * F production, the
corresponding semantic action is T.val = T1.val * F.val . Hence, T.val = 4 * 5 = 20
Similarly, combination of E1.val + T.val becomes E.val i.e. E.val = E1.val + T.val = 26.
Then, the production S –> E is applied to reduce E.val = 26 and semantic action associated with it
prints the result E.val . Hence, the output will be 26.
2. Inherited Attributes – These are the attributes which derive their values from their parent
or sibling nodes i.e. value of inherited attributes are computed by value of parent or sibling
nodes.
Example:
A --> BCD { C.in = A.in, C.type = B.type }
Let us assume an input string int a, c for computing inherited attributes. The annotated parse
tree for the input string is
The value of L nodes is obtained from T.type (sibling) which is basically lexical value
obtained as int, float or double. Then L node gives type of identifiers a and c. The computation of
type is done in top down manner or preorder traversal. Using function Enter_type the type of
identifiers a and c is inserted in symbol table at corresponding id.entry.
To see how the semantic rules are used, consider the annotated parse tree for 3 * 5. The left
most leaf in the parse tree, labeled digit, has attribute value lexval = 3, where the 3 is supplied by the
lexical analyzer. Its parent is for production 4, F ->digit. The only semantic rule associated with this
production denes F.val = digit.lexval, which equals 3.
At the second child of the root, the inherited attribute T’.inh is defined by the semantic rule
T’.inh = F.val associated with production 1. Thus, the left operand, 3, for the * operator is passed
from left to right across the children of the root.
The production at the node for T' is T'->FT'. The inherited attribute T’.inh is defined by the
semantic rule T’.inh = T’.inh X F.val associated with production 2.
With T’.inh = 3 and F.val =5, we get T’.inh = 15. At the lower node for T', the production is
T’->€. The semantic rule T’.syn = T’.inh defines T'.syn = 15. The syn attributes at the nodes for T’
pass the value 15 up the tree to the node for T, where T.val = 15.
Limited error recovery: SDT is limited in its ability to recover from errors during the
translation process. This can result in poor error messages and may make it difficult to locate
and fix errors in the input program.
Control statements are the statements that change the flow of execution of statements.
S → if E then S1
|while E do S1
In this grammar, E is the Boolean expression depending upon which S1 or S2 will be executed.
Following representation shows the order of execution of an instruction of if-then, if then-else, &
while do.
1. 𝐒 → 𝐢𝐟 𝐄 𝐭𝐡𝐞𝐧 𝐒𝟏
E.CODE & S.CODE are a sequence of statements which generate three address code.
E.TRUE is the label to which control flow if E is true.
E.FALSE is the label to which control flow if E is false.
The code for E generates a jump to E.TRUE if E is true and a jump to S.NEXT if E is false.
When S1.CODE will be executed, and the control will be jumped to statement following S, i.e.,
to S1.NEXT.
2. 𝐒 → 𝐈𝐟 𝐄 𝐭𝐡𝐞𝐧 𝐒𝟏 𝐞𝐥𝐬𝐞 𝐒𝟐
If E is true, control will go to E.TRUE, i.e., S1.CODE will be executed and after that S.NEXT
appears after S1.CODE.
Initially, both E.TRUE & E.FALSE are taken as new labels. Hen S1.CODE at label E.TRUE is
executed, control will jump to S.NEXT.
Therefore, after S1, control will jump to the next statement of complete statement S.
S1.NEXT=S.NEXT
∴ S2.NEXT=S.NEXT
3. 𝐒 → 𝐰𝐡𝐢𝐥𝐞 𝐄 𝐝𝐨 𝐒𝟏
Another important control statement is while E do S1, i.e., statement S1 will be executed till
Expression E is true. Control will arrive out of the loop as the expression E will become
false.
A Label S. BEGIN is created which points to the first instruction for E. Label E. TRUE is attached
with the first instruction for S1. If E is true, control will jump to the label E. TRUE & S1. CODE will
be executed. If E is false, control will jump to E. FALSE. After S1. CODE, again control will jump
to S. BEGIN, which will again check E. CODE for true or false.
If E. CODE is false, control will jump to E. FALSE, which causes the next statement after S to be
executed.
∴ E. FALSE = S. NEXT
Boolean expressions
Boolean expressions have two primary purposes. They are used for computing the logical values.
They are also used as conditional expression using if-then-else or while-do.
The AND and OR are left associated. NOT has the higher precedence then AND and lastly OR.
E → E1 OR E2 E1.true := E.true
E1.false := newlabel
E2.true := E.true
E2.false := E.false
E.code := E1.code | | generate(E1.false':') | | E2.code
E → E1 and E2 E1.true := newlabel
E1.false := E.false
E2.true := E.true
E2.false := E.false
E.code := E1.code | | generate(E1.true':') | | E2.code
E → NOT E1 E1.true := E.false
E1.false := E.true
E.code := E1.code
E → (E1) {E.place = E1.place}
E → id relop id2 code1:=generate(if id1.place relop id2.place goto E.True)
code2 := generate('goto' E.false)
E.code := code1 | | code2
E → TRUE E.code := generate('goto' E.true)
E → FALSE E.code := generate('goto' E.false)
CODE OPTIMIZATION:
Issues in the design of code optimization
Optimizing code is a crucial aspect of software development, ensuring that programs run efficiently,
consume fewer resources, and perform tasks quickly. Here are some common issues that developers
encounter when optimizing code:
1. Premature Optimization: Optimizing code before identifying performance bottlenecks can lead
to wasted effort and may even make the code more complex and harder to maintain.
2. Lack of Profiling: Without profiling tools, developers may optimize code in areas that don't
significantly impact performance. Profiling helps identify hotspots where optimizations will yield
the most benefit.
3. Inefficient Algorithms: Choosing the wrong algorithm or data structure can result in poor
performance. Understanding algorithm complexity and selecting appropriate algorithms is
essential.
4. Excessive Memory Usage: Allocating too much memory or using inefficient data structures can
lead to excessive memory usage, causing performance degradation, especially in memory-
constrained environments.
5. Inefficient Loops: Nested loops, unnecessary iterations, or redundant calculations within loops
can significantly impact performance, especially in large datasets.
6. Poor Resource Management: Not releasing resources properly, such as file handles, network
connections, or memory, can lead to resource leaks and degrade performance over time.
7. Ineffective Parallelization: In multi-threaded or distributed systems, improper synchronization,
excessive context switching, or inefficient use of parallel resources can hinder performance gains.
8. Unnecessary Function Calls: Calling functions excessively, especially in tight loops, can
introduce overhead. Inlining functions or reducing unnecessary function calls can improve
performance.
9. I/O Bottlenecks: Excessive disk I/O or network I/O operations can slow down performance.
Using buffered I/O, batching requests, or optimizing network communication can alleviate these
bottlenecks.
10. Platform-specific Issues: Code may perform differently on different platforms or hardware
architectures. Ensuring code is optimized for the target platform can improve performance.
11. Unnecessary Copying: Making unnecessary copies of data, especially large datasets, can lead to
performance overhead. Using references or pointers judiciously can reduce copying overhead.
12. Excessive Logging/Debugging: Excessive logging or debugging statements can impact
performance, especially in production environments. Using logging levels and conditional
logging can mitigate this issue.
13. Unnecessary Overhead: Code may contain unnecessary operations, such as redundant checks or
computations, which can be eliminated to improve performance.
14. Inefficient Compilation: Compilation flags, optimization settings, and build configurations can
impact the performance of the generated code. Ensuring proper compilation settings are used can
improve performance.
Addressing these issues requires a combination of careful analysis, profiling, algorithmic
optimization, and code refactoring. It's essential to balance optimization efforts with code
readability, maintainability, and the specific performance requirements of the application.
Why Optimize?
Optimizing an algorithm is beyond the scope of the code optimization phase. So the program is
optimized. And it may involve reducing the size of the code. So optimization helps to:
Reduce the space consumed and increases the speed of compilation.
Manually analyzing datasets involves a lot of time. Hence we make use of software like
Tableau for data analysis. Similarly manually performing the optimization is also tedious
and is better done using a code optimizer.
An optimized code often promotes re-usability.
Types of Code Optimization:
The optimization process can be broadly classified into two types :
Function-Preserving Transformations
There are a number of ways in which a compiler can improve a program without changing the
function it computes. Function preserving transformations examples:
Example
a = 10;
b = a + 1 * 2;
c = a + 1 * 2;
//’c’ has common expression as ‘b’
d = c + a;
After elimination
a = 10;
b = a + 1 * 2;
d = b + a;
Before elimination –
x = 11;
y = 11 * 24;
z = x * 24;
//'z' has common expression as 'y' as 'x' can be evaluated directly as done in 'y'.
After elimination –
y = 11 * 24;
Copy Propagation:
Assignments of the form f : = g called copy statements, or copies for short. The idea behind the
copy-propagation transformation is to use g for f, whenever possible after the copy statement f: =
g. Copy propagation means use of one variable instead of another. This may not appear to be an
improvement, but as we shall see it gives us an opportunity to eliminate x.
Example:
x=Pi; ……
A=x*r*r;
Dead-Code Eliminations:
A variable is live at a point in a program if its value can be used subsequently; otherwise, it is
dead at that point. A related idea is dead or useless code, statements that compute values that
never get used. While the programmer is unlikely to introduce any dead code intentionally, it
may appear as the result of previous transformations.
Example:
i=0;
if(i= =1)
{
a=b+5;
}
Here, ‘if’ statement is dead code because this condition will never get satisfied.
Constant folding:
Deducing at compile time that the value of an expression is a constant and using the constant
instead is known as constant folding. One advantage of copy propagation is that it often turns the
copy statement into dead code.
For example
a=3.14157/2 can be replaced by a=1.570 there by eliminating a division operation.
Optimization is applied to the basic blocks after the intermediate code generation phase
of the compiler. Optimization is the process of transforming a program that improves the code by
consuming fewer resources and delivering high speed. In optimization, high-level codes are
replaced by their equivalent efficient low-level codes. Optimization of basic blocks can be
machine-dependent or machine-independent. These transformations are useful for improving the
quality of code that will be ultimately generated from basic block.
I. Structure-Preserving Transformations:
The structure-preserving transformation on basic blocks includes:
int main()
{
x = 2;
cout << "Optimization"; // Dead Code Eliminated
return 0;
}
2. Common Sub expression Elimination:
In this technique, the sub-expression which are common are used frequently are
calculated only once and reused when needed. DAG ( Directed Acyclic Graph ) is used to
eliminate common subexpressions.
Example:
t1: = 4*i
t2: = a [t1]
t3: = 4*j
t4: = 4*i
t5: = n
t6: = b [t4] +t5
The above code can be optimized using the common sub-expression elimination as
t1: = 4*i
t2: = a [t1]
t3: = 4*j
t5: = n
t6: = b [t1] +t5
3.Renaming of Temporary Variables:
Statements containing instances of a temporary variable can be changed to instances of a new
temporary variable without changing the basic block value.
Example: Statement t = a + b can be changed to x = a + b where t is a temporary variable and x is
a new temporary variable without changing the value of the basic block.
4.Interchange of Two Independent Adjacent Statements:
If a block has two adjacent statements which are independent can be interchanged without
affecting the basic block value.
Example:
t1 = a + b
t2 = c + d
These two independent statements of a block can be interchanged without affecting the value of
the block.
II. Algebraic Transformation:
Countless algebraic transformations can be used to change the set of expressions computed by a
basic block into an algebraically equivalent set. Some of the algebraic transformation on basic
blocks includes:
1. Constant Folding
2. Copy Propagation
3. Strength Reduction
1. Constant Folding:
Solve the constant terms which are continuous so that compiler does not need to solve this
expression.
Example:
x = 2 * 3 + y ⇒ x = 6 + y
2. Copy Propagation:
It is of two types, Variable Propagation, and Constant Propagation.
Variable Propagation:
x=y ⇒ z = y + 2 (Optimized code)
z=x+2
Constant Propagation:
x=3 ⇒ z = 3 + a (Optimized code)
z=x+a
3.Strength Reduction:
Replace expensive statement/ instruction with cheaper ones.
x = 2 * y (costly) ⇒ x = y + y (cheaper)
x = 2 * y (costly) ⇒ x = y << 1 (cheaper)
Loop Optimization:
Loop optimization includes the following strategies:
1. Code motion & Frequency Reduction
2. Induction variable elimination
3. Loop merging/combining
4. Loop Unrolling
1. Code Motion & Frequency Reduction
Move loop invariant code outside of the loop.
Program with loop variant inside loop
int main()
{
for (i = 0; i < n; i++)
{
x = 10;
y = y + i;
}
return 0;
}
int main()
{
for (i = 0; i < n; i++) {
A[i] = B[i]; // Only one induction variable
}
return 0;
}
3. Loop Merging/Combining:
If the operations performed can be done in a single loop then, merge or combine the loops.
4. Loop Unrolling:
If there exists simple code which can reduce the number of times the loop executes then, the loop
can be replaced with these codes.
Program with loops
int main()
{
for (i = 0; i < 3; i++)
cout << "Cd";
return 0;
}
It basically works on the theory of replacement in which a part of code is replaced by shorter
and faster code without a change in output. The peephole is machine-dependent optimization.
Objectives of Peephole Optimization:
The objective of peephole optimization is as follows:
To improve performance
To reduce memory footprint
To reduce code size
Initial code:
y = x + 5;
i = y;
z = i;
w = z * 3;
Optimized code:
y = x + 5;
w = y * 3; //* there is no i now
//* We've removed two redundant variables i & z whose value were just being copied
from one another.
2. Constant folding: The code that can be simplified by the user itself, is simplified.
Here simplification to be done at runtime are replaced with simplified code to avoid
additional computation.
Initial code:
x = 2 * 3;
Optimized code:
x = 6;
Initial code:
y = x * 2;
Optimized code:
y = x + x; or y = x << 1;
Initial code:
y = x / 2;
Optimized code:
y = x >> 1;
6. Dead code Elimination:- Dead code refers to portions of the program that are never
executed or do not affect the program’s observable behavior. Eliminating dead code
helps improve the efficiency and performance of the compiled program by reducing
unnecessary computations and memory usage.
Initial Code:-
int Dead(void)
{
int a=10;
int z=50;
int c;
c=z*5;
printf(c);
a=20;
a=a*10; //No need of These Two Lines
return 0;
}
Optimized Code:-
int Dead(void)
{
int a=10;
int z=50;
int c;
c=z*5;
printf(c);
return 0;
}