unit 4 new
unit 4 new
Properties of a Good IR
●
● Advantages: Simple and easy to optimize.
●
● Advantages:
○ Clearly represents operator precedence and associativity.
○ Used in syntax analysis and type checking.
●
● Advantages: Simplifies data-flow analysis and enables advanced
optimizations.
4. Bytecode
●
● Advantages: Portable and directly interpretable by virtual machines.
Steps in Intermediate Code Generation
● The compiler parses the source code to create an Abstract Syntax Tree
(AST).
Example:
a = b + c;
AST:
=
/\
a +
/\
b c
2. Semantic Analysis
3. Code Translation
●
Advantages of Using IR
1. Machine Independence:
○ The IR can be reused for different target architectures.
2. Facilitates Optimization:
○ Allows the compiler to perform optimizations like loop unrolling,
constant folding, and dead code elimination.
3. Simplifies Code Generation:
○ IR acts as a middle layer, simplifying the transition from source code
to machine code.
1. Arithmetic Expression
Source Code:
a = b + c * d - e;
TAC:
t1 = c * d
t2 = b + t1
t3 = t2 - e
a = t3
●
2. If-Else Statement
Source Code:
if (a > b) {
c = d + e;
} else {
c = d - e;
}
TAC:
if a > b goto L1
t1 = d - e
c = t1
goto L2
L1: t2 = d + e
c = t2
L2:
3. Loop
Source Code:
for (int i = 0; i < n; i++) {
sum += arr[i];
}
TAC:
i=0
L1: if i >= n goto L2
t1 = arr[i]
sum = sum + t1
i=i+1
goto L1
L2:
●
Conclusion
What is a Declaration?
Definition: A declaration defines a variable or function and associates it with a
data type and optionally an initial value. For example:
int x = 10;
float pi = 3.14;
●
● Purpose: The purpose of declarations is to inform the compiler about the
variable or function's type, scope, memory allocation, and initialization.
Steps in Translation of Declarations
1. Lexical Analysis
● In lexical analysis, the compiler scans the source code to break it into
tokens. These tokens are basic building blocks of the source code, such as
keywords, identifiers, operators, and literals.
Example: For the declaration int x = 5;, the lexical analyzer will produce the
following tokens:
[int], [x], [=], [5], [;]
2. Syntax Analysis
●
● Example:
○ For the declaration int x = 5;, the parser checks if it follows the
grammar:
■ <type> is int
■ <identifier> is x
■ <value> is 5 (optional)
■ The ; indicates the end of the declaration.
3. Semantic Analysis
Example:
int x;
int x; // Error: Duplicate declaration
■
3. Scope Checking: Ensures that variables are declared in the correct
scope (local, global, etc.).
Example: For the declaration int x = 5;, the symbol table entry might look
like:
Name: x
Type: int
Scope: local
Memory Address: 0x1000 (example address)
●
5. Intermediate Code Generation
○
● This allows the compiler to generate a consistent intermediate
representation, which is later optimized or translated into machine-specific
code.
6. Code Generation
● Code generation is the phase where the intermediate code is translated into
machine code or assembly code that the computer can execute.
1. Variable Declarations
Source Code:
int a = 10;
● Translation:
1. Lexical Analysis: Tokens: int, a, =, 10, ;.
2. Syntax Analysis: Matches the grammar <type> <identifier>
[= <value>];.
3. Semantic Analysis: Ensures a is an integer and no previous
declaration of a exists.
Symbol Table:
Name: a, Type: int, Scope: local, Address: 0x1000
4.
Intermediate Code:
DECLARE a, int
ASSIGN a, 10
5.
Machine Code:
MOV R1, 10
STORE R1, 0x1000
6.
2. Array Declarations
Source Code:
int arr[5];
●
● Translation:
1. Lexical Analysis: Tokens: int, arr, [, 5, ], ;.
2. Syntax Analysis: Matches <type> <identifier>[<size>];.
Symbol Table:
Name: arr, Type: array, Size: 20 bytes (5 × 4), Address: 0x2000
3.
Intermediate Code:
DECLARE arr, ARRAY[5] of int
4.
5. Machine Code: Reserves 20 bytes for the array.
3. Function Declarations
Source Code:
int add(int x, int y);
●
● Translation:
1. Lexical Analysis: Tokens: int, add, (, int, x, ,, int, y, ), ;.
2. Syntax Analysis: Matches <return_type>
<function_name>(<parameter_list>);.
Symbol Table:
Name: add, Type: function, Parameters: [x:int, y:int], Return Type: int
3.
Intermediate Code:
DECLARE FUNCTION add(int, int) RETURN int
4.
5. Machine Code: Generated after function definition.
Conclusion
Example:
if (x > 5) {
y = x + 10;
} else {
y = x - 10;
}
○ For the above example, tokens would be: [if], [(], [x],
[>], [5], [)], [{], [y], [=], [x], [+], [10], [;],
[}], [else], [{], [y], [=], [x], [-], [10], [;],
[}]
2. Syntax Analysis: Build a syntax tree to ensure the program follows
correct grammar.
○ The syntax tree would show the structure of the if-else statement.
3. Semantic Analysis: Verify the logic, such as checking whether x and y
are valid variables and if x is correctly compared to 5.
4.
5. Code Generation: Finally, machine code is generated, and the control
flow instructions (like goto and conditional jumps) are translated into
assembly or machine instructions.
Boolean expressions involve logical operations that return either true or false.
These expressions are crucial for decision-making processes in a program, such
as in if statements or while loops.
Boolean Operators:
Example:
if (x > 5 && y < 10) {
z = 1;
}
○
5. Code Generation: The intermediate code is converted to assembly or
machine code with instructions for comparison and logical operations.
A procedure call (or function call) is a statement that causes the program to
jump to another location, execute a block of code (the procedure or function),
and then return to the point where the procedure was called.
Example:
int add(int a, int b) {
return a + b;
}
int main() {
int sum = add(5, 3);
}
For the function call add(5, 3), the intermediate code could be:
t1 = 5
t2 = 3
CALL add, t1, t2
○
5. Code Generation:
1. Optimization:
● Efficiently managing stack frames for function calls, ensuring that local
variables, parameters, and return addresses are stored and retrieved
properly.
● Ensuring that control flow instructions (such as if statements or loops) are
translated into efficient jump or conditional branch instructions.
3. Error Detection:
● Ensuring that the compiler detects and reports errors in control flow,
boolean expressions, and procedure calls.
● For example, if a function is called with incorrect parameters, the compiler
should catch the error during semantic analysis and provide meaningful
error messages.
4. Register Allocation:
● Efficiently assigning registers for procedure calls and intermediate
variables to minimize the number of memory accesses.
Conclusion
1. Loop Optimization
Definition: Loop optimization aims to enhance the efficiency of loops in the code,
as loops are often the most performance-critical part of a program. By optimizing
loops, compilers can reduce the number of iterations, improve memory access
patterns, and eliminate unnecessary computations.
1. Loop Unrolling:
○ What it is: This optimization involves expanding the loop body so
that fewer iterations are required. This reduces the overhead of
looping.
Example:
// Original code
for (int i = 0; i < 4; i++) {
a[i] = b[i] + 10;
}
After loop unrolling:
a[0] = b[0] + 10;
a[1] = b[1] + 10;
a[2] = b[2] + 10;
a[3] = b[3] + 10;
○
○ Benefit: Fewer loop iterations, hence less overhead.
2. Loop Fusion:
○ What it is: Combining adjacent loops that perform similar tasks into
a single loop, reducing the total number of iterations and improving
cache locality.
Example:
// Before loop fusion
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
Example:
// Original while loop
int i = 0;
while (i < n) {
a[i] = b[i] + c[i];
i++;
}
○
○ Benefit: May reduce conditional checks and improve performance.
2. Constant Folding
Example:
int x = 5 * 10;
Example:
int x = 10;
int y = 20;
int z = 30;
int x = 10;
int z = 30;
Benefit: Reduces the size of the program and avoids unnecessary calculations.
● x * y is computed twice.
int temp = x * y;
int a = temp;
int b = temp + 5;
5. Strength Reduction
Example:
// Before strength reduction
for (int i = 0; i < n; i++) {
a[i] = b[i] * 2;
}
Definition: Function inlining replaces a function call with the actual code of the
function, removing the overhead of calling the function and improving execution
speed.
Example:
// Before inlining
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3);
}
// After inlining
int main() {
int result = 5 + 3;
}
Benefit: Reduces function call overhead and increases execution speed, though
at the cost of potentially larger code size.
Definition: This optimization moves code that does not change within a loop
outside the loop, thus reducing the number of times it is executed.
Example:
for (int i = 0; i < n; i++) {
x = y + 10;
a[i] = x * 2;
}
x = y + 10;
for (int i = 0; i < n; i++) {
a[i] = x * 2;
}
Example:
int a = 5;
int b = 10;
int c = a + b;
Before optimization:
9. Constant Propagation
Definition: Constant propagation is an optimization where the compiler
substitutes known constant values for variables during compilation.
Example:
int x = 10;
int y = x + 5;
int y = 10 + 5;
Benefit: Reduces the need for runtime calculations and simplifies the code.
Conclusion
1. Loop Unrolling
How It Works:
● Instead of iterating once for every element, you unroll the loop to perform
multiple operations in one iteration.
Example:
// Original loop
for (int i = 0; i < 4; i++) {
a[i] = b[i] + 10;
}
Unrolled loop:
a[0] = b[0] + 10;
a[1] = b[1] + 10;
a[2] = b[2] + 10;
a[3] = b[3] + 10;
Benefits:
Drawbacks:
● Larger code size due to repetitive code, which can increase instruction
cache misses in some cases.
2. Loop Fusion
Definition: Loop fusion (or loop merging) is the technique of combining two or
more adjacent loops that operate on the same data into a single loop. This
reduces the number of iterations and can improve data locality and reduce
memory access overhead.
How It Works:
● Adjacent loops that work on the same data are merged into one loop,
eliminating redundant processing and reducing the total number of loops in
the code.
Example:
// Before loop fusion
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
Benefits:
Drawbacks:
● In some cases, merging loops can reduce readability and may increase the
loop body size, which could negatively impact instruction cache
performance if the body becomes too large.
How It Works:
● Identify computations in the loop that do not depend on the loop index and
move them outside the loop. This avoids redundant calculations within the
loop and reduces execution time.
Example:
for (int i = 0; i < n; i++) {
x = y + 10; // This computation is invariant within the loop
a[i] = x * 2;
}
Benefits:
Drawbacks:
● May increase code size if too many computations are moved outside the
loop, potentially affecting cache locality.
4. Loop Splitting
Definition: Loop splitting involves dividing a loop into smaller loops to exploit
opportunities for further optimization, such as improving parallelism or reducing
memory access conflicts.
How It Works:
● A single loop is split into smaller sub-loops, which can then be optimized
individually, such as performing different optimizations in parallel.
Example:
// Original loop
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
d[i] = e[i] * f[i];
}
// First loop
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
// Second loop
for (int i = 0; i < n; i++) {
d[i] = e[i] * f[i];
}
Benefits:
Drawbacks:
5. Loop Interchange
How It Works:
// Interchanged loops
for (int j = 0; j < m; j++) {
for (int i = 0; i < n; i++) {
a[i][j] = b[i][j] + c[i][j];
}
}
Benefits:
Drawbacks:
● Loop interchange is only beneficial when the data access pattern aligns
with the optimization goals. It may not always improve performance.
6. Software Pipelining
How It Works:
● Each loop iteration is divided into multiple stages, and different stages from
consecutive iterations are executed simultaneously, improving CPU
utilization.
Example:
// Original code
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
Benefits:
Drawbacks:
How It Works:
● Rather than performing multiplication inside a loop, the compiler replaces it
with an incrementing addition, which is computationally less expensive.
Example:
// Before strength reduction
for (int i = 0; i < n; i++) {
a[i] = b[i] * 2;
}
Benefits:
Drawbacks:
Conclusion
DAGs are widely used in many applications like scheduling tasks, representing
expressions in compilers, and performing optimization operations.
a = (b + c) * (d + e)
*
/\
+ +
/\ /\
b cd e
*
/\
/ \
+ +
/\ /\
b cd e
\ / \
(b+c) (d+e)
In this DAG:
a. Reduces Redundancy:
Let’s take a more complex example and show how DAGs can help optimize
code.
Expression:
a = (b + c) * (d + e) + (b + c) * f
+
/\
* *
/\/\
+ f + c
/\ /\
b c b c
+
/\
* *
/\/\
+ f * c
/\ /\
b c b c
\ /
(b+c) (b+c)
Step 3: Optimization
In the DAG:
With the DAG representation, the compiler can generate optimized intermediate
code:
t1 = b + c // Compute b + c
t2 = d + e // Compute d + e
t3 = t1 * t2 // Multiply (b + c) * (d + e)
t4 = t1 * f // Multiply (b + c) * f
a = t3 + t4 // Add the results
DAGs are also useful in optimizing loops. For example, in loop optimization,
DAGs can help identify operations that can be moved outside the loop (like loop
invariant code motion) or determine which operations can be shared between
loop iterations to reduce redundant calculations.
*
/\
+ d
/\
b c
(b+c)
In this case:
Let’s go through data-flow analysis in detail, covering its principles, types, and
how it’s used in compilers.
Through the analysis of how data flows through the program, the compiler can
generate more efficient machine code, minimize memory access, and optimize
control structures like loops and conditional branches.
Once the CFG is constructed, data-flow analysis is applied to track data at each
node in the graph.
4. Types of Data-Flow Analysis
There are several types of data-flow analysis that a compiler may use, depending
on the optimization goals and the specific analysis being performed. These
include:
a. Reaching Definitions
x = 5;
y = x + 1;
Example:
x = 5;
y = x + 1;
● After the assignment x = 5, x is considered live because it is used in the
computation y = x + 1.
Live variable analysis helps the compiler optimize register allocation and reduce
the number of variables that need to be stored in memory.
c. Available Expressions
Example:
x = a + b;
y = a + b;
d. Constant Propagation
Example:
x = 5;
y = x + 2;
● Dead code elimination involves removing code that does not affect the
program’s output.
● A variable or expression is considered dead if its value is never used or if it
is overwritten before being used.
Example:
x = 5;
y = 10;
z = x + y;
x = 6; // x is re-assigned before being used again
Dead code elimination helps in reducing the size of the program and improving
performance.
○ Initial data flow values (e.g., for reaching definitions, live variables,
etc.) are set for each basic block. This often involves setting initial
values for variables, such as assuming that no variables are live at
the start.
3. Propagation of Data:
○ After convergence, the final results of the analysis are used for
further optimization (e.g., removing dead code, reusing expressions,
etc.).
int x, y, z;
x = 5;
y = x + 1;
z = y + 2;
1. Block 1: x = 5;
2. Block 2: y = x + 1;
3. Block 3: z = y + 2;
● Initial Definitions:
○ Block 1 defines x.
○ Block 2 defines y.
○ Block 3 defines z.
● Flow Propagation:
7. Conclusion
Let’s delve into the issues in code generation, explaining the major challenges
faced by compilers during this phase.
The first challenge faced during code generation is dealing with the target
architecture and instruction set of the machine on which the code will run.
Different processors and platforms have different instruction sets, registers,
addressing modes, and capabilities.
Challenges:
Example:
● On an x86 processor, you might have registers like eax, ebx, etc., which
are specific to that architecture.
● On an ARM processor, you would have a different set of registers (r0,
r1, etc.), each optimized for specific tasks.
2. Instruction Selection
Challenges:
Example:
If you need to perform an addition in the source code, the compiler must
determine which machine instruction corresponds to an addition on the target
machine:
Challenges:
Example:
Challenges:
● Memory Access Patterns: The compiler must decide how to access data in
arrays, structures, and other memory locations. It must choose efficient
ways to load/store data, considering the available addressing modes.
● Alignment: Some architectures require data to be aligned in memory in
certain ways (e.g., 4-byte boundaries for integers). Misaligned memory
accesses can cause performance degradation or even runtime errors.
● Stack Management: The compiler must manage the stack properly for
function calls, local variables, and return values. The stack must be
allocated, deallocated, and accessed in a manner that is both efficient and
correct.
Example:
● x86: Using an indexed addressing mode, like MOV eax, [ebx + ecx *
4] (where ebx contains the base address of the array, and ecx is the
index).
● ARM: Using an offset-based addressing mode, like LDR r0, [r1, r2,
LSL #2] (where r1 is the base address and r2 is the index).
Control flow in programs involves conditionals, loops, and function calls. The
compiler must translate high-level control structures into machine code that
works efficiently on the target architecture.
Challenges:
Example:
● The compiler might choose to unroll the loop for optimization, converting
it into multiple iterations to reduce the overhead of checking the loop
condition and updating the index variable:
a[0] = b[0] + 1;
a[1] = b[1] + 1;
// More unrolled iterations...
6. Instruction Scheduling
Challenges:
Example:
7. Peephole Optimization
Challenges:
● Identifying Redundant Code: The compiler must analyze the code to find
redundant operations or inefficient instruction sequences.
● Minimizing the Impact on Performance: Even small changes can have a
large impact on the overall performance, so the compiler must be careful to
avoid introducing unnecessary changes that might degrade performance.
Example:
MOV R1, 5
ADD R2, R1, R1 ; R2 = R1 + R1
8. Conclusion
1. A Machine Model
● Instruction Set Architecture (ISA): This specifies the set of instructions that
the machine can understand and execute (e.g., ADD, MOV, SUB).
● Registers: The set of registers available for storing temporary data.
Registers are much faster than memory, so efficient use of registers is a
critical part of code generation.
● Memory Layout: The way memory is organized (stack, heap, global
memory, etc.).
● Addressing Modes: Ways to access data in memory (e.g., direct
addressing, indirect addressing, indexed addressing).
Example:
Example:
a=b+c
Challenges:
Example:
a=b+c
d=a*e
The code generator might assign:
Here, eax, ebx, and ecx are registers used to hold intermediate values.
Example:
a=b+c*d
+
/\
b *
/\
c d
Example:
Another example:
All of these aspects work together to generate efficient machine code that runs
on the target hardware, ensuring that the compiled program performs well and
operates correctly.