Compler
Compler
Symbol Table – It is a data structure being used and maintained by the compiler, consists all the
identifier’s name along with their types. It helps the compiler to function smoothly by finding the
identifiers quickly.
The compiler has two modules namely front end and back end. Front-end constitutes of the Lexical
analyzer, semantic analyzer, syntax analyzer and intermediate code generator. And the rest are assembled
to form the back end.
1. Lexical Analyzer – It reads the program and converts it into tokens. It converts a stream of lexemes
into a stream of tokens. Tokens are defined by regular expressions which are understood by the
lexical analyzer. It also removes white-spaces and comments.
2. Syntax Analyzer – It is sometimes called as parser. It constructs the parse tree. It takes all the tokens
one by one and uses Context Free Grammar to construct the parse tree.
Why Grammar ?
The rules of programming can be entirely represented in some few productions. Using these
productions we can represent what the program actually is. The input has to be checked whether it is
in the desired format or not.
Syntax error can be detected at this level if the input is not in accordance with the grammar.
3. Semantic Analyzer – It verifies the parse tree, whether it’s meaningful or not. It furthermore
produces a verified parse tree.It also does type checking, Label checking and Flow control checking.
4. Intermediate Code Generator – It generates intermediate code, that is a form which can be readily
executed by machine We have many popular intermediate codes. Example – Three address code etc.
Intermediate code is converted to machine language using the last two phases which are platform
dependent.
Till intermediate code, it is same for every compiler out there, but after that, it depends on the
platform. To build a new compiler we don’t need to build it from scratch. We can take the
intermediate code from the already existing compiler and build the last two parts.
5. Code Optimizer – It transforms the code so that it consumes fewer resources and produces more
speed. The meaning of the code being transformed is not altered. Optimisation can be categorized into
two types: machine dependent and machine independent.
6. Target Code Generator – The main purpose of Target Code generator is to write a code that the
machine can understand and also register allocation, instruction selection etc. The output is dependent
on the type of assembler. This is the final stage of compilation.
Lexical analysis is the first phase of a compiler. It takes the modified source code from language
preprocessors that are written in the form of sentences. The lexical analyzer breaks these syntaxes into a
series of tokens, by removing any whitespace or comments in the source code.
If the lexical analyzer finds a token invalid, it generates an error. The lexical analyzer works closely with
the syntax analyzer. It reads character streams from the source code, checks for legal tokens, and passes
the data to the syntax analyzer when it demands.
Tokens
Lexemes are said to be a sequence of characters (alphanumeric) in a token. There are some predefined
rules for every lexeme to be identified as a valid token. These rules are defined by grammar rules, by
means of a pattern. A pattern explains what can be a token, and these patterns are defined by means of
regular expressions.
In programming language, keywords, constants, identifiers, strings, numbers, operators and punctuations
symbols can be considered as tokens.
For example, in C language, the variable declaration line
int value = 100;
contains the tokens:
int (keyword), value (identifier), = (operator), 100 (constant) and ; (symbol).
Specifications of Tokens
Let us understand how the language theory undertakes the following terms:
Alphabets
Any finite set of symbols {0,1} is a set of binary alphabets, {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F} is a set of
Hexadecimal alphabets, {a-z, A-Z} is a set of English language alphabets.
Strings
Any finite sequence of alphabets is called a string. Length of the string is the total number of occurrence
of alphabets, e.g., the length of the string tutorialspoint is 14 and is denoted by |tutorialspoint| = 14. A
string having no alphabets, i.e. a string of zero length is known as an empty string and is denoted by ε
(epsilon).
Special Symbols
A typical high-level language contains the following symbols:-
Preprocessor #
Language
A language is considered as a finite set of strings over some finite set of alphabets. Computer languages
are considered as finite sets, and mathematically set operations can be performed on them. Finite
languages can be described by means of regular expressions.
Back-tracking
Top- down parsers start from the root node (start symbol) and match the input string against the
production rules to replace them (if matched). To understand this, take the following example of CFG:
S → rXd | rZd
X → oa | ea
Z → ai
For an input string: read, a top-down parser, will behave like this:
It will start with S from the production rules and will match its yield to the left-most letter of the input,
i.e. ‘r’. The very production of S (S → rXd) matches with it. So the top-down parser advances to the next
input letter (i.e. ‘e’). The parser tries to expand non-terminal ‘X’ and checks its production from the left
(X → oa). It does not match with the next input symbol. So the top-down parser backtracks to obtain the
next production rule of X, (X → ea).
Now the parser matches all the input letters in an ordered manner. The string is accepted.
Predictive Parser
Predictive parser is a recursive descent parser, which has the capability to predict which production is to
be used to replace the input string. The predictive parser does not suffer from backtracking.
To accomplish its tasks, the predictive parser uses a look-ahead pointer, which points to the next input
symbols. To make the parser back-tracking free, the predictive parser puts some constraints on the
grammar and accepts only a class of grammar known as LL(k) grammar.
Predictive parsing uses a stack and a parsing table to parse the input and generate a parse tree. Both the
stack and the input contains an end symbol $ to denote that the stack is empty and the input is consumed.
The parser refers to the parsing table to take any decision on the input and stack element combination.
In recursive descent parsing, the parser may have more than one production to choose from for a single
instance of input, whereas in predictive parser, each step has at most one production to choose. There
might be instances where there is no production matching the input string, making the parsing procedure
to fail.
LL Parser
An LL Parser accepts LL grammar. LL grammar is a subset of context-free grammar but with some
restrictions to get the simplified version, in order to achieve easy implementation. LL grammar can be
implemented by means of both algorithms namely, recursive-descent or table-driven.
LL parser is denoted as LL(k). The first L in LL(k) is parsing the input from left to right, the second L in
LL(k) stands for left-most derivation and k itself represents the number of look aheads. Generally k = 1,
so LL(k) may also be written as LL(1).
LL Parsing Algorithm
We may stick to deterministic LL(1) for parser explanation, as the size of table grows exponentially with
the value of k. Secondly, if a given grammar is not LL(1), then usually, it is not LL(k), for any given k.
Parsing | Set 2 (Bottom Up or Shift Reduce Parsers)
In this article, we are discussing the Bottom Up parser.
Bottom Up Parsers / Shift Reduce Parsers
Build the parse tree from leaves to root. Bottom-up parsing can be defined as an attempt to reduce the
input string w to the start symbol of grammar by tracing out the rightmost derivations of w in reverse.
Eg.
A general shift reduce parsing is LR parsing. The L stands for scanning the input from left to right and R
stands for constructing a rightmost derivation in reverse.
Benefits of LR parsing:
1. Many programming languages using some variations of an LR parser. It should be noted that C++ and
Perl are exceptions to it.
2. LR Parser can be implemented very efficiently.
3. Of all the Parsers that scan their symbols from left to right, LR Parsers detect syntactic errors, as soon
as possible.
Here we will look at the construction of GOTO graph of grammar by using all the four LR parsing
techniques. For solving questions in GATE we have to construct the GOTO directly for the given
grammar to save time.
LR(0) Parser
We need two functions –
Closure()
Goto()
Augmented Grammar
If G is a grammar with start symbol S then G’, the augmented grammar for G, is the grammar with new
start symbol S’ and a production S’ -> S. The purpose of this new starting production is to indicate to the
parser when it should stop parsing and announce acceptance of input.
Let a grammar be S -> AA
A -> aA | b
The augmented grammar for the above grammar will be
S’ -> S
S -> AA
A -> aA | b
LR(0) Items
An LR(0) is the item of a grammar G is a production of G with a dot at some position in the right side.
S -> ABC yields four items
S -> .ABC
S -> A.BC
S -> AB.C
S -> ABC.
The production A -> ε generates only one item A -> .ε
Closure Operation:
If I is a set of items for a grammar G, then closure(I) is the set of items constructed from I by the two
rules:
1. Initially every item in I is added to closure(I).
2. If A -> α.Bβ is in closure(I) and B -> γ is a production then add the item B -> .γ to I, If it is not
already there. We apply this rule until no more items can be added to closure(I).
Eg:
Goto Operation :
Goto(I, X) = 1. Add I by moving dot after X.
2. Apply closure to first step.
Construction of GOTO graph-
State I0 – closure of augmented LR(0) item
Using I0 find all collection of sets of LR(0) items with the help of DFA
Convert DFA to LR(0) parsing table
Construction of LR(0) parsing table:
The action function takes as arguments a state i and a terminal a (or $ , the input end marker). The
value of ACTION[i, a] can have one of four forms:
1. Shift j, where j is a state.
2. Reduce A -> β.
3. Accept
4. Error
We extend the GOTO function, defined on sets of items, to states: if GOTO[I i , A] = Ij then GOTO
also maps a state i and a nonterminal A to state j.
Eg:
Consider the grammar S ->AA
A -> aA | b
Augmented grammar S’ -> S
S -> AA
A -> aA | b
The LR(0) parsing table for above GOTO graph will be –
Action part of the table contains all the terminals of the grammar whereas the goto part contains all the
nonterminals. For every state of goto graph we write all the goto operations in the table. If goto is applied
to a terminal than it is written in the action part if goto is applied on a nonterminal it is written in goto
part. If on applying goto a production is reduced ( i.e if the dot reaches at the end of production and no
further closure can be applied) then it is denoted as Ri and if the production is not reduced (shifted) it is
denoted as Si.
If a production is reduced it is written under the terminals given by follow of the left side of the
production which is reduced for ex: in I5 S->AA is reduced so R1 is written under the terminals in
follow(S)={$} (To know more about how to calculate follow function: Click here ) in LR(0) parser.
If in a state the start symbol of grammar is reduced it is written under $ symbol as accepted.
NOTE: If in any state both reduced and shifted productions are present or two reduced productions are
present it is called a conflict situation and the grammar is not LR grammar.
NOTE:
1. Two reduced productions in one state – RR conflict.
2. One reduced and one shifted production in one state – SR conflict.
If no SR or RR conflict present in the parsing table then the grammar is LR(0) grammar.
In above grammar no conflict so it is LR(0) grammar.
NOTE —In solving GATE question we don’t need to make the parsing table, by looking at the GOTO
graph only we can determine if the grammar is LR(0) grammar or not. We just have to look for conflicts
in the goto graph i.e if a state contains two reduced or one reduced and one shift entry for a TERMINAL
variable then there is a conflict and it is not LR(0) grammar. (In case of one shift with a VARIABLE and
one reduced there will be no conflict because then the shift entries will go to GOTO part of table and
reduced entries will go in ACTION part and thus no multiple entries).
To evaluate translation rules, we can employ one depth first search traversal on the parse tree. This is
possible only because SDT rules don’t impose any specific order on evaluation until children attributes
are computed before parents for a grammar having all synthesized attributes. Otherwise, we would have
to figure out the best suited plan to traverse through the parse tree and evaluate all the attributes in one or
more traversals. For better understanding, we will move bottom up in left to right fashion for computing
translation rules of our example.
Above diagram shows how semantic analysis could happen. The flow of information happens bottom-up
and all the children attributes are computed before parents, as discussed above. Right hand side nodes are
sometimes annotated with subscript 1 to distinguish between children and parent.
Additional Information
Synthesized Attributes are such attributes that depend only on the attribute values of children nodes.
Thus [ E -> E+T { E.val = E.val + T.val } ] has a synthesized attribute val corresponding to node E. If all
the semantic attributes in an augmented grammar are synthesized, one depth first search traversal in any
order is sufficient for semantic analysis phase.
Inherited Attributes are such attributes that depend on parent and/or siblings attributes.
Thus [ Ep -> E+T { Ep.val = E.val + T.val, T.val = Ep.val } ], where E & Ep are same production
symbols annotated to differentiate between parent and child, has an inherited attribute val corresponding
to node T.
TYPE CHECKING
A compiler must check that the source program follows both syntactic and semantic conventions
of the source language. This checking, called static checking, detects and reports programming errors.
1. Type checks - A compiler should report an error if an operator is applied to an incompatible operand.
Example: If an array variable and function variable are added together.
2. Flow-of-control checks - Statements that cause flow of control to leave a construct must have some
place to which to transfer the flow of control. Example: An enclosing statement, such as break, does not
exist in switch statement.
Fig. 2.6 Position of type checker
A typechecker verifies that the type of a construct matches that expected by its context. For
example : arithmetic operator mod in Pascal requires integer operands, so a type checker verifies that the
operands of mod have type integer. Type information gathered by a type checker may be needed when
code is generated.
Type Systems
The design of a type checker for a language is based on information about the syntactic constructs
in the language, the notion of types, and the rules for assigning types to language
constructs.
For example : “ if both operands of the arithmetic operators of +,- and * are of type integer, then the result
is of type integer ”
Type Expressions
The type of a language construct will be denoted by a “type expression.” A type expression is
either a basic type or is formed by applying an operator called a type constructor to other type
expressions. The sets of basic types and constructors depend on the language to be checked. The
following are the definitions of type expressions:
1. Basic types such as boolean, char, integer, real are type expressions.
A special basic type, type_error , will signal an error during type checking; void denoting “the
absence of a value” allows statements to be checked.
Constructors include:
Arrays : If T is a type expression then array (I,T) is a type expression denoting the type of an
array with elements of type T and index set I.
Products : If T1 and T2 are type expressions, then their Cartesian product T1 X T2 is a type
expression.
Records : The difference between a record and a product is that the names. The record type
constructor will be applied to a tuple formed from field names and field types.
For example:
declares the type name row representing the type expression record((address X integer) X
(lexeme X array(1..15,char))) and the variable table to be an array of records of this type.
Pointers : If T is a type expression, then pointer(T) is a type expression denoting the type “pointer
to an object of type T”.
For example, var p: ↑ row declares variable p to have type pointer(row).
Functions : A function in programming languages maps a domain type D to a range type R. The
type of such function is denoted by the type expression D → R
4. Type expressions may contain variables whose values are type expressions.
Type systems
A type system is a collection of rules for assigning type expressions to the various parts of a
program. A type checker implements a type system. It is specified in a syntax-directed manner. Different
type systems may be used by different compilers or processors of the same language.
Checking done by a compiler is said to be static, while checking done when the target program
runs is termed dynamic. Any check can be done dynamically, if the target code carries the type of an
element along with the value of that element.
A sound type system eliminates the need for dynamic checking fo allows us to determine
statically that these errors cannot occur when the target program runs. That is, if a sound type system
assigns a type other than type_error to a program part, then type errors cannot occur when the target code
for the program part is run.
A language is strongly typed if its compiler can guarantee that the programs it accepts will
execute without type errors.
Error Recovery
Since type checking has the potential for catching errors in program, it is desirable for type
checker to recover from errors, so it can check the rest of the input. Error handling has to be designed into
the type system right from the start; the type checking rules must be prepared to cope with errors.
A type checker for a simple language checks the type of each identifier. The type checker is a
translation scheme that synthesizes the type of each expression from the types of its subexpressions. The
type checker can handle arrays, pointers, statements and functions.
A Simple Language
Consider the following grammar:
P→D;E
D → D ; D | id : T
T → char | integer | array [ num ] of T | ↑ T
E → literal | num | id | E mod E | E [ E ] | E ↑
Translation scheme:
P→D;E
D→D;D
D → id : T { addtype (id.entry , T.type) }
T → char { T.type : = char }
T → integer { T.type : = integer }
T → ↑ T1 { T.type : = pointer(T1.type) }
T → array [ num ] of T1 { T.type : = array ( 1… num.val , T1.type) }
pointer ( integer ).
In the following rules, the attribute type for E gives the type expression assigned to the expression
generated by E.
lookup ( e ) is used to fetch the type saved in the symbol table entry pointed to by e.
3. E → E1 mod E2 { E.type : = if E1. type = integer and E2. type = integer then integer
else type_error }
The expression formed by applying the mod operator to two subexpressions of type integer has type
integer; otherwise, its type is type_error.
4. E → E1 [ E2 ] { E.type : = if E2.type = integer and E1.type = array(s,t) then t
else type_error }
In an array reference E1 [ E2 ] , the index expression E2 must have type integer. The result is the element
type t obtained from the type array(s,t) of E1.
Statements do not have values; hence the basic type void can be assigned to them. If an error is
detected within a statement, then type_error is assigned.
3. While statement:
S → while E do S1
4. Sequence of statements:
S → S1 ; S2 { S.type : = if S1.type = void and S1.type = void then void else type_error }
Int n;
readarray();
quicksort(1,n);
}
quicksort(int m, int n) {
Int i= partition(m,n);
quicksort(m,i-1);
quicksort(i+1,n);
}
The activation tree for this program will be:
First main function as root then main calls readarray and quicksort. Quicksort in turn calls partition and
quicksort again. The flow of control in a program corresponds to the depth first traversal of activation tree
which starts at the root.
CONTROL STACK AND ACTIVATION RECORDS
Control stack or runtime stack is used to keep track of the live procedure activations i.e the procedures
whose execution have not been completed. A procedure name is pushed on to the stack when it is called
(activation begins) and it is popped when it returns (activation ends). Information needed by a single
execution of a procedure is managed using an activation record or frame. When a procedure is called, an
activation record is pushed into the stack and as soon as the control returns to the caller function the
activation record is popped.
PARAMETER PASSING
The communication medium among procedures is known as parameter passing. The values of the
variables from a calling procedure are transferred to the called procedure by some mechanism.
Basic terminology :
R- value: The value of an expression is called its r-value. The value contained in a single variable
also becomes an r-value if its appear on the right side of the assignment operator. R-value can always
be assigned to some other variable.
L-value: The location of the memory(address) where the expression is stored is known as the l-value
of that expression. It always appears on the left side if the assignment operator.
i.Formal Parameter: Variables that take the information passed by the caller procedure are called
formal parameters. These variables are declared in the definition of the called function.
ii.Actual Parameter: Variables whose values and functions are passed to the called function are
called actual parameters. These variables are specified in the function call as arguments.
Different ways of passing the parameters to the procedure
Call by Value
In call by value the calling procedure pass the r-value of the actual parameters and the compiler puts
that into called procedure’s activation record. Formal parameters hold the values passed by the calling
procedure, thus any changes made in the formal parameters does not affect the actual paramet
ers.
Call by ReferenceIn call by reference the formal and actual parameters refers to same memory
location. The l-value of actual parameters is copied to the activation record of the called function.
Thus the called function has the address of the actual parameters. If the actual parameters does not
have a l-value (eg- i+3) then it is evaluated in a new temporary location and the address of the
location is passed. Any changes made in the formal parameter is reflected in the actual parameters
(because changes are made at the address).
Call by Copy Restore
In call by copy restore compiler copies the value in formal parameters when the procedure is called
and copy them back in actual parameters when control returns to the called function. The r-values are
passed and on return r-value of formals are copied into l-value of actuals.
Call by Name
In call by name the actual parameters are substituted for formals in all the places formals occur in the
procedure. It is also referred as lazy evaluation because evaluation is done on parameters only when
needed.
Code generation can be considered as the final phase of compilation. Through post code generation,
optimization process can be applied on the code, but that can be seen as a part of code generation phase
itself. The code generated by the compiler is an object code of some lower-level programming language,
for example, assembly language. We have seen that the source code written in a higher-level language is
transformed into a lower-level language that results in a lower-level object code, which should have the
following minimum properties:
[t1 = t0 + c]
[d = t0 + t1]
Peephole Optimization
This optimization technique works locally on the source code to transform it into an optimized code. By
locally, we mean a small portion of the code block at hand. These methods can be applied on
intermediate codes as well as on target codes. A bunch of statements is analyzed and are checked for the
following possible optimization:
MOV x, R0
MOV R0, R1
We can delete the first instruction and re-write the sentence as:
MOV x, R1
Unreachable code
Unreachable code is a part of the program code that is never accessed because of programming
constructs. Programmers may have accidently written a piece of code that can never be reached.
Example:
void add_ten(int x)
{
return x + 10;
printf(“value of x is %d”, x);
}
In this code segment, the printf statement will never be executed as the program control returns back
before it can execute, hence printf can be removed.
Strength reduction
There are operations that consume more time and space. Their ‘strength’ can be reduced by replacing
them with other operations that consume less time and space, but produce the same result.
For example, x * 2 can be replaced by x << 1, which involves only one left shift. Though the output of a
* a and a2 is same, a2 is much more efficient to implement.
Code Generator
A code generator is expected to have an understanding of the target machine’s runtime environment and
its instruction set. The code generator should take the following things into consideration to generate the
code:
Target language : The code generator has to be aware of the nature of the target language for
which the code is to be transformed. That language may facilitate some machine-specific
instructions to help the compiler generate the code in a more convenient way. The target
machine can have either CISC or RISC processor architecture.
IR Type : Intermediate representation has various forms. It can be in Abstract Syntax Tree
(AST) structure, Reverse Polish Notation, or 3-address code.
Selection of instruction : The code generator takes Intermediate Representation as input and
converts (maps) it into target machine’s instruction set. One representation can have many ways
(instructions) to convert it, so it becomes the responsibility of the code generator to choose the
appropriate instructions wisely.
Register allocation : A program has a number of values to be maintained during the execution.
The target machine’s architecture may not allow all of the values to be kept in the CPU memory
or registers. Code generator decides what values to keep in the registers. Also, it decides the
registers to be used to keep these values.
Ordering of instructions : At last, the code generator decides the order in which the instruction
will be executed. It creates schedules for instructions to execute them.
Descriptors
The code generator has to track both the registers (for availability) and addresses (location of values)
while generating the code. For both of them, the following two descriptors are used:
Register descriptor : Register descriptor is used to inform the code generator about the
availability of registers. Register descriptor keeps track of values stored in each register.
Whenever a new register is required during code generation, this descriptor is consulted for
register availability.
Address descriptor : Values of the names (identifiers) used in the program might be stored at
different locations while in execution. Address descriptors are used to keep track of memory
locations where the values of identifiers are stored. These locations may include CPU registers,
heaps, stacks, memory or a combination of the mentioned locations.
Code generator keeps both the descriptor updated in real-time. For a load statement, LD R1, x, the code
generator:
Code Generation
Basic blocks comprise of a sequence of three-address instructions. Code generator takes these sequence
of instructions as input.
Note : If the value of a name is found at more than one place (register, cache, or memory), the register’s
value will be preferred over the cache and main memory. Likewise cache’s value will be preferred over
the main memory. Main memory is barely given any preference.
getReg : Code generator uses getReg function to determine the status of available registers and the
location of name values. getReg works as follows:
If variable Y is already in register R, it uses that register.
Else if some register R is available, it uses that register.
Else if both the above options are not possible, it chooses a register that requires minimal number
of load and store instructions.
For an instruction x = y OP z, the code generator may perform the following actions. Let us assume that
L is the location (preferably register) where the output of y OP z is to be saved:
Call function getReg, to decide the location of L.
Determine the present location (register or memory) of y by consulting the Address Descriptor
of y. If y is not presently in register L, then generate the following instruction to copy the value
of y to L:
MOV y’, L
where y’ represents the copied value of y.
Determine the present location of z using the same method used in step 2 for y and generate the
following instruction:
OP z’, L
where z’ represents the copied value of z.
Now L contains the value of y OP z, that is intended to be assigned to x. So, if L is a register,
update its descriptor to indicate that it contains the value of x. Update the descriptor of x to
indicate that it is stored at location L.
If y and z has no further use, they can be given back to the system.
Other code constructs like loops and conditional statements are transformed into assembly language in
general assembly way.
Compiler Design - Code Optimization
Optimization is a program transformation technique, which tries to improve the code by making it
consume less resources (i.e. CPU, Memory) and deliver high speed.
In optimization, high-level general programming constructs are replaced by very efficient low-level
programming codes. A code optimizing process must follow the three rules given below:
The output code must not, in any way, change the meaning of the program.
Optimization should increase the speed of the program and if possible, the program should
demand less number of resources.
Optimization should itself be fast and should not delay the overall compiling process.
Efforts for an optimized code can be made at various levels of compiling the process.
At the beginning, users can change/rearrange the code or use better algorithms to write the code.
After generating intermediate code, the compiler can modify the intermediate code by address
calculations and improving loops.
While producing the target machine code, the compiler can make use of memory hierarchy and
CPU registers.
Optimization can be categorized broadly into two types : machine independent and machine dependent.
Machine-independent Optimization
In this optimization, the compiler takes in the intermediate code and transforms a part of the code that
does not involve any CPU registers and/or absolute memory locations. For example:
do
{
item = 10;
value = value + item;
} while(value<100);
This code involves repeated assignment of the identifier item, which if we put this way:
Item = 10;
do
{
value = value + item;
} while(value<100);
should not only save the CPU cycles, but can be used on any processor.
Machine-dependent Optimization
Machine-dependent optimization is done after the target code has been generated and when the code is
transformed according to the target machine architecture. It involves CPU registers and may have
absolute memory references rather than relative references. Machine-dependent optimizers put efforts to
take maximum advantage of memory hierarchy.
Basic Blocks
Source codes generally have a number of instructions, which are always executed in sequence and are
considered as the basic blocks of the code. These basic blocks do not have any jump statements among
them, i.e., when the first instruction is executed, all the instructions in the same basic block will be
executed in their sequence of appearance without losing the flow control of the program.
A program can have various constructs as basic blocks, like IF-THEN-ELSE, SWITCH-CASE
conditional statements and loops such as DO-WHILE, FOR, and REPEAT-UNTIL, etc.
Dead-code Elimination
Dead code is one or more than one code statements, which are:
Likewise, the picture above depicts that the conditional statement is always false, implying that the code,
written in true case, will never be executed, hence it can be removed.
Partial Redundancy
Redundant expressions are computed more than once in parallel path, without any change in
operands.whereas partial-redundant expressions are computed more than once in a path, without any
change in operands. For example,
[redundant expression] [partially redundant
expression]
Loop-invariant code is partially redundant and can be eliminated by using a code-motion technique.
Another example of a partially redundant code can be:
If (condition)
{
a = y OP z;
}
else
{
...
}
c = y OP z;
We assume that the values of operands (y and z) are not changed from assignment of variable a to
variable c. Here, if the condition statement is true, then y OP z is computed twice, otherwise once. Code
motion can be used to eliminate this redundancy, as shown below:
If (condition)
{
...
tmp = y OP z;
a = tmp;
...
}
else
{
...
tmp = y OP z;
}
c = tmp;
Here, whether the condition is true or false; y OP z should be computed only once.
Recent development on compilers
.Over the past decade, production compilers for general-purpose processors have adopted a
number of major technologies emerging from compiler research, including SSA-based
optimization, pointer analysis, profile-guided optimization, link-time cross-module
optimization, automatic vectorization, and just-in-time compilation with adaptive
optimization for dynamic languages. These features are here to stay for the foreseeable future.
So what major new features could emerge from compiler research over the next decade?
First, just-in-time and dynamic optimization will be extended to static languages, such as C,
C++, and Fortran. This has already happened for graphics applications, as in the MacOS X
OpenGL library and the AMD ATI compiler, and is now being adopted for general-purpose
multicore platforms such as the RapidMind Multicore Development Platform.
Second, and perhaps most predictably, compilers will play a major role in tackling the
multicore programming challenge. This does not mean that automatic parallelization will
come back from the dead. Rather compiler support for parallel programming will take two
forms: optimization and code generation for explicitly parallel programs; and interactive,
potentially optimistic, parallelization technology to support semi-automatic porting of
existing code to explicitly parallel programming models.
Third, compilers will increasingly be responsible for enhancing or enforcing safety and
reliability properties for programs. The last few years have seen new language and compiler
techniques (e.g. in the Cyclone, CCured, and SAFECode projects) that guarantee complete
memory safety and sound operational semantics even for C and C++ programs. There is no
longer any excuse for production C/C++ compilers not to provide these capabilities, at least
as an option for security-sensitive software, including all privileged software.
Furthermore, these capabilities can be deployed via a typed virtual machine that enables more
powerful security and reliability techniques than with native machine code.Fourth, compilers
will increasingly incorporate more sophisticated auto-tuning strategies for exploring
optimization sequences, or even arbitrary code sequences for key kernels. This is one of the
major sources of unexploited performance improvements with existing compiler technology.
Finally, compilers will adopt speculative optimizations in order to compensate for the
constraints imposed by conservative static analysis. Recent architecture research has led to
novel hardware mechanisms that can make such speculation efficient and the ball is in the
compiler community's court to invent new ways to exploit this hardware support for more
powerful, traditional and non-traditional, optimizations.