Download as pdf or txt
Download as pdf or txt
You are on page 1of 17

III BTECH II-SEM, CSE: COMPILER DESIGN

UNIT- V
Code Optimization
Introduction:
• It is possible for programmer to perform compiler using his or her knowledge in choosing
better algorithm and data items.
• Optimization is process of transformation of code to an efficient code in terms of space and
time without changing the meaning of code.
• Following constraints are to be considered while applying techniques for code
improvements.
1. Transformation preserves meaning of program. i.e., target code should ensure semantic
equivalence with source program.
2. Program efficiency must be improved by measurable amount without changing
algorithm used in program.
3. When technique is applied on special format, then it is worth transforming to that
format only.
4. Some transformations are applied only after detailed analysis, which is time
consuming. It is not worthy if program runs very few number of times.

Principal source of optimization:


• Transformation of program is called local if it is applied with in basic block and global if
applied across basic block.
• There are different types of transformations to improve code and these transformations
depend on kind of optimization required.
1. Function preserving transformations:
These are performed without changing function it computes. These are primarily used
when global optimizations are performed.
2. Structure preserving transformations:
These are performed without changing the set of expressions computed by block.
These are mostly applied locally.
3. Algebraic transformations:
These are used to simplify computation of expression set using algebraic identities.
These can replace expensive operations by cheaper ones.
Example: Multiplication by 2 replace with left shift.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 1


III BTECH II-SEM, CSE: COMPILER DESIGN

Function preserving transformations:


• Function preserving transformations are
1. Common sub expression elimination
2. Copy propagation
3. Dead code elimination
4. Constant propagation
• Common sub expression elimination can be applied locally and globally. Other techniques
are applied globally.
1. Common sub expression elimination:
• Code can be improved by eliminating common sub expressions from code.
• Expression whose value was previously computed and values of variables in
expressions are not changed since computation can be avoided to recompute it by
using earlier computed value.
Example1:Consider following sample code Example2: Consider following sequence code
a=b*c a=b*c
....... c=4+c
z=b*c+d-c .......
Here z have common sub expression b * c. z=b*c+d-c
its value is not changed after point it was Here expression b*c is not common because
computed and it is used in z. We can avoid value of c is changed after computing b*c.
recomputing by replacing above code as So, we cannot eliminate this expression.
t1 = b * c
a = t1
..........
z = t1 + d - c

2. Copy propagation:
• If there are copy statements of form x=y then in copy propagation use of x is
replaced by y in subsequent expressions.
• Copy propagation is possible if none of the variable is changed after this argument.
Example: consider following code:
a=b+c
d=c //copy statement
........
e=b+d-3

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 2


III BTECH II-SEM, CSE: COMPILER DESIGN
We may notice that code has common sub expressions b + c and b + d, if we replace
variable d by variable c as both have same value.
After apply copy propagation, the above code is
a=b+c
d=c
........
e=b+c-3 ⇒ e=a-3
By copy propagation we can eliminate assignment.
3. Dead code elimination:
• A piece of code which is not reachable that is value it computes is never used
anywhere in program then it is said to be dead code and can be removed from
program safely.
Example:
a=b+c
d=c
........
e=b+c-3
Here expression d = c is never used in the program and d is not used in any lines of
code. So, it can be considered as dead code. It should be eliminated if it is not
disturbing execution of program.
After dead code elimination code should be
a=b+c
........
e=b+c–3
4. Constant Propagation :
• Constant Propagation is process of recognizing and evaluating constant expressions
at compile time rather than run time.
Example: Pi=2217
void area_per(int r)
{
float area, perimeter;
area = Pi * r * r;
print area, perimeter;
}
• At line 1, variable Pi is constant and its value 3.413 is assigned at compile time.
• At line 5 and 6 value of Pi is assigned with 3.413 in runtime.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 3


III BTECH II-SEM, CSE: COMPILER DESIGN

Loop Optimization:
• The major source of code optimization is loops. Most of runtime is spent inside loops
which can be reduced by reducing instructions in the loop.
1. Code motion
2. Elimination of induction variable
3. Strength reduction
1. Code motion :
• It reduces number of instructions in loop by moving instruction outside a loop.
• Here we move instructions or expressions that result is same value independent of
number of times .Loop is executed and place them at beginning of loop.
while(x != n - 2)
{
x = x + 2;
}
• Here n-2 is loop invariant computation it should remain unchanged because of that
it places outside of loop.
m=n-2
while(x != m)
{
x = x + 2;
}

2. Elimination of induction variable :


• An induction variable is loop control variable or any variable depends on induction
variable in some fixed way.
• It also defined as variable which is incremented and decremented by fixed number
in loop each time it executed.
• Induction variables are of form i=i+-c(c is constant)
• If there are two or more induction variables in loop then by elimination of
induction process eliminate all except one in loop.

Example:
int a[10], b[10];
void fun(void)
{
int i, j, k;
GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 4
III BTECH II-SEM, CSE: COMPILER DESIGN
for(i=0, j=0, k=0; i<10; i++)
a[j++] = b[k++];
return;
}
• Here i, j, k are induction variables. These are incremented by 1 each time loop is
executed. By changing the above code we can eliminate j, k induction variables.

int a[10], b[10];


void fun(void)
{
int i;
for(i=0; i<=10; i++)
a[i] = b[i];
return;
}
• It reduces the space and number of additions and subtractions in loop.

3. Strength Reduction:
• Strength Reduction is process of replacing expensive operation by equivalent
cheaper operations on target machine.
• On many machines multiplication replaced by addition and subtraction because
these have less than multiplications.
Example:
for(i=1; i<5; i++)
{
x = 4 * i;
}
• The instruction x=4*i in loop can be replaced by equivalent addition instruction as
x=x+4.
for(i=1; i<5; i++)
{
x = x + 4;
}

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 5


III BTECH II-SEM, CSE: COMPILER DESIGN

LOOPS IN FLOW GRAPH


• A graph representation of three-address statements, called a flow graph, is useful for
understanding code-generation algorithms, even if the graph is not explicitly
constructed by a code-generation algorithm. Nodes in the flow graph represent
computations, and the edges represent the flow of control.
Dominators:
• In a flow graph, a node d dominates node n, if every path from initial node of the flow
graph to n goes through d. This will be denoted by d dom n.
• Every initial node dominates all the remaining nodes in the flow graph and the entry of
a loop dominates all nodes in the loop. Similarly every node dominates itself.
Example:

Flow graph
*In the flow graph above,
• Initial node, node1 dominates every node.
• node 2 dominates itself
• node 3 dominates all but 1 and 2.
• node 4 dominates all but 1, 2 and 3.
• node 5 and 6 dominates only themselves ,since flow of control can skip around
either by going through the other.
• node 7 dominates 7,8 ,9 and 10.
• node 8 dominates 8,9 and 10.
• node 9 and 10 dominates only themselves.
• The way of presenting dominator information is in a tree, called the dominator tree in
which the initial node is the root.
• The parent of each other node is its immediate dominator. Each node d dominates only
its descendents in the tree.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 6


III BTECH II-SEM, CSE: COMPILER DESIGN
• The existence of dominator tree follows from a property of dominators; each node has
a unique immediate dominator in that is the last dominator of n on any path from the
initial node to n.
• In terms of the dom relation, the immediate dominator m has the property is d=!n and
d dom n, then d dom m

Dominator tree
D(1)={1} D(6)={1,3,4,6}
D(2)={1,2} D(7)={1,3,4,7}
D(3)={1,3} D(8)={1,3,4,7,8}
D(4)={1,3,4} D(9)={1,3,4,7,8,9}
D(5)={1,3,4,5} D(10)={1,3,4,7,8,10}

Natural Loop:
• One application of dominator information is in determining the loops of a flow graph
suitable for improvement.
• The properties of loops are
• A loop must have a single entry point, called the header. This entry point-dominates all
nodes in the loop, or it would not be the sole entry to the loop.
• There must be at least one way to iterate the loop(i.e.)at least one path back to the
header.
• One way to find all the loops in a flow graph is to search for edges in the flow graph
whose heads dominate their tails. If a→b is an edge, b is the head and a is the tail. These
types of edges are called as back edges.
Example:
In the above graph,
7 → 4 4 DOM 7
10 →7 7 DOM 10
4→3
8→3
9 →1

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 7


III BTECH II-SEM, CSE: COMPILER DESIGN

• The above edges will form loop in flow graph.


• Given a back edge n → d, we define the natural loop of the edge to be d plus the set of
nodes that can reach n without going through d. Node d is the header of the loop.

Inner loop:
• If we use the natural loops as “the loops”, then we have the useful property that unless
two loops have the same header, they are either disjointed or one is entirely contained
in the other. Thus, neglecting loops with the same header for the moment, we have a
natural notion of inner loop: one that contains no other loop.
• When two natural loops have the same header, but neither is nested within the other,
they are combined and treated as a single loop.

Pre-Headers:
• Several transformations require us to move statements “before the header”.
Therefore begin treatment of a loop L by creating a new block, called the preheater.
• The pre-header has only the header as successor, and all edges which formerly
entered the header of L from outside L instead enter the pre-header.
• Edges from inside loop L to the header are not changed.
• Initially the pre-header is empty, but transformations on L may place statements in it.

Two loops with the same header

Introduction of the preheader

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 8


III BTECH II-SEM, CSE: COMPILER DESIGN

Reducible flow graphs:


• Reducible flow graphs are special flow graphs, for which several code optimization
transformations are especially easy to perform, loops are unambiguously defined,
dominators can be easily calculated, data flow analysis problems can also be solved
efficiently.
• Exclusive use of structured flow-of-control statements such as if-then-else, while-do,
continue, and break statements produces programs whose flow graphs are always
reducible.
• The most important properties of reducible flow graphs are that there are no jumps
into the middle of loops from outside; the only entry to a loop is through its header.
Definition:
• A flow graph G is reducible if and only if we can partition the edges into two disjoint
groups, forward edges and back edges, with the following properties.
• The forward edges from an acyclic graph in which every node can be reached from
initial node of G.
• The back edges consist only of edges where heads dominate their tails.
Example: The above flow graph is reducible.
• If we know the relation DOM for a flow graph, we can find and remove all the back
edges.
• The remaining edges are forward edges.
• If the forward edges form an acyclic graph, then we can say the flow graph reducible.
• In the above example remove the five back edges 4→3, 7→4, 8→3, 9→1 and 10→7
whose heads dominate their tails, the remaining graph is acyclic.
• The key property of reducible flow graphs for loop analysis is that in such flow graphs
every set of nodes that we would informally regard as a loop must contain a back
edge.

Code Optimization:
• In compiler design, code optimization is a program transformation technique that
tries to improve the intermediate code to consume fewer resources such as CPU,
memory, etc., resulting in faster machine code.
• There are two types of code optimization techniques.
1. Local optimization- This code optimization applies to a small block of statements.
Examples of local optimization are variable/constant propagation and common
sub expression elimination.
2. Global optimization- This code optimization applies to large program segments
like functions, loops, procedures etc. An example of global optimization is data
flow analysis.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 9


III BTECH II-SEM, CSE: COMPILER DESIGN

Data Flow Analysis:


• Data flow analysis is a global code optimization technique. The compiler performs
code optimization efficiently by collecting all the information about a program and
distributing it to each block of its control flow graph (CFG).
• Since optimization has to be done on the entire program, the whole program is
examined, and the compiler collects information.
• We will proceed with our discussion with some basic terminologies in data flow
analysis.
1. Definition Point- A definition point is a point in a program that defines a data
item.
2. Reference Point- A reference point is a point in a program that contains a
reference to a data item.
3. Evaluation Point- An evaluation point is a point in a program that contains an
expression to be evaluated.
• The below diagram shows an example of a definition point, a reference point, and an
evaluation point in a program.

Data Flow Analysis Equation:


• The data flow analysis equation is used to collect information about a program block.
The following is the data flow analysis equation for a statement s
Out[s] = gen[s] U In[s] - Kill[s]
Where
Out[s] is the information at the end of the statement s.
gen[s] is the information generated by the statement s.
In[s] is the information at the beginning of the statement s.
Kill[s] is the information killed or removed by the statement s.
• The main aim of the data flow analysis is to find a set of constraints on the In[s]’s and
Out[s]’s for the statement s. The constraints include two types of constraints
1. The transfer function
2. The Control Flow constraint.
GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 10
III BTECH II-SEM, CSE: COMPILER DESIGN
Transfer Function:
• The semantics of the statement are the constraints for the data flow values before and
after a statement.
• For example, consider two statements x = y and z = x. Both these statements are
executed. Thus, after execution, we can say that both x and z have the same value, i.e.
y.
• Thus, a transfer function depicts the relationship between the data flow values before
and after a statement.
• There are two types of transfer functions-
1. Forward propagation
2. Backward propagation
Forward propagation:
• In forward propagation, the transfer function is represented by Fs for any statement s.
• This transfer function accepts the data flow values before the statement and outputs
the new data flow value after the statement.
• Thus the new data flow or the output after the statement will be Out[s] = Fs(In[s]).
Backward propagation:
• The backward propagation is the converse of the forward propagation.
• After the statement, a data flow value is converted to a new data flow value before the
statement using this transfer function.
• Thus the new data flow or the output will be In[s] = Fs(Out[s]).

Control-Flow Constraints:
• The second set of constraints comes from the control flow. The control flow value of
Si will be equal to the control flow values into Si+1 if block B contains statements S1,
S2,........, Sn. That is:
IN[Si + 1] = OUT[Si], for all i = 1 , 2, ....., n – 1.

Data Flow Properties:


• Some properties of the data flow analysis are-
1. Available expression
2. Reaching definition
3. Line variable
Available Expression:
• An expression a + b is said to be available at a program point x if none of its operands
gets modified before their use. It is used to eliminate common sub expressions.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 11


III BTECH II-SEM, CSE: COMPILER DESIGN
• An expression is available at its evaluation point.
Example:

• In the above example, the expression L1: 4 * i is an available expression since this
expression is available for blocks B2 and B3, and no operand is getting modified.

Reaching Definition:
• A definition D is reaching a point x if D is not killed or redefined before that point. It is
generally used in variable/constant propagation.
Example:

• In the above example, D1 is a reaching definition for block B2 since the value of x is
not changed (it is two only) but D1 is not a reaching definition for block B3 because
the value of x is changed to x + 2. This means D1 is killed or redefined by D2.

Live Variable:
• A variable x is said to be live at a point p if the variable's value is not killed or
redefined by some block. If the variable is killed or redefined, it is said to be dead.
• It is generally used in register allocation and dead code elimination.
Example:

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 12


III BTECH II-SEM, CSE: COMPILER DESIGN
• In the above example, the variable a is live at blocks B1, B2, B3 and B4 but is killed at
block B5 since its value is changed from 2 to b + c. Similarly, variable b is live at block
B3 but is killed at block B4.

The foundations of data flow analysis:


• The foundations of data flow analysis in compiler design follow basics concepts:
1. Lattices
2. Reaching Definition
3. Control Flow Graph
4. Iterative Algorithms
Lattices:
• These are partially ordered sets. The sets represent the state of the program during
data flow analysis.
• They perform two operations - join operation and meet operation.
• The set elements' lower bound is calculated in the join operation. In meet operation,
there is a merge of two lattices.
Reaching Definition:
• It determines which definition may reach a specific code point. It tracks the
information flow in a code.
• A definition is said to reach a point such that there has been no variable redefinition.
• Reaching definitions track the variables and the values they hold at different points.
Control Flow Graph:
• It is a graphical representation of the control flow in compiler design. The control
flow refers to the computations during the execution of a program.
• Control flow graphs are process-oriented directed graphs. The control flow graph has
two blocks: the entry and exit.
• It helps to analyze the flow of computations and identify potential performance
loopholes.
Iterative Algorithms:
• Iterative algorithms are used to solve data flow analysis problems. The iterative
algorithms used are for reaching problems and for available expressions.
• Iterative algorithms for reaching problems assume that all definitions reach all
points. It then iteratively solves and updates the reachable points for different
definitions.
• For available expressions, it assumes that all expressions are available at all points. It
then iteratively solves and updates the point of use of the expressions.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 13


III BTECH II-SEM, CSE: COMPILER DESIGN

Constant Propagation:
• Constant Propagation is process of recognizing and evaluating constant expressions
at compile time rather than run time.
Example: Pi=2217
void area_per(int r)
{
float area, perimeter;
area = Pi * r * r;
print area, perimeter;
}
• At line 1, variable Pi is constant and its value 3.413 is assigned at compile time.
• At line 5 and 6 value of Pi is assigned with 3.413 in runtime.

Partial Redundancy Elimination:


• PRE (Partial Redundancy Elimination) is a compiler optimization method. PRE
aims to remove redundant computations from the program.
• Redundant computations are those that are achieved in more than one instance.
However, it produces the same result every time.
• By removing these redundancies, PRE can enhance the overall performance of
the program by reducing the number of instructions carried out.
• An example of full redundancy is given below.
if(condition1) {
x = a + b;
}
else{
x = a + b;
}
• As you can see in the above example, the computation of A+B is fully redundant.
But the condition of partial redundancy arrived if we modified the code as
follows.
if(condition1) {
x = a + b;
}
if(condition2) {
y = a + b;}

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 14


III BTECH II-SEM, CSE: COMPILER DESIGN
• As you can see in the above example, A+B is only partially redundant because it
is performed along some execution path(“given that both conditions are true”)
but not along all execution paths.

Benefits of PRE:
• We can significantly improve the performance of a program by eliminating
partial redundancies.
1. Execution Time: PRE will reduce the number of instructions that are needed
to be executed to run a program. Since our system has fewer instructions to
execute, it will result in a faster execution time.
2. Power Consumption: PRE will reduce the number of instructions that are
needed to be executed, which will save us power and make our system more
power efficient.
3. Readability: PRE will improve the readability of our program by removing
repeated code and redundancies, which will make it easier for the developers
to read and maintain the code.
4. Debug: PRE will remove unwanted or unnecessary code from our code, which
will make debugging easier and enable developers to find and correct any
errors in the code.

PRE algorithms:
There are various algorithms that are available in the market for our use. Some of the
algorithms that are used worldwide are:
1. Lazy Code Motion Algorithm (LCM)
2. Global Value Numbering Algorithm (GVN).
The Lazy Code Motion Algorithm (LCM):
• The Lazy code motion algorithm (LCM) aims to minimize the total number of
computations that is to be performed to execute a program.
• LCM is the algorithm that works by identifying partially redundant expressions.
And then, it will move them to a point in the control flow graph where they are
guaranteed to be executed at most once.
• It is a Four-Step Algorithm:
1. The first step uses anticipation to determine where the expression can be
placed.
2. The second step will be computing the earliest and latest points for each
expression.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 15


III BTECH II-SEM, CSE: COMPILER DESIGN
3. The third step then pushes the cut set down to the point where any further
delay would alter the semantics of the program and may even introduce
redundancy in the program.
4. The final step will be to clean up the pod, and we can do that by removing the
assignment to temporary variables that were used previously.
• Each step should be accomplished with a data flow. The first and fourth are
backward flow programs. At the same time, the 2nd and the 3rd are forward flow
problems.
• Example for LCM:
Before applying LCM:
for (int i = 0; i < n; i++) {
int x = a[i] * b[i];
int y = x + c[i];
sum += y;
}
• in this code, we are calculating the value of ‘x’, only to be used to compute the
value ‘y’ and the value is only used to update the ‘sum’ variable.
• Now we will use lazy code motion. We will move the computation of ‘x’ outside
the loop because it does not depend on the loop variable ‘i’. This will reduce the
number of multiplications performed and also improve the performance of the
code.
• After applying LCM:
int x = 0;
for (int i = 0; i < n; i++) {
x += a[i] * b[i];
int y = x + c[i];
sum += y;
}
• As you can see in the above-modified code, we compute acts only once before the
loop. And then, we will accumulate its value inside the loop. And by doing this,
we will avoid the redundant computation of ‘a[i]*b[i]’ for each iteration of the
loop.

Global Value Numbering Algorithm:


• It is an optimization algorithm utilized during compiling processes and focuses
on identifying redundant computations within programs.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 16


III BTECH II-SEM, CSE: COMPILER DESIGN
• To eliminate operations that do not add any extra value or information, GVN
assigns unique values for any expression representing similar values, henceforth
replacing repetitive calculations with only one computation.
• For maximum efficiency of GVN's application on programs, it operates solely on
their intermediate representations(IR), resulting in advancements in their
performances over time.
• GVN is a Three-Step Algorithm:
1. In the first step, GVN will assign the value number to the expression.
2. In the second step, GVN will identify expressions that are equivalent.
3. In the third and final step, the GVN will replace the redundant expression,
which is equivalent to a single computation.
• Example of GVM:
x = 2 + 3;
y = 4 + 5;
z = x + y;
w = x + y;
Solution:
1. It will assign value numbers to each of the expressions.
x = 2 + 3; // 1
y = 4 + 5; // 2
z = x + y; // 3
w = x + y; // 3
2. Then it will replace the occurrence of identical expressions with their value
number:
x = 2 + 3; // 1
y = 4 + 5; // 2
z = 1 + 2; // 3 (1 + 2)
w = 1 + 2; // 3 (1 + 2)
• As we can see, this algorithm has identified the expression ‘x+y’ in lines 3 and 4
as identical. So it has replaced them both with the value "3”.
• This optimization will reduce the number of computations required to be
performed in the program, which will lead to faster and more efficient
execution of our code.

GEETHANJALI INSTITUTE OF SCIENCE AND TECHNOLOGY, NELLORE Y.v.R 17

You might also like