Unit 5 Code Generation
Unit 5 Code Generation
Definition:
Code optimization is a phase in the compilation process where the compiler tries to improve
the intermediate code so that the final output code is efficient in terms of execution time
and/or memory usage, without altering the functionality of the program.
1. Lexical Analysis
2. Syntax Analysis
3. Semantic Analysis
4. Intermediate Code Generation
5. Code Optimization ← (This stage)
6. Code Generation
7. Linking and Loading
In the code optimization phase, the compiler takes the intermediate representation (IR) of
the source program and applies various transformations to produce a more optimized
version. This optimized IR is then translated into machine code during code generation.
1. Constant Folding:
o Evaluates constant expressions at compile-time.
o Example:
o int x = 3 * 4; // Optimized to: int x = 12;
2. Dead Code Elimination:
o Removes code statements that do not affect the program result.
o Example:
o int x = 10;
o x = 20; // 'x = 10' is dead code and can be removed
3. Loop Optimization:
o Improves efficiency of loops.
o Types:
Loop Invariant Code Motion: Move code that doesn’t change within
the loop to outside.
Loop Unrolling: Reduce the overhead of loop control.
o Example:
o for(int i = 0; i < 10; i++) {
o int x = a * b; // invariant expression
o }
becomes:
int x = a * b;
for(int i = 0; i < 10; i++) {
// use x
}
4. Strength Reduction:
o Replaces costly operations with cheaper equivalents.
o Example:
o x = y * 2; // Replaced by: x = y + y;
5. Common Subexpression Elimination:
o Reuses the result of a previously computed expression.
o Example:
o int a = b * c + d;
o int e = b * c + f;
becomes:
temp = b * c;
int a = temp + d;
int e = temp + f;
6. Peephole Optimization:
o Performs small, localized improvements in machine code.
o Example:
o MOV R1, R2
o MOV R2, R1
Ques 2) Explain different code generation techniques with benefit and example.
Definition:
Code generation is the process of converting the intermediate representation (IR) of the
source code into target machine code (assembly or binary). The main goal is to generate
efficient, correct, and optimized code that executes well on the target hardware.
1. Loop Optimization
Description:
Loop optimization improves the performance of programs by optimizing the way loops are
written or executed. Since loops are repeated many times, optimizing them can significantly
impact the overall performance.
Common Types:
Loop Invariant Code Motion: Moves computations that do not change inside the
loop, outside of it.
Loop Unrolling: Expands the loop body to reduce the number of iterations and loop
control instructions.
Optimized To:
int x = a * b;
for(int i = 0; i < 10; i++) {
arr[i] = x + i;
}
Benefits:
Description:
Registers are the fastest storage in a CPU. This technique tries to store frequently accessed
variables in registers instead of memory for faster access.
Strategy:
Example:
int a = b + c;
int d = a + e;
MOV R1, b
ADD R1, c ; R1 = a
MOV R2, R1
ADD R2, e ; R2 = d
Benefits:
Description:
Stack allocation is used to manage memory for function calls, local variables, parameters,
and return addresses using a LIFO (Last-In-First-Out) structure called the stack.
How it works:
Example (C Code):
Conceptual Stack:
| return address |
| value of b |
| value of a |
| local var |
Benefits:
Supports recursion
Provides memory isolation for each function call
Efficient space management
4. Instruction Selection
Description:
This technique selects the most efficient machine instruction for a given intermediate
operation based on the target architecture.
Example:
Instead of:
x = y * 2;
Benefits:
5. Instruction Scheduling
Description:
Instruction scheduling reorders the machine instructions to reduce stalls or delays due to
hardware issues like pipeline hazards or data dependency.
Example:
If instruction A depends on the result of instruction B, the compiler can insert independent
instructions in between to avoid waiting cycles.
Benefits:
Description:
This technique optimizes complex address calculations, especially in arrays and pointer
arithmetic, to reduce overhead during memory access.
Example:
Accessing an array element:
a[i][j] = x;
Instead of calculating the address again and again, the compiler computes a base address and
applies offset efficiently.
Optimized Form:
Benefits:
Ques 3) Explain the code generation for control flow statement in compiler.
Control flow statements like if, if-else, while, for, and switch allow a program to alter its
execution path based on conditions. In the code generation phase of a compiler, these high-
level constructs are converted into low-level instructions, typically using labels and jump
statements (conditional or unconditional). The goal is to preserve the logic and flow of the
original program in the target machine or assembly code.
1. If Statement
High-Level Code:
if (condition) {
statement;
}
Generated Code:
Evaluate condition
CJUMP_FALSE L1 ; If condition is false, jump to label L1
statement ; Code block if condition is true
L1: Label after the if-block
Explanation:
2. If-Else Statement
High-Level Code:
if (condition) {
statement1;
} else {
statement2;
}
Generated Code:
Evaluate condition
CJUMP_FALSE L1 ; If false, go to else block
statement1 ; Executed if condition is true
JUMP L2 ; Jump to end
L1:
statement2 ; Else block
L2: End of if-else
Explanation:
3. While Loop
High-Level Code:
while (condition) {
body;
}
Generated Code:
Explanation:
4. For Loop
High-Level Code:
Generated Code:
init
L1: Evaluate condition
CJUMP_FALSE L2
body
increment
JUMP L1
L2:
Explanation:
5. Switch Statement
High-Level Code:
switch (expr) {
case 1: statement1; break;
case 2: statement2; break;
default: statement3;
}
Generated Code:
Evaluate expr → R
CMP R, 1
JE L1
CMP R, 2
JE L2
JUMP L3 ; Default case
L1: statement1
JUMP L4
L2: statement2
JUMP L4
L3: statement3 ; Default
L4: Exit point
Explanation:
Ques 4) Explain the properties of code generation phase also explain the design
issue.
The code generation phase is the final phase in the compiler design process. It is responsible
for translating the intermediate representation (IR) of a program into target machine
code, such as assembly or binary instructions. This machine code should be efficient, correct,
and executable by the hardware. The code generation phase bridges the gap between high-
level language constructs and low-level hardware instructions.
A well-designed code generator is critical because poor code generation can drastically
reduce the performance of even well-written programs.
For the code generation phase to be effective, it must fulfill the following essential properties:
1. Correctness
The most fundamental property of code generation is correctness. The machine code
generated must preserve the semantic meaning of the source program. This means the
compiled code must produce the same output and behavior for every valid input as the
original high-level program. Any deviation from the intended behavior is considered a
serious compiler error.
2. Efficiency
Efficiency refers to the performance of the generated code in terms of:
Execution speed
Memory usage
Instruction count
Efficient code minimizes CPU cycles, memory access, and redundant operations. Efficient
register usage, minimal instructions, and reduced branching are all signs of good code
efficiency. This directly affects the runtime of programs and is especially important for real-
time or embedded systems.
The process of code generation should be simple, modular, and easy to implement.
Simplicity ensures that the code generator is:
Maintainable
Understandable
Less prone to errors
A simple design also allows easier integration with other compiler phases such as
optimization or symbol management.
4. Speed of Compilation
The code generation phase must not slow down the entire compilation process. Especially in
large-scale software development or interactive development environments (IDEs), fast
compilation is important for better productivity. Therefore, code generation must be both fast
and efficient, even if it means sacrificing some optimization in simpler compilers.
A good code generation phase should be compatible with optimization techniques. It must
allow:
Loop optimization
Constant folding
Strength reduction
Dead code elimination, etc.
Well-structured intermediate code and data flow analysis can make optimization more
effective during code generation.
Design Issues in Code Generation
Designing the code generation phase involves addressing several important issues and
decisions that affect the final machine code quality and performance.
The input to the code generator is typically intermediate code such as:
The choice of intermediate format greatly affects how easily and effectively code can be
generated. A good IR allows for easy mapping to machine instructions.
A key design issue is the format of the output machine code. It can be:
Most compilers generate assembly code first because it is readable, editable, and allows
debugging before final binary translation.
3. Instruction Selection
Instruction selection involves choosing the appropriate machine instruction for each
intermediate code operation. This selection must consider:
Incorrect or inefficient selection can lead to suboptimal or even faulty machine code.
Registers are limited in number and are the fastest memory locations. A key challenge is to:
Various algorithms like graph coloring or linear scan allocation are used for efficient
register management.
5. Evaluation Order of Instructions
The order in which expressions and statements are evaluated can affect:
For example, evaluating a common subexpression first may reduce redundancy and register
use.
Translating control flow statements such as if, while, for, and switch into jumps, labels, and
branches is another major concern. Efficient control flow code must:
Poor handling of control flow can degrade performance and increase code size.
Local variables
Parameters
Return addresses
Temporaries
Using the stack and activation records, the compiler must manage memory in a structured
way, especially to support function calls and recursion.
Ques 5) Explain code optimization with the concept of machine dependent and
independent code optimization.
Code optimization is the phase of the compiler in which the intermediate code is improved
to make the final target code more efficient in terms of execution speed, memory usage, or
other resources, without altering the program's output or behavior.
Executes faster
Uses fewer instructions
Reduces memory usage
Improves overall program performance
➤ Definition:
Machine-independent optimizations are those that are not tied to the architecture of any
specific hardware. They are applied to the intermediate representation (IR) of the code and
are focused on improving logic and structure.
➤ Features:
➤ Common Techniques:
🔹 Constant Folding:
a = b + c;
d = b + c; → Reuse value of b + c from a
🔹 Dead Code Elimination:
if (false) { x = 5; } // Eliminated
🔹 Loop Invariant Code Motion:
Move calculations outside the loop if they do not change inside the loop.
x = y * 2; → x = y + y;
2. Machine Dependent Optimization
➤ Definition:
Machine-dependent optimizations are applied after target code generation and depend on
the hardware architecture, such as the number of registers, instruction set, and pipeline
design.
➤ Features:
➤ Common Techniques:
🔹 Register Allocation:
Assign frequently used variables to CPU registers instead of memory to improve speed.
🔹 Instruction Scheduling:
🔹 Peephole Optimization:
MOV A, B
MOV B, A → Can be removed or optimized
🔹 Address Mode Optimization:
Comparison Table
Phase applied During intermediate code phase During/after target code generation
This phase directly affects the performance, size, and speed of the generated program.
The compiler first translates source code into an intermediate form (like three-address
code, syntax tree, or DAG).
This IR is machine-independent and needs to be mapped to actual hardware
instructions.
2. Pattern Matching
The code generator compares parts of the IR with predefined instruction patterns
that correspond to machine-level instructions.
Each IR operation is matched with one or more potential machine instructions.
Example:
x=a+b
Could match:
Among the available matching instructions, the compiler chooses the one that:
o Uses fewer CPU cycles
o Consumes less memory
o Requires fewer instructions
Often guided by a cost model that assigns weights to instructions based on execution
time or size.
For expressions with multiple operators, the compiler may generate a sequence of
instructions based on evaluation order and register availability.
After selecting instructions, the compiler emits the machine or assembly code.
The code is further passed through register allocation and possibly machine-
dependent optimization.
The choice of instructions has a significant impact on the performance of the generated
code:
1. Execution Speed
2. Code Size
Compact instructions lead to smaller executables, which are faster to load and use
less memory.
Reducing unnecessary instructions improves overall code density.
3. Register Usage
Efficient instruction selection can reduce the need for extra registers.
Poor selection may increase register pressure, leading to more memory accesses
(slower).
4. Pipeline Performance
Modern CPUs use instruction pipelines. Some instructions cause stalls or hazards.
Instruction selection helps in scheduling operations to minimize pipeline stalls.
5. Power Consumption
x = y * 2;
Control enters at the beginning of the block (only at the first instruction).
Control leaves at the end of the block without any possibility of branching or
stopping in between.
There are no jumps or branch instructions inside the block, except possibly at the
end.
There are no labels (jump targets) inside the block, except at the beginning.
In simple terms, a basic block is a straight-line code sequence with no branches except into
the entry and out of the exit.
Basic blocks are the fundamental units for many compiler optimizations.
Control flow analysis and data flow analysis are easier on basic blocks.
They help in constructing control flow graphs (CFG).
Given intermediate code (e.g., three-address code), the compiler divides it into basic blocks
in the following steps:
Once leaders are identified, each leader marks the start of a new basic block.
The basic block includes the leader and all instructions that follow it until the next
leader or the end of the program.
This means the block contains instructions from the leader up to (but not including)
the next leader.
Continue this process for all leaders to partition the entire intermediate code into a
sequence of basic blocks.
Example
1: a = 5
2: b = 10
3: if a < b goto 7
4: c = a + b
5: goto 8
6: d = c - a
7: e = d * 2
8: print e
1: a = 5
2: b = 10
3: if a < b goto 7
4: c = a + b
5: goto 8
6: d = c - a
7: e = d * 2
8: print e
Identified Leaders:
Block 1: [1, 2, 3]
a=5
b = 10
if a < b goto 7
Block 2: [4, 5]
c=a+b
goto 8
Block 3: [6]
d=c-a
Block 4: [7, 8]
e=d*2
print e
Ques 8) explain the concept of DAG representation of baic block. What is the
optimization of basic block?
1. Introduction
In compiler design, a basic block is a straight-line code sequence with no branches except at
the end. To optimize such a block efficiently, the compiler uses an intermediate
representation called a Directed Acyclic Graph (DAG).
A DAG (Directed Acyclic Graph) is a graph structure with directed edges and no
cycles.
In the context of a basic block, a DAG is used to represent expressions and their
computations.
Each node in the DAG corresponds to either:
o An operator (e.g., +, -, *, /),
o A variable, or
o A constant (leaf nodes).
The edges represent the operands of the operators.
5. Example
1) t1 = a + b
2) t2 = a + b
3) t3 = t1 * c
4) t4 = t2 * c
DAG Construction:
Create leaf nodes for a, b, and c.
Create an operator node for a + b (used in both t1 and t2).
Create an operator node for (a + b) * c (used in t3 and t4).
Nodes for t1 and t2 point to the same (a + b) node.
Nodes for t3 and t4 point to the same ((a + b) * c) node.
Diagram:
(*)
/ \
(+) c
/ \
a b
Points Explanation
Basic Block Optimization Improve code within a block using techniques like common subexpr
Ques 9) Define basic block and flow graph with example. How given
program can be converted into flow graph.
In simple words: It is a straight-line code sequence with no branches in or out except at the
entry and exit points.
A Flow Graph (or Control Flow Graph - CFG) is a directed graph that represents
the flow of control in a program.
Each node in the graph represents a basic block.
A directed edge from one node to another represents possible flow of control from
the first block to the second.
It shows all possible paths that the execution may take through the program.
3. Example Program
1. a = 5;
2. b = 10;
3. if (a < b) goto L1;
4. c = a + b;
5. goto L2;
L1:
6. c = a - b;
L2:
7. print(c);
Summary
Term Definition
Flow Graph Directed graph with basic blocks as nodes and control flow as edges
The runtime environment is dynamically active when the program is running, providing the
context and infrastructure necessary for executing instructions generated by the compiler.
Memory management is a critical aspect of the runtime environment and is primarily divided
into two regions:
The Stack: used for static or temporary memory allocation related to procedure calls.
The Heap: used for dynamic memory allocation during program execution.
3. Stack Allocation
The stack is a contiguous block of memory that functions according to the Last-In-First-
Out (LIFO) principle.
Each function call creates an activation record (or stack frame) that stores:
o Function parameters,
o Local variables,
o Return address (to resume after function execution),
o Saved processor registers.
When a function is invoked, its activation record is pushed onto the stack.
When the function completes, its activation record is popped off, freeing that memory
instantly.
This automatic memory management ensures fast allocation and deallocation.
Limitations:
4. Heap Allocation
The heap is a large, unstructured memory region used for dynamic memory allocation. It
supports objects and data structures whose sizes or lifetimes are not known at compile time.
Memory on the heap is explicitly allocated and freed by the program, using language-
specific operations (e.g., malloc and free in C, new and delete in C++).
Unlike the stack, heap allocation is flexible but slower, due to the need for searching
free blocks and managing fragmentation.
Heap memory is suitable for:
o Objects whose size cannot be determined at compile time.
o Data structures that persist beyond the scope of a single function.
o Complex structures like linked lists, trees, graphs.
Limitations:
The runtime environment manages memory by combining stack allocation for predictable,
temporary data and heap allocation for flexible, dynamic data needs.
Programmer-defined or garbage
Lifetime Limited to function execution
collected