You are on page 1of 34

System programs are a crucial component of a computer's operating system, responsible for managing

and controlling the hardware and software resources of a computer system. These programs facilitate
the execution of application programs and ensure the overall functionality and stability of the system.
Here's an introduction to system programs:

### Definition:

System programs, also known as system software, are a set of programs designed to manage and
control the operation of a computer system. They provide essential services to both the users and the
application programs running on the computer.

### Functions of System Programs:

1. **Operating System Kernel:**

- The core component of system software is the operating system kernel. It interacts directly with the
hardware and provides essential services such as process management, memory management, file
system management, and device management.

2. **Process Management:**

- System programs handle the creation, scheduling, and termination of processes. They ensure that
each process gets the required resources and executes efficiently.

3. **Memory Management:**

- Memory management system programs control and coordinate the allocation and deallocation of
memory space for processes. They also handle virtual memory, ensuring efficient use of available
resources.

4. **File System Management:**

- These programs manage file creation, deletion, reading, and writing. They organize and maintain the
structure of the file system, allowing users and applications to store and retrieve data.

5. **Device Drivers:**

- System programs include device drivers, which are specialized software components responsible for
communication between the operating system and hardware devices. They enable the operating system
to control various peripherals.

6. **Security and Protection:**

- System programs implement security measures to protect the system and user data. This includes
user authentication, access control, and encryption.

7. **User Interface:**

- User interfaces, such as command-line interfaces (CLI) or graphical user interfaces (GUI), are part of
system programs. They allow users to interact with the computer and execute commands or launch
applications.
8. **Utilities:**

- System programs often include utility programs that perform specific tasks, such as disk cleanup,
antivirus scanning, and system diagnostics.

### Examples of System Programs:

1. **Operating Systems:**

- Examples include Windows, macOS, Linux, and Unix.

2. **Device Drivers:**

- Drivers for printers, graphics cards, network adapters, etc.

3. **Utilities:**

- Disk Cleanup, Task Manager, Disk Defragmenter, etc.

4. **User Interfaces:**

- Command prompt (CLI), Windows Explorer (GUI), Finder (macOS), etc.

### Conclusion:

System programs play a crucial role in ensuring the smooth and efficient operation of computer systems.
They provide a layer of abstraction between the hardware and application programs, simplifying the
development and execution of software while managing the complexities of the underlying hardware.

Linkers are software tools that play a critical role in the process of converting source code written in a
programming language into executable code that can be run on a computer. They are part of the
software development toolchain and are specifically involved in the final stages of program compilation.
Here's an overview of linkers:

### Definition:
A linker is a program that combines one or more object files generated by a compiler into a single
executable program. It resolves references between files, addresses, and data, ensuring that all the
pieces fit together correctly to create a runnable program.

### Functions of Linkers:

1. **Symbol Resolution:**

- Linkers resolve symbolic references between different object files. Symbols are names or addresses
of memory locations used in the program, and the linker ensures that references to these symbols are
correctly connected.

2. **Address Binding:**

- Linkers assign final memory addresses to program variables and instructions. This process can involve
adjusting the addresses of symbols to reflect their actual locations in memory.

3. **Code and Data Combining:**

- Linkers combine code and data from multiple object files into a single executable file. This includes
merging functions, data structures, and other program components.

4. **Library Linking:**

- Linkers can incorporate external libraries into the executable. They resolve references to functions or
procedures defined in libraries, linking only the necessary parts of the library to the program.

5. **Relocation:**

- Linkers perform relocation, adjusting the addresses of instructions and data to reflect the final
memory layout of the program. This is crucial for ensuring that the program runs correctly regardless of
its loading address.

6. **Dynamic Linking:**

- Some linkers support dynamic linking, where certain parts of the program can be loaded into
memory at runtime. This allows for shared libraries and more efficient memory usage.
### Types of Linkers:

1. **Static Linkers:**

- Static linkers combine all necessary code and libraries into a single executable file before the program
runs. The resulting executable is self-contained and does not depend on external files during runtime.

2. **Dynamic Linkers:**

- Dynamic linkers link certain portions of the program at runtime. This can reduce memory footprint
and allow for the sharing of libraries among multiple programs.

### Example of Linking Process:

1. **Compilation:**

- Source code is compiled into object files (.obj or .o) by the compiler.

2. **Linking:**

- The linker combines object files, resolves symbols, and generates the final executable file.

3. **Loading:**

- The operating system loader loads the executable into memory for execution.

### Conclusion:

Linkers are a crucial part of the software development process, ensuring that individual components of a
program are correctly connected and that the program can be executed as intended. They contribute to
the creation of efficient, standalone executables and facilitate the use of external libraries in software
development.

Loaders are software components responsible for loading executable files into the computer's memory
so that they can be executed by the operating system. The loading process involves bringing the
compiled program into memory and preparing it for execution. Loaders are an integral part of the
overall process of running a program. Here's an overview of loaders:
### Definition:

A loader is a program or part of the operating system that loads executable files into memory for
execution. It is responsible for preparing the program for runtime by resolving addresses and setting up
the necessary data structures.

### Functions of Loaders:

1. **Address Binding:**

- Loaders perform address binding, which involves assigning final memory addresses to the program's
variables and instructions. This is necessary because the addresses specified in the compiled program
may not be the final addresses used in memory.

2. **Loading into Memory:**

- Loaders bring the executable file from storage (such as a disk) into the computer's memory (RAM).
This is a crucial step in the process of executing a program.

3. **Symbol Resolution:**

- Loaders resolve symbolic references in the program. Symbols are names or addresses that were left
as placeholders during the linking process. The loader assigns the correct addresses to these symbols.

4. **Relocation:**

- Loaders perform relocation, adjusting the addresses of instructions and data to reflect the actual
memory locations where the program is loaded. This ensures that the program can run correctly
regardless of its loading address.

5. **Dynamic Linking:**

- Some loaders support dynamic linking, allowing certain parts of a program to be loaded into memory
at runtime. This is common in systems that use shared libraries.

6. **Loading External Libraries:**


- Loaders can load external libraries into memory if the program relies on functions or procedures
defined in these libraries. This is especially important for programs that use dynamic linking.

### Types of Loaders:

1. **Absolute Loaders:**

- Assign fixed memory addresses to the program and load it without modification. These loaders are
less flexible and may lead to issues if the specified memory locations are already occupied.

2. **Relocating Loaders:**

- Adjust the program's addresses during the loading process, allowing it to be loaded into any area of
memory without modification.

3. **Dynamic Loaders:**

- Load portions of a program into memory on-demand during runtime. This is common in systems that
support dynamic linking.

### Loading Process:

1. **Loading:**

- The loader reads the executable file from storage, typically a disk or other non-volatile storage.

2. **Address Binding:**

- The loader performs address binding, adjusting addresses to reflect the program's actual memory
layout.

3. **Relocation:**

- The loader adjusts addresses and resolves symbolic references to ensure the program can run
correctly in the allocated memory space.

4. **Transfer of Control:**
- The loader transfers control to the starting point of the program, initiating its execution.

### Conclusion:

Loaders are essential components in the execution of programs, ensuring that executable files are
properly loaded into memory and prepared for execution. They play a crucial role in address resolution,
relocation, and overall system efficiency.

MACROS

In the context of compiler design, macros refer to a mechanism that allows programmers to
define reusable code snippets or abstractions, which are expanded by the preprocessor before
the actual compilation process begins. Macros enhance code readability, maintainability, and
provide a way to create generic and parameterized code. There are two main types of macros in
the context of compiler design: object-like macros and function-like macros.

Compiler, interpreter, and assembler are all tools used in the process of converting high-level
programming languages into machine code or an executable form. However, they perform different
tasks and operate at different stages of the software development process.

### Compiler:

1. **Definition:**

- A compiler is a program that translates the entire source code of a high-level programming language
into machine code or an intermediate code all at once.

2. **Functionality:**

- The compilation process involves scanning the entire source code, parsing it, and generating an
intermediate code or machine code. The output is a standalone executable file that can be run
independently of the original source code.

3. **Output:**

- The output of a compiler is typically an executable file or binary code that can be directly executed by
the computer's hardware.

4. **Examples:**
- GCC (GNU Compiler Collection), Clang (C Language Family Frontend), Microsoft Visual C++ Compiler.

5. **Advantages:**

- Compiled code tends to be faster in execution as the translation is done before execution.

- Compilation optimizes the code for the target machine, potentially leading to better performance.

### Interpreter:

1. **Definition:**

- An interpreter is a program that reads and executes the source code of a high-level programming
language line by line, without generating an intermediate code or a standalone executable.

2. **Functionality:**

- Interpreters translate and execute source code directly, interpreting and executing each statement or
line at runtime.

3. **Output:**

- There is no separate compilation step. The source code is executed directly, and the output is the
result of the code execution.

4. **Examples:**

- Python interpreter, JavaScript engines (e.g., V8 for Node.js), Ruby interpreter.

5. **Advantages:**

- Interpreted code is generally more portable, as it doesn't rely on a specific target machine
architecture.

- Easier debugging since errors are reported at the point of execution.

### Assembler:
1. **Definition:**

- An assembler is a program that translates assembly language code into machine code or object code.

2. **Functionality:**

- Assembly language is a low-level programming language that is a human-readable representation of


machine code. The assembler translates assembly code into the corresponding machine code.

3. **Output:**

- The output of an assembler is typically an object file containing machine code in a format specific to
the target architecture.

4. **Examples:**

- NASM (Netwide Assembler), MASM (Microsoft Macro Assembler), GAS (GNU Assembler).

5. **Advantages:**

- Provides a way to write programs at a lower level than high-level languages while still being more
human-readable than machine code.

- Allows for direct control over hardware, memory, and registers.

### Summary:

- **Compiler:** Translates entire high-level source code into machine code or an intermediate code at
once, producing an executable file.

- **Interpreter:** Executes high-level source code line by line without generating a standalone
executable. Code is translated and executed at runtime.

- **Assembler:** Translates assembly language code into machine code or object code, allowing for
low-level programming close to machine instructions.
In practice, many modern programming languages use a combination of these tools. For example, Java
code is compiled to an intermediate bytecode by the Java compiler and then interpreted by the Java
Virtual Machine (JVM) at runtime.

LOADING SCHEMES:

Loading schemes refer to the methods and strategies used by operating systems to load programs into
memory for execution. Loading is an essential step in the execution of a program, as it involves bringing
the program code and data into the computer's memory, making it accessible for the CPU. Different
loading schemes are designed to optimize memory usage, execution efficiency, and the overall
performance of the system. Here are some common loading schemes:

### 1. **Absolute Loading:**

- **Definition:** In absolute loading, the entire program is loaded into a predetermined memory
location. The memory addresses in the program code are fixed during the compilation process.

- **Advantages:** Simple and straightforward.

- **Disadvantages:** Limits the ability to run multiple programs concurrently and can lead to
fragmentation.

### 2. **Relocatable Loading:**

- **Definition:** Relocatable loading allows a program to be loaded into any available area of
memory. The compiler generates code with relative addresses, and the loader adjusts these addresses
based on the actual loading location.

- **Advantages:** Supports loading multiple programs simultaneously and reduces fragmentation.

- **Disadvantages:** Requires additional processing to adjust addresses during loading.

### 3. **Dynamic Loading:**

- **Definition:** Dynamic loading allows a program to load specific modules or functions into memory
only when they are required during execution. Unused portions of the program are not loaded until
needed.

- **Advantages:** Efficient use of memory, faster program startup, and reduced memory footprint.

- **Disadvantages:** Increased complexity in managing dynamically loaded modules.

### 4. **Dynamic Linking:**


- **Definition:** Dynamic linking is a form of dynamic loading where a program's modules or libraries
are linked at runtime rather than during the compilation or linking phase. This is common in shared
libraries.

- **Advantages:** Efficient use of memory, shared libraries can be updated without recompiling all
programs.

- **Disadvantages:** Slight runtime overhead due to resolving symbols dynamically.

### 5. **Static Linking:**

- **Definition:** Static linking occurs during the compilation phase, where library code is combined
with the program's code to create a single executable file. The linker resolves external references at this
stage.

- **Advantages:** Faster execution, no runtime linking overhead.

- **Disadvantages:** Each program has a separate copy of the library code, potentially leading to
larger executable sizes.

### 6. **Overlay Loading:**

- **Definition:** Overlay loading is a technique used in systems with limited memory. Only the
portions of a program needed for the current execution phase are loaded, and different overlays replace
each other as needed.

- **Advantages:** Allows running programs that are larger than available memory.

- **Disadvantages:** Complex management of overlay swapping, potential performance overhead.

### 7. **Demand Paging:**

- **Definition:** Demand paging is commonly used in virtual memory systems. It brings only the
necessary pages of a program into memory as they are referenced during execution, rather than loading
the entire program at once.

- **Advantages:** Efficient use of virtual memory, reduced startup time.

- **Disadvantages:** May incur page faults, leading to occasional delays during execution.

These loading schemes are part of the broader memory management strategies employed by operating
systems to optimize the use of available resources and enhance the overall performance of computer
systems. The choice of a particular loading scheme depends on factors such as system architecture,
memory constraints, and the desired balance between speed and efficiency.
Compiler Writing Tools:

The compiler writer can use some specialized tools that help in implementing
various phases of a compiler. These tools assist in the creation of an entire
compiler or its parts. Some commonly used compiler construction tools include:
1. Parser Generator – It produces syntax analyzers (parsers) from the input
that is based on a grammatical description of programming language or on a
context-free grammar. It is useful as the syntax analysis phase is highly
complex and consumes more manual and compilation time. Example: PIC,
EQM

2. Scanner Generator – It generates lexical analyzers from the input that


consists of regular expression description based on tokens of a language. It
generates a finite automaton to recognize the regular expression. Example:
Lex

3. Syntax directed translation engines – It generates intermediate code with


three address format from the input that consists of a parse tree. These
engines have routines to traverse the parse tree and then produces the
intermediate code. In this, each node of the parse tree is associated with
one or more translations.
4. Automatic code generators – It generates the machine language for a
target machine. Each operation of the intermediate language is translated
using a collection of rules and then is taken as an input by the code
generator. A template matching process is used. An intermediate language
statement is replaced by its equivalent machine language statement using
templates.
5. Data-flow analysis engines – It is used in code optimization.Data flow
analysis is a key part of the code optimization that gathers the information,
that is the values that flow from one part of a program to another. Refer
– data flow analysis in Compiler
6. Compiler construction toolkits – It provides an integrated set of routines
that aids in building compiler components or in the construction of various
phases of compiler.

Features of compiler construction tools :

Lexical Analyzer Generator: This tool helps in generating the lexical analyzer
or scanner of the compiler. It takes as input a set of regular expressions that
define the syntax of the language being compiled and produces a program that
reads the input source code and tokenizes it based on these regular
expressions.
Parser Generator: This tool helps in generating the parser of the compiler. It
takes as input a context-free grammar that defines the syntax of the language
being compiled and produces a program that parses the input tokens and builds
an abstract syntax tree.
Code Generation Tools: These tools help in generating the target code for the
compiler. They take as input the abstract syntax tree produced by the parser
and produce code that can be executed on the target machine.
Optimization Tools: These tools help in optimizing the generated code for
efficiency and performance. They can perform various optimizations such as
dead code elimination, loop optimization, and register allocation.
Debugging Tools: These tools help in debugging the compiler itself or the
programs that are being compiled. They can provide debugging information
such as symbol tables, call stacks, and runtime errors.
Profiling Tools: These tools help in profiling the compiler or the compiled code
to identify performance bottlenecks and optimize the code accordingly.
Documentation Tools: These tools help in generating documentation for the
compiler and the programming language being compiled. They can generate
documentation for the syntax, semantics, and usage of the language.
Language Support: Compiler construction tools are designed to support a
wide range of programming languages, including high-level languages such as
C++, Java, and Python, as well as low-level languages such as assembly
language.
Cross-Platform Support: Compiler construction tools may be designed to work
on multiple platforms, such as Windows, Mac, and Linux.
User Interface: Some compiler construction tools come with a user interface
that makes it easier for developers to work with the compiler and its associated
tools.
Finite Autamata:
Finite Automata(FA) is the simplest machine to recognize patterns.It is used to
characterize a Regular Language, for example: /baa+!/.
Also it is used to analyze and recognize Natural language Expressions. The
finite automata or finite state machine is an abstract machine that has five
elements or tuples. It has a set of states and rules for moving from one state to
another but it depends upon the applied input symbol. Based on the states and
the set of rules the input string can be either accepted or rejected. Basically, it
is an abstract model of a digital computer which reads an input string and
changes its internal state depending on the current input symbol. Every
automaton defines a language i.e. set of strings it accepts. The following figure
shows some essential features of general automation.

Figure: Features of Finite Automata

The above figure shows the following features of automata:


1. Input
2. Output
3. States of automata
4. State relation
5. Output relation
A Finite Automata consists of the following:
Q : Finite set of states.
Σ : set of Input Symbols.
q : Initial state.
F : set of Final States.
δ : Transition Function.
Formal specification of machine is
{ Q, Σ, q, F, δ }
FA is characterized into two types:
1) Deterministic Finite Automata (DFA):
DFA consists of 5 tuples {Q, Σ, q, F, δ}.
Q : set of all states.
Σ : set of input symbols. ( Symbols which machine takes as input )
q : Initial state. ( Starting state of a machine )
F : set of final state.
δ : Transition Function, defined as δ : Q X Σ --> Q.
In a DFA, for a particular input character, the machine goes to one state only. A
transition function is defined on every state for every input symbol. Also in DFA
null (or ε) move is not allowed, i.e., DFA cannot change state without any input
character.
For example, construct a DFA which accept a language of all strings ending
with ‘a’.
Given: Σ = {a,b}, q = {q0}, F={q1}, Q = {q0, q1}
First, consider a language set of all the possible acceptable strings in order to
construct an accurate state transition diagram.
L = {a, aa, aaa, aaaa, aaaaa, ba, bba, bbbaa, aba, abba, aaba, abaa}
Above is simple subset of the possible acceptable strings there can many other
strings which ends with ‘a’ and contains symbols {a,b}.

Fig 1. State Transition Diagram for DFA with Σ = {a, b}

Strings not accepted are,


ab, bb, aab, abbb, etc.
State transition table for above automaton,

\Symbol⇢
⇣State a b

q0 q1 q0

q1 q1 q0

One important thing to note is, there can be many possible DFAs for a
pattern. A DFA with a minimum number of states is generally preferred.
2) Nondeterministic Finite Automata(NFA): NFA is similar to DFA except
following additional features:
1. Null (or ε) move is allowed i.e., it can move forward without reading
symbols.
2. Ability to transmit to any number of states for a particular input.
However, these above features don’t add any power to NFA. If we compare
both in terms of power, both are equivalent.
Due to the above additional features, NFA has a different transition function, the
rest is the same as DFA.
δ: Transition Function
δ: Q X (Σ U ε ) --> 2 ^ Q.
As you can see in the transition function is for any input including null (or ε),
NFA can go to any state number of states. For example, below is an NFA for
the above problem.

Fig 2. State Transition Diagram for NFA with Σ = {a, b}

State Transition Table for above Automaton,

\Symbol⇢
⇣State a b

q0 {q0,q1} q0

q1 ∅ ∅

One important thing to note is, in NFA, if any path for an input string leads to
a final state, then the input string is accepted. For example, in the above
NFA, there are multiple paths for the input string “00”. Since one of the paths
leads to a final state, “00” is accepted by the above NFA.
Some Important Points:
 Justification:
Since all the tuples in DFA and NFA are the same except for one of the tuples,
which is Transition Function (δ)
In case of DFA
δ : Q X Σ --> Q
In case of NFA
δ : Q X Σ --> 2Q
Now if you observe you’ll find out Q X Σ –> Q is part of Q X Σ –> 2 Q.
On the RHS side, Q is the subset of 2 Q which indicates Q is contained in 2 Q or Q
is a part of 2Q, however, the reverse isn’t true. So mathematically, we can
conclude that every DFA is NFA but not vice-versa. Yet there is a way to
convert an NFA to DFA, so there exists an equivalent DFA for every NFA.

1. Both NFA and DFA have the same power and each NFA can be translated
into a DFA.
2. There can be multiple final states in both DFA and NFA.
3. NFA is more of a theoretical concept.
4. DFA is used in Lexical Analysis in Compiler.
5. If the number of states in the NFA is N then, its DFA can have maximum
2N number of states.

In compiler design, optimization refers to the process of improving the performance of a program by
transforming its source code or intermediate code in such a way that it produces the same output but
executes more efficiently. The goal of optimization is to make the generated machine code or
intermediate representation run faster, use less memory, or consume fewer resources.

Compiler optimization can occur at various stages of the compilation process, including:

1. **Source Code Level:** Some optimizations can be performed directly on the source code before it is
translated into machine code. These may include algebraic simplifications, loop transformations, and
other high-level code transformations.

2. **Intermediate Code Level:** Compilers often generate an intermediate representation of the source
code before translating it into machine code. Optimizations can be applied at this intermediate level to
improve the efficiency of the generated code.

3. **Machine Code Level:** After the intermediate code has been generated, the compiler translates it
into machine code for a specific target architecture. Low-level optimizations, such as instruction
scheduling, register allocation, and code motion, can be applied at this stage.

Some common types of compiler optimizations include:

- **Loop Optimization:** Improving the efficiency of loops by unrolling, vectorizing, or parallelizing


them.

- **Constant Folding:** Evaluating constant expressions at compile time rather than runtime.
- **Dead Code Elimination:** Removing code that will never be executed, improving both space and
time efficiency.

- **Inlining:** Inserting the code of a called function directly into the calling function to reduce the
overhead of function calls.

- **Register Allocation:** Efficiently assigning variables to processor registers to minimize memory


access.

- **Common Subexpression Elimination:** Identifying and eliminating redundant computations.

- **Code Motion:** Moving computations or instructions to a more optimal location within the code.

Optimization is a complex and challenging aspect of compiler design because it involves making trade-
offs between different factors such as execution speed, code size, and memory usage. The effectiveness
of optimizations can vary depending on the characteristics of the program and the target architecture.
Compiler designers aim to implement optimizations that provide a good balance between improved
performance and the cost of compilation.

Loop optimization is a crucial aspect of compiler design aimed at improving the performance of loops
within a program. Since loops often represent a significant portion of a program's execution time,
optimizing them can lead to substantial overall performance gains. Various techniques are employed for
loop optimization, and these optimizations can be applied at different stages of the compilation process.
Here are some common loop optimization techniques:

1. **Loop Unrolling:**

- **Description:** Loop unrolling involves replicating the body of a loop multiple times, reducing the
overhead of loop control instructions.

- **Benefits:** Increases instruction-level parallelism, reduces loop control overhead, and allows
better utilization of hardware pipelines.

- **Considerations:** May lead to increased code size; optimal unrolling factor depends on the
specific architecture and application.

2. **Loop Fusion:**
- **Description:** Merging multiple loops into a single loop to reduce loop overhead and improve
data locality.

- **Benefits:** Reduces loop control instructions and enhances cache performance by accessing data
sequentially.

- **Considerations:** Requires careful analysis to ensure that fusion does not introduce dependencies
or negatively impact performance.

3. **Loop-Invariant Code Motion:**

- **Description:** Identifying and moving computations outside the loop if their values remain
constant throughout the loop execution.

- **Benefits:** Reduces redundant computations within the loop, improving overall performance.

- **Considerations:** Care must be taken to ensure that moving code outside the loop does not affect
correctness or introduce additional dependencies.

4. **Loop Tiling (Loop Nesting):**

- **Description:** Dividing a loop into smaller blocks (tiles) to improve cache locality and reduce
memory access latency.

- **Benefits:** Enhances data reuse by working on a smaller data set at a time, improving cache
efficiency.

- **Considerations:** Requires careful analysis of loop dependencies and memory access patterns.

5. **Vectorization:**

- **Description:** Exploiting SIMD (Single Instruction, Multiple Data) capabilities of modern processors
to process multiple data elements simultaneously.

- **Benefits:** Increases parallelism by performing operations on multiple data elements with a single
instruction.

- **Considerations:** Requires data-level parallelism in the loop; compilers may need hints or
annotations for effective vectorization.

6. **Parallelization:**

- **Description:** Identifying loops that can be executed concurrently on multiple processors or cores.

- **Benefits:** Exploits multi-core architectures to achieve parallel execution, potentially reducing


overall execution time.
- **Considerations:** Requires careful analysis of dependencies and possible data races; may not be
suitable for all loops.

Loop optimization is often an intricate process that involves trade-offs between factors such as code
size, execution speed, and memory usage. The effectiveness of loop optimizations depends on the
specific characteristics of the loop, the underlying hardware architecture, and the compiler's ability to
analyze and transform the code. Compiler designers employ a combination of these techniques to
achieve optimal performance for loops in a given program.

Directed Acyclic Graph :


The Directed Acyclic Graph (DAG) is used to represent the structure of basic
blocks, to visualize the flow of values between basic blocks, and to provide
optimization techniques in the basic block. To apply an optimization technique
to a basic block, a DAG is a three-address code that is generated as the result
of an intermediate code generation.
 Directed acyclic graphs are a type of data structure and they are used to
apply transformations to basic blocks.
 The Directed Acyclic Graph (DAG) facilitates the transformation of basic
blocks.
 DAG is an efficient method for identifying common sub-expressions.
 It demonstrates how the statement’s computed value is used in subsequent
statements.
Examples of directed acyclic graph :
Directed Acyclic Graph Characteristics :
A Directed Acyclic Graph for Basic Block is a directed acyclic graph with the
following labels on nodes.
 The graph’s leaves each have a unique identifier, which can be variable
names or constants.
 The interior nodes of the graph are labelled with an operator symbol.
 In addition, nodes are given a string of identifiers to use as labels for storing
the computed value.
 Directed Acyclic Graphs have their own definitions for transitive closure and
transitive reduction.
 Directed Acyclic Graphs have topological orderings defined.
Algorithm for construction of Directed Acyclic Graph :
There are three possible scenarios for building a DAG on three address codes:
Case 1 – x = y op z
Case 2 – x = op y
Case 3 – x = y
Directed Acyclic Graph for the above cases can be built as follows :
Step 1 –
 If the y operand is not defined, then create a node (y).
 If the z operand is not defined, create a node for case(1) as node(z).
Step 2 –
 Create node(OP) for case(1), with node(z) as its right child and node(OP) as
its left child (y).
 For the case (2), see if there is a node operator (OP) with one child node (y).
 Node n will be node(y) in case (3).
Step 3 –
Remove x from the list of node identifiers. Step 2: Add x to the list of attached
identifiers for node n.
Example :
T0 = a + b —Expression 1
T1 = T0 + c —-Expression 2
d = T0 + T1 —–Expression 3
Expression 1 : T0 = a + b

Expression 2: T1 = T0 + c
Application of Directed Acyclic Graph:
 Directed acyclic graph determines the subexpressions that are commonly
used.
 Directed acyclic graph determines the names used within the block as well
as the names computed outside the block.
 Determines which statements in the block may have their computed value
outside the block.
 Code can be represented by a Directed acyclic graph that describes the
inputs and outputs of each of the arithmetic operations performed within the
code; this representation allows the compiler to perform common
subexpression elimination efficiently.
 Several programming languages describe value systems that are linked
together by a directed acyclic graph. When one value changes, its
successors are recalculated; each value in the DAG is evaluated as a
function of its predecessors.

Data flow analysis is a technique used in compiler design to analyze how data
flows through a program. It involves tracking the values of variables and
expressions as they are computed and used throughout the program, with the
goal of identifying opportunities for optimization and identifying potential errors.
The basic idea behind data flow analysis is to model the program as a graph,
where the nodes represent program statements and the edges represent data
flow dependencies between the statements. The data flow information is then
propagated through the graph, using a set of rules and equations to compute
the values of variables and expressions at each point in the program.

Some of the common types of data flow analysis performed by


compilers include:

1. Reaching Definitions Analysis: This analysis tracks the definition of a


variable or expression and determines the points in the program where the
definition “reaches” a particular use of the variable or expression. This
information can be used to identify variables that can be safely optimized or
eliminated.
2. Live Variable Analysis: This analysis determines the points in the program
where a variable or expression is “live”, meaning that its value is still needed
for some future computation. This information can be used to identify
variables that can be safely removed or optimized.
3. Available Expressions Analysis: This analysis determines the points in the
program where a particular expression is “available”, meaning that its value
has already been computed and can be reused. This information can be
used to identify opportunities for common subexpression elimination and
other optimization techniques.
4. Constant Propagation Analysis: This analysis tracks the values of constants
and determines the points in the program where a particular constant value
is used. This information can be used to identify opportunities for constant
folding and other optimization techniques.

Data flow analysis can have a number of advantages in compiler


design, including:

1. Improved code quality: By identifying opportunities for optimization and


eliminating potential errors, data flow analysis can help improve the quality
and efficiency of the compiled code.
2. Better error detection: By tracking the flow of data through the program, data
flow analysis can help identify potential errors and bugs that might otherwise
go unnoticed.
3. Increased understanding of program behavior: By modeling the program as
a graph and tracking the flow of data, data flow analysis can help
programmers better understand how the program works and how it can be
improved.
Basic Terminologies –

 Definition Point: a point in a program containing some definition.


 Reference Point: a point in a program containing a reference to a data item.
 Evaluation Point: a point in a program containing evaluation of expression.

Data Flow Properties –


 Available Expression – A expression is said to be available at a program
point x if along paths its reaching to x. A Expression is available at its
evaluation point.
An expression a+b is said to be available if none of the operands gets
modified before their use.

Example –

 Advantage –
It is used to eliminate common sub expressions.

 Reaching Definition – A definition D is reaches a point x if there is path


from D to x in which D is not killed, i.e., not redefined.
Example –
 Advantage –
It is used in constant and variable propagation.

 Live variable – A variable is said to be live at some point p if from p to end


the variable is used before it is redefined else it becomes dead.
Example –

 Advantage –
1. It is useful for register allocation.
2. It is used in dead code elimination.
 Busy Expression – An expression is busy along a path if its evaluation
exists along that path and none of its operand definition exists before its
evaluation along the path.
Advantage –
It is used for performing code movement optimization.

Features :

Identifying dependencies: Data flow analysis can identify dependencies


between different parts of a program, such as variables that are read or
modified by multiple statements.
Detecting dead code: By tracking how variables are used, data flow analysis
can detect code that is never executed, such as statements that assign values
to variables that are never used.
Optimizing code: Data flow analysis can be used to optimize code by
identifying opportunities for common subexpression elimination, constant
folding, and other optimization techniques.
Detecting errors: Data flow analysis can detect errors in a program, such as
uninitialized variables, by tracking how variables are used throughout the
program.
Handling complex control flow: Data flow analysis can handle complex
control flow structures, such as loops and conditionals, by tracking how data is
used within those structures.
Interprocedural analysis: Data flow analysis can be performed across multiple
functions in a program, allowing it to analyze how data flows between different
parts of the program.
Scalability: Data flow analysis can be scaled to large programs, allowing it to
analyze programs with many thousands or even millions of lines of code.

Code generator converts the intermediate representation of source code into a


form that can be readily executed by the machine. A code generator is
expected to generate the correct code. Designing of the code generator should
be done in such a way that it can be easily implemented, tested, and
maintained.
The following issue arises during the code generation phase:
Input to code generator – The input to the code generator is the intermediate
code generated by the front end, along with information in the symbol table that
determines the run-time addresses of the data objects denoted by the names in
the intermediate representation. Intermediate codes may be represented mostly
in quadruples, triples, indirect triples, Postfix notation, syntax trees, DAGs, etc.
The code generation phase just proceeds on an assumption that the input is
free from all syntactic and state semantic errors, the necessary type checking
has taken place and the type-conversion operators have been inserted
wherever necessary.
 Target program: The target program is the output of the code generator.
The output may be absolute machine language, relocatable machine
language, or assembly language.
 Absolute machine language as output has the advantages that it
can be placed in a fixed memory location and can be immediately
executed. For example, WATFIV is a compiler that produces the
absolute machine code as output.
 Relocatable machine language as an output allows subprograms
and subroutines to be compiled separately. Relocatable object
modules can be linked together and loaded by a linking loader. But
there is added expense of linking and loading.
Assembly language as output makes the code generation easier.
We can generate symbolic instructions and use the macro-facilities
of assemblers in generating code. And we need an additional
assembly step after code generation.
 Memory Management – Mapping the names in the source program to the
addresses of data objects is done by the front end and the code generator. A
name in the three address statements refers to the symbol table entry for the
name. Then from the symbol table entry, a relative address can be
determined for the name.
Instruction selection – Selecting the best instructions will improve the
efficiency of the program. It includes the instructions that should be complete
and uniform. Instruction speeds and machine idioms also play a major role
when efficiency is considered. But if we do not care about the efficiency of the
target program then instruction selection is straightforward. For example, the
respective three-address statements would be translated into the latter code
sequence as shown below:
P:=Q+R
S:=P+T

MOV Q, R0
ADD R, R0
MOV R0, P
MOV P, R0
ADD T, R0
MOV R0, S
Here the fourth statement is redundant as the value of the P is loaded again in
that statement that just has been stored in the previous statement. It leads to an
inefficient code sequence. A given intermediate representation can be
translated into many code sequences, with significant cost differences between
the different implementations. Prior knowledge of instruction cost is needed in
order to design good sequences, but accurate cost information is difficult to
predict.
 Register allocation issues – Use of registers make the computations faster
in comparison to that of memory, so efficient utilization of registers is
important. The use of registers is subdivided into two subproblems:
1. During Register allocation – we select only those sets of variables that will
reside in the registers at each point in the program.
2. During a subsequent Register assignment phase, the specific register is
picked to access the variable.
To understand the concept consider the following three address code
sequence
t:=a+b
t:=t*c
t:=t/d
Their efficient machine code sequence is as follows:
MOV a,R0
ADD b,R0
MUL c,R0
DIV d,R0
MOV R0,t
1. Evaluation order – The code generator decides the order in which the
instruction will be executed. The order of computations affects the efficiency
of the target code. Among many computational orders, some will require only
fewer registers to hold the intermediate results. However, picking the best
order in the general case is a difficult NP-complete problem.
2. Approaches to code generation issues: Code generator must always
generate the correct code. It is essential because of the number of special
cases that a code generator might face. Some of the design goals of code
generator are:
 Correct
 Easily maintainable
 Testable
 Efficient

Disadvantages in the design of a code generator:

Limited flexibility: Code generators are typically designed to produce a


specific type of code, and as a result, they may not be flexible enough to handle
a wide range of inputs or generate code for different target platforms. This can
limit the usefulness of the code generator in certain situations.
Maintenance overhead: Code generators can add a significant maintenance
overhead to a project, as they need to be maintained and updated alongside
the code they generate. This can lead to additional complexity and potential
errors.
Debugging difficulties: Debugging generated code can be more difficult than
debugging hand-written code, as the generated code may not always be easy
to read or understand. This can make it harder to identify and fix issues that
arise during development.
Performance issues: Depending on the complexity of the code being
generated, a code generator may not be able to generate optimal code that is
as performant as hand-written code. This can be a concern in applications
where performance is critical.
Learning curve: Code generators can have a steep learning curve, as they
typically require a deep understanding of the underlying code generation
framework and the programming languages being used. This can make it more
difficult to onboard new developers onto a project that uses a code generator.
Over-reliance: It’s important to ensure that the use of a code generator doesn’t
lead to over-reliance on generated code, to the point where developers are no
longer able to write code manually when necessary. This can limit the flexibility
and creativity of a development team, and may also result in lower quality code
overall.



Registers are the fastest locations in the memory hierarchy. But unfortunately, this
resource is limited. It comes under the most constrained resources of the target processor.
Register allocation is an NP-complete problem. However, this problem can be reduced to
graph coloring to achieve allocation and assignment. Therefore a good register allocator
computes an effective approximate solution to a hard problem.

Figure – Input-Output
The register allocator determines which values will reside in the register and which
register will hold each of those values. It takes as its input a program with an arbitrary
number of registers and produces a program with a finite register set that can fit into the
target machine. (See image)
Allocation vs Assignment:
Allocation –
Maps an unlimited namespace onto that register set of the target machine.
 Reg. to Reg. Model: Maps virtual registers to physical registers but spills excess
amount to memory.
 Mem. to Mem. Model: Maps some subset of the memory location to a set of names
that models the physical register set.
Allocation ensures that code will fit the target machine’s reg. set at each instruction.
Assignment –
Maps an allocated name set to the physical register set of the target machine.
 Assumes allocation has been done so that code will fit into the set of physical
registers.
 No more than ‘k’ values are designated into the registers, where ‘k’ is the no. of
physical registers.
General register allocation is an NP-complete problem:
 Solved in polynomial time, when (no. of required registers) <= (no. of available
physical registers).
 An assignment can be produced in linear time using Interval-Graph Coloring.
Local Register Allocation And Assignment:
Allocation just inside a basic block is called Local Reg. Allocation. Two approaches for
local reg. allocation: Top-down approach and bottom-up approach.
Top-Down Approach is a simple approach based on ‘Frequency Count’. Identify the
values which should be kept in registers and which should be kept in memory.
Algorithm:
1. Compute a priority for each virtual register.
2. Sort the registers into priority order.
3. Assign registers in priority order.
4. Rewrite the code.
Moving beyond single Blocks:
 More complicated because the control flow enters the picture.
 Liveness and Live Ranges: Live ranges consist of a set of definitions and uses that are
related to each other as they i.e. no single register can be common in a such couple of
instruction/data.
Following is a way to find out Live ranges in a block. A live range is represented as an
interval [i,j], where i is the definition and j is the last use.
Global Register Allocation and Assignment:
1. The main issue of a register allocator is minimizing the impact of spill code;
 Execution time for spill code.
 Code space for spill operation.
 Data space for spilled values.
2. Global allocation can’t guarantee an optimal solution for the execution time of spill
code.
3. Prime differences between Local and Global Allocation:
 The structure of a global live range is naturally more complex than the local one.
 Within a global live range, distinct references may execute a different number of
times. (When basic blocks form a loop)
4. To make the decision about allocation and assignments, the global allocator mostly
uses graph coloring by building an interference graph.
5. Register allocator then attempts to construct a k-coloring for that graph where ‘k’ is the
no. of physical registers.
 In case, the compiler can’t directly construct a k-coloring for that graph, it modifies
the underlying code by spilling some values to memory and tries again.
 Spilling actually simplifies that graph which ensures that the algorithm will halt.
6. Global Allocator uses several approaches, however, we’ll see top-down and bottom-up
allocations strategies. Subproblems associated with the above approaches.
 Discovering Global live ranges.
 Estimating Spilling Costs.
 Building an Interference graph.
Discovering Global Live Ranges:
How to discover Live range for a variable?
Figure – Discovering live ranges in a single block
The above diagram explains everything properly. Let’s take the example of Rarp, it’s
been initialized at program point 1 and its last usage is at program point 11. Therefore,
the Live Range of Rarp i.e. Larp is [1,11]. Similarly, others follow up.

Figure – Discovering Live Ranges


Estimating Global Spill Cost:
 Essential for taking a spill decision which includes – address computation, memory
operation cost, and estimated execution frequency.
 For performance benefits, these spilled values are kept typically for the Activation
records.
 Some embedded processors offer ScratchPad Memory to hold such spilled values.
 Negative Spill Cost: Consecutive load-store for a single address needs to be removed
as it increases the burden, hence incurs negative spill cost.
 Infinite Spill Cost: A live range should have infinite spill cost if no other live range
ends between its definition and its use.

Peephole optimization is a type of code Optimization performed on a small


part of the code. It is performed on a very small set of instructions in a segment
of code.
The small set of instructions or small part of code on which peephole
optimization is performed is known as peephole or window.
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:
1. To improve performance
2. To reduce memory footprint
3. To reduce code size
Peephole Optimization Techniques
A. Redundant load and store elimination: In this technique, redundancy is
eliminated.
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.
B. 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;
C. Strength Reduction: The operators that consume higher execution time are
replaced by the operators consuming less execution time.
Initial code:
y = x * 2;

Optimized code:
y = x + x; or y = x << 1;
Initial code:
y = x / 2;

Optimized code:
y = x >> 1;
D. Null sequences/ Simplify Algebraic Expressions : Useless operations are
deleted.
a := a + 0;
a := a * 1;
a := a/1;
a := a - 0;
E. Combine operations: Several operations are replaced by a single
equivalent operation.
F. Deadcode 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;
}

You might also like