Compiler Design Unit-5
Compiler Design Unit-5
UNIT 5
MACHINE INDEPENDENT OPTIMIZATION
Elimination of unnecessary instructions in object code, or the replacement of one sequence of
instructions by a faster sequence of instructions that does the same thing is usually called "code
improvement" or "code optimization."
Optimizations are classified into two categories.
Machine independent optimizations are program transformations that improve the target
code without taking into consideration any properties of the target machine
Machine dependant optimizations are based on register allocation and utilization of special
machine-instruction sequences.
Function-Preserving Transformations: There are a number of ways in which a compiler can improve
a program without changing the function it computes.
: Common sub expression elimination
Copy propagation,
Dead-code elimination
Constant folding
Common Sub expressions elimination:
An occurrence of an expression E is called a common sub-expression if E was previously
computed, and the values of variables in E have not changed since the previous computation. We can
avoid recomputing the expression if we can use the previously computed value.
• For example
t1: = 4*i
t2: = a [t1]
t3: = 4*j
t4: = 4*i
t5: = n
t6: = b [t4] +t5
.
The above code can be optimized using the common sub-expression elimination as
t1: = 4*i
t2: = a [t1]
t3: = 4*j
t5: = n
t6: = b [t1] +t5
The common sub expression t4: =4*i is eliminated as its computation is already in t1 and the value of i
is not been changed from definition to use.
Copy Propagation:
Assignments of the form f : = g called copy statements, or copies for short. The idea behind the
copy-propagation transformation is to use g for f, whenever possible after the copy statement f: = g.
Copy propagation means use of one variable instead of another.
• For example:
x=Pi;
A=x*r*r;
Dead-Code Eliminations:
A variable is live at a point in a program if its value can be used subsequently; otherwise, it is
dead at that point.
Example:
i=0;
if(i==1)
{
a=b+5;
}
Here, ‘if’ statement is dead code because this condition will never get satisfied.
Constant folding:
Deducing at compile time that the value of an expression is a constant and using the constant
instead is known as constant folding. One advantage of copy propagation is that it often turns the copy
statement into dead code.
For example,
a=3.14157/2 can be replaced by
a=1.570
.
Loop Optimizations:
In loops, especially in the inner loops, programs tend to spend the bulk of their time. The
running time of a program may be improved if the number of instructions in an inner loop is decreased,
even if we increase the amount of code outside that loop.
Three techniques are important for loop optimization:
1. Code motion, which moves code outside a loop;
2. Induction-variable elimination, which we apply to replace variables from inner loop.
3.Reduction in strength, which replaces expensive operation by a cheaper one, such as a
multiplication by an addition.
This transformation takes an expression that yields the same result independent of the number of
times a loop is executed (a loop-invariant computation) and places the expression before the loop. Note
that the notion “before the loop” assumes the existence of an entry for the loop. For example, evaluation
of limit-2 is a loop-invariant computation in the following while-statement:
t= limit-2;
while (i<=t) /* statement does not change limit or t */
Induction Variables :
Loops are usually processed inside out. For example consider the loop around B3. Note that the
values of j and t4 remain in lock-step; every time the value of j decreases by 1, that of t4 decreases by 4
because 4*j is assigned to t4. Such identifiers are called induction variables.
When there are two or more induction variables in a loop, it may be possible to get rid of all but
one, by the process of induction-variable elimination. For the inner loop around B3 in Fig.5.3 we cannot
get rid of either j or t4 completely; t4 is used in B3 and j in B4.
However, we can illustrate reduction in strength and illustrate a part of the process of induction-
variable elimination. Eventually j will be eliminated when the outer loop of B2- B5 is considered.
Example:
As the relationship t4:=4*j surely holds after such an assignment to t4 in Fig. and t4 is not
changed elsewhere in the inner loop around B3, it follows that just after the statement j:=j-1 the
relationship t4:= 4*j-4 must hold. We may therefore replace the assignment t4:= 4*j by t4:= t4-4. The
only problem is that t4 does not have a value when we enter block B3 for the first time. Since we must
maintain the relationship t4=4*j on entry to the block B3, we place an initializations of t4 at the end of
the block where j itself is initialized, shown by the dashed addition to block B1 in Fig.5.3.
Reduction In Strength:
Reduction in strength replaces expensive operations by equivalent cheaper ones on the target
machine. Certain machine instructions are considerably cheaper than others and can often be used as
special cases of more expensive operators. For example, x² is invariably cheaper to implement as x*x
than as a call to an exponentiation routine. Fixed-point multiplication or division by a power of two is
cheaper to implement as a shift. Floating-point division by a constant can be implemented as
multiplication by a constant, which may be cheaper.
.
PEEPHOLE OPTIMIZATION
A statement-by-statement code-generations strategy often produces target code that contains
redundant instructions and suboptimal constructs. The quality of such target code can be improved by
applying “optimizing” transformations to the target program.
A simple but effective technique for improving the target code is peephole optimization, A
method for trying to improving the performance of the target program by examining a short sequence of
target instructions (called the peephole) and replacing these instructions by a shorter or faster sequence,
whenever possible.
The peephole is a small, moving window on the target program.
Characteristics of peephole optimizations:
Redundant-instructions elimination
Flow-of-control optimizations
Algebraic simplifications
Use of machine idioms
Unreachable code
.
Redundant-instructions elimination
see the instructions sequence
(1) MOV R0,a
(2) MOV a,R0
we can delete instructions (2) because whenever (2) is executed. (1) will ensure that the value of
a is already in register R0.If (2) had a label we could not be sure that (1) was always executed
immediately before (2) and so we could not remove (2).
Unreachable Code:
#define debug 0
….
If ( debug ) {
Print debugging information
}
In the intermediate representations the if-statement may be translated as:
One obvious peephole optimization is to eliminate jumps over jumps .Thus no matter what the
value of debug; (a) can be replaced by:
If debug ≠1 goto L2
Print debugging information
L2:..............................................(b)
If debug ≠0 goto L2
Print debugging information
L2:..............................................(c)
As the argument of the statement of (c) evaluates to a constant true it can be replaced
.
By goto L2. Then all the statement that print debugging aids are manifestly unreachable and can
be eliminated one at a time.
Flows-Of-Control Optimizations:
The unnecessary jumps can be eliminated in either the intermediate code or the target code by
the following types of peephole optimizations. We can replace the jump sequence
goto L1
….
L1: goto L2
If there are now no jumps to L1, then it may be possible to eliminate the statement L1:goto L2
provided it is preceded by an unconditional jump .Similarly, the sequence
if a < b goto L1
….
can be replaced by
If a < b goto L2
….
L1: goto L2
Ø Finally, suppose there is only one jump to L1 and L1 is preceded by an unconditional goto.
Then the sequence
goto L1
may be replaced by
.
If a < b goto L2
goto L3
…….
L3:
While the number of instructions in(e) and (f) is the same, we sometimes skip the unconditional jump
in (f), but never in (e).Thus (f) is superior to (e) in execution time
Algebraic Simplification:
There is no end to the amount of algebraic simplification that can be attempted through peephole
optimization. Only a few algebraic identities occur frequently enough that it is worth considering
implementing them. For example, statements such as
x := x+0 or
x := x * 1
are often produced by straightforward intermediate code-generation algorithms, and they can be
eliminated easily through peephole optimization.
Reduction in Strength:
Reduction in strength replaces expensive operations by equivalent cheaper ones on the target
machine. Certain machine instructions are considerably cheaper than others and can often be used as
special cases of more expensive operators.
X2 → X*X
The target machine may have hardware instructions to implement certain specific operations
efficiently. For example, some machines have auto-increment and auto-decrement addressing modes.
These add or subtract one from an operand before or after using its value. The use of these modes
greatly improves the quality of code when pushing or popping a stack, as in parameter passing. These
modes can also be used in code for statements like i : =i+1.
i:=i+1 → i++
.
i:=i-1 → i- -
4 Reaching Definitions
5 Live-Variable Analysis
6 Available Expressions
"Data-flow analysis" refers to a body of techniques that derive information about the flow of data along
program execution paths.
When we analyze the behavior of a program, we must consider all the possible sequences of
program points ("paths") through a flow graph that the program execution can take. We then extract,
from the possible program states at each point, the information we need for the particular data-flow
analysis problem we want to solve. In more complex analyses, we must consider paths that jump among
the flow graphs for various procedures, as calls and returns are executed.
Within one basic block, the program point after a statement is the same as the program point
before the next statement.
If there is an edge from block B1 to block B22 , then the program point after the last statement
of B1 may be followed immediately by the program point before the first statement of B2.
Thus, we may define an execution path (or just path) from point pi to point pn to be a sequence of
points pi,p2,... ,pn such that for each i = 1,2, ... ,n - 1, either
1. Pi is the point immediately preceding a statement and pi+i is the point immediately following
that same statement, or
2.pi is the end of some block and pi+1 is the beginning of a successor block.
.
. In data-flow analysis, we do not distinguish among the paths taken to reach a program point.
Moreover, we do not keep track of entire states; rather, we abstract out certain details, keeping only the
data we need for the purpose of the analysis. Two examples will illustrate how the same program states
may lead to different information abstracted at a point.
1. To help users debug their programs, we may wish to find out what are all the values a variable may
have at a program point, and where these values may be defined. For instance, we may summarize all
the program states at point (5) by saying that the value of a is one of {1,243}, and that it may be defined
by one of { ^ 1 , ^ 3 } . The definitions that may reach a program point along some path are known
as reaching definitions.
2. Suppose, instead, we are interested in implementing constant folding. If a use of the variable x is
reached by only one definition, and that definition assigns a constant to x, then we can simply
replace x by the constant. If, on the other hand, several definitions of x may reach a single program
point, then we cannot perform constant folding on x. Thus, for constant folding we wish to find those
definitions that are the unique definition of their variable to reach a given program point, no matter
which execution path is taken. For point (5) of Fig. 9.12, there is no definition that must be the
definition of a at that point, so this set is empty for a at point (5). Even if a variable has a unique
definition at a point, that definition must assign a constant to the variable. Thus, we may simply
describe certain variables as "not a constant," instead of collecting all their possible values or all their
possible definitions.
, we associate with every program point a data-flow value that represents an abstraction of the set of all
possible program states that can be observed for that point. The set of possible data-flow values is the
domain for this application. For example, the domain of data-flow values for reaching definitions is the
set of all subsets of definitions in the program.
.
A particular data-flow value is a set of definitions, and we want to associate with each point in the
program the exact set of definitions that can reach that point. As discussed above, the choice of
abstraction depends on the goal of the analysis; to be efficient, we only keep track of information that is
relevant.
Denote the data-flow values before and after each statement s by IN[S ] and OUT[s], respectively.
The data-flow problem is to find a solution to a set of constraints on the IN[S]'S and OUT[s]'s, for all
statements s. There are two sets of constraints: those based on the semantics of the statements ("transfer
functions") and those based on the flow of control.
Transfer Functions
The data-flow values before and after a statement are constrained by the semantics of the statement. For
example, suppose our data-flow analysis involves determining the constant value of variables at points.
If variable a has value v before executing statement b = a, then both a and b will have the value v after
the statement. This relationship between the data-flow values before and after the assignment
statement is known as a transfer function.
Transfer functions come in two flavors: information may propagate forward along execution paths, or it
may flow backwards up the execution paths. In a forward-flow problem, the transfer function of a
statement s, which we shall usually denote f(a), takes the data-flow value before the statement and
produces a new data-flow value after the statement. That is,
Conversely, in a backward-flow problem, the transfer function f(a) for statement 8 converts a data-flow
value after the statement to a new data-flow value before the statement. That is,
The second set of constraints on data-flow values is derived from the flow of control. Within a basic
block, control flow is simple. If a block B consists of statements s1, s 2 , • • • ,sn in that order, then the
control-flow value out of Si is the same as the control-flow value into Si+i. That is,
However, control-flow edges between basic blocks create more complex constraints between the last
statement of one basic block and the first statement of the following block. For example, if we are
interested in collecting all the definitions that may reach a program point, then the set of definitions
reaching the leader statement of a basic block is the union of the definitions after the last statements of
each of the predecessor blocks. The next section gives the details of how data flows among the blocks.
.
Suppose block B consists of statements s 1 , . . . , sn, in that order. If si is the first statement of basic
block B, then m[B] = I N [ S I ] , Similarly, if sn is the last statement of basic block B, then OUT[S] =
OUT[s„] . The transfer function of a basic block B, which we denote fB, can be derived by composing
the transfer functions of the statements in the block. That is, let fa. be the transfer function of
statement st. Then of statement si. Then fB = f,sn, o . . . o f,s2, o fsl. . The relationship between the
beginning and end of the block is
The constraints due to control flow between basic blocks can easily be rewritten by
substituting IN[B] and OUT[B] for IN[SI ] and OUT[sn], respectively. For instance, if data-flow values
are information about the sets of constants that may be assigned to a variable, then we have a forward-
flow problem in which
When the data-flow is backwards as we shall soon see in live-variable analy-sis, the equations are
similar, but with the roles of the IN's and OUT's reversed. That is,
Unlike linear arithmetic equations, the data-flow equations usually do not have a unique solution. Our
goal is to find the most "precise" solution that satisfies the two sets of constraints: control-flow and
transfer constraints. That is, we need a solution that encourages valid code improvements, but does not
justify unsafe transformations — those that change what the program computes.
4. Reaching Definitions
.
"Reaching definitions" is one of the most common and useful data-flow schemas. By knowing where in
a program each variable x may have been defined when control reaches each point p, we can determine
many things about x. For just two examples, a compiler then knows whether x is a constant at
point p, and a debugger can tell whether it is possible for x to be an undefined variable, should x be used
at p.
We say a definition d reaches a point p if there is a path from the point immediately
following d to p, such that d is not "killed" along that path. We kill a definition of a variable x if there is
any other definition of x anywhere along the path . if a definition d of some variable x reaches point p,
then d might be the place at which the value of x used at p was last defined.
Here, and frequently in what follows, + is used as a generic binary operator. This statement "generates"
a definition d of variable u and "kills" all the
other definitions in the program that define variable u, while leaving the re-maining incoming
definitions unaffected. The transfer function of definition d thus can be expressed as
where gend = {d}, the set of definitions generated by the statement, and killd is the set of all other
definitions of u in the program.
The transfer function of a basic block can be found by composing the transfer functions of the
statements contained therein. The composition of functions of the form (9.1), which we shall refer to as
"gen-kill form," is also of that form, as we can see as follows. Suppose there are two functions fi(x) =
gen1 U (x - kill1) and f2(x) = gen2 U (x — kill2). Then
.
This rule extends to a block consisting of any number of statements. Suppose block B has n statements,
with transfer functions fi(x) = geni U (x — kilh) for i = 1,2, ... , n. Then the transfer function for
block B may be written as:
Thus, like a statement, a basic block also generates a set of definitions and kills a set of definitions. The
gen set contains all the definitions inside the block that are "visible" immediately after the block — we
refer to them as downwards exposed. A definition is downwards exposed in a basic block only if it is
.
not "killed" by a subsequent definition to the same variable inside the same basic block. A basic block's
kill set is simply the union of all the definitions killed by the individual statements. Notice that a
definition may appear in both the gen and kill set of a basic block. If so, the fact that it is in gen takes
precedence, because in gen-kill form, the kill set is applied before the gen set.
is {d2} since d1 is not downwards exposed. The kill set contains both d1 and d2, since d1 kills d2
and vice versa. Nonetheless, since the subtraction of the kill set precedes the union operation with the
gen set, the result of the transfer function for this block always includes definition d2.
We refer to union as the meet operator for reaching definitions. In any data-flow schema, the meet
operator is the one we use to create a summary of the contributions from different paths at the
confluence of those paths.
A l g o r i t h m 9 . 1 1 : Reaching definitions.
INPUT: A flow graph for which kills and genB have been computed for each block B.
OUTPUT: I N [ B ] and O U T [ B ] , the set of definitions reaching the entry and exit of each
block B of the flow graph.
METHOD: We use an iterative approach, in which we start with the "estimate" OUT[JB] = 0 for
all B and converge to the desired values of IN and OUT. As we must iterate until the IN ' s (and hence
the OUT's) converge, we could use a boolean variable change to record, on each pass through the
blocks, whether any OUT has changed. However, in this and in similar algorithms described later, we
assume that the exact mechanism for keeping track of changes is understood, and we elide those details.
The algorithm is sketched in Fig. 9.14. The first two lines initialize certain data-flow values. 4 Line (3)
starts the loop in which we iterate until convergence, and the inner loop of lines (4) through (6) applies
the data-flow equations to every block other than the entry. •
Algorithm 9.11 propagates definitions as far as they will go with-out being killed, thus simulating all
possible executions of the program. Algo-rithm 9.11 will eventually halt, because for every B, OUT[B]
never shrinks; once a definition is added, it stays there forever. (See Exercise 9.2.6.) Since the set of all
definitions is finite, eventually there must be a pass of the while-loop during which nothing is added to
any OUT, and the algorithm then terminates. We are safe terminating then because if the OUT's have
not changed, the IN ' s will
not change on the next pass. And, if the IN'S do not change, the OUT's cannot, so on all subsequent
passes there can be no changes.
The number of nodes in the flow graph is an upper bound on the number of times around the while-
loop. The reason is that if a definition reaches a point, it can do so along a cycle-free path, and the
number of nodes in a flow graph is an upper bound on the number of nodes in a cycle-free path. Each
.
time around the while-loop, each definition progresses by at least one node along the path in question,
and it often progresses by more than one node, depending on the order in which the nodes are visited.
In fact, if we properly order the blocks in the for-loop of line (5), there is empirical evidence that the
average number of iterations of the while-loop is under 5 (see Section 9.6.7). Since sets of definitions
can be represented by bit vectors, and the operations on these sets can be implemented by logical
operations on the bit vectors, Algorithm 9.11 is surprisingly efficient in practice.
Example 9 . 1 2 : We shall represent the seven definitions d1, d2, • • • ,d>j in the flow graph of Fig.
9.13 by bit vectors, where bit i from the left represents definition d{. The union of sets is computed by
taking the logical OR of the corresponding bit vectors. The difference of two sets S — T is computed by
complementing the bit vector of T, and then taking the logical AND of that complement, with the bit
vector for S.
Shown in the table of Fig. 9.15 are the values taken on by the IN and OUT sets in Algorithm 9.11. The
initial values, indicated by a superscript 0, as in OUTfS]0 , are assigned, by the loop of line (2) of Fig.
9.14. They are each the empty set, represented by bit vector 000 0000. The values of subsequent passes
of the algorithm are also indicated by superscripts, and labeled IN [I?]1 and OUTfS]1 for the first pass
and m[Bf and OUT[S]2 for the second.
Suppose the for-loop of lines (4) through (6) is executed with B taking on the values
in that order. With B = B1, since OUT [ ENTRY ] = 0, [IN B1]-Pow(1) is the empty set, and OUT[P1]1
is genBl. This value differs from the previous value OUT[Si]0 , so
we now know there is a change on the first round (and will proceed to a second round).
Notice that after the second round, OUT [ B2 ] has changed to reflect the fact that d& also reaches the
beginning of B2 and is not killed by B2. We did not learn that fact on the first pass, because the path
from d6 to the end of B2, which is B3 -» B4 -> B2, is not traversed in that order by a single pass. That
is, by the time we learn that d$ reaches the end of B4, we have already computed IN[B2 ] and OUT [ B
2 ] on the first pass.
There are no changes in any of the OUT sets after the second pass. Thus, after a third pass, the
algorithm terminates, with the IN's and OUT's as in the final two columns of Fig. 9.15.
5. Live-Variable Analysis
Some code-improving transformations depend on information computed in the direction opposite to the
flow of control in a program; we shall examine one such example now. In live-variable analysis we
wish to know for variable x and point p whether the value of x at p could be used along some path in the
flow graph starting at p. If so, we say x is live at p; otherwise, x is dead at p.
An important use for live-variable information is register allocation for basic blocks. Aspects of this
issue were introduced in Sections 8.6 and 8.8. After a value is computed in a register, and presumably
used within a block, it is not necessary to store that value if it is dead at the end of the block. Also, if all
registers are full and we need another register, we should favor using a register with a dead value, since
that value does not have to be stored.
Here, we define the data-flow equations directly in terms of IN [5] and OUTpB], which represent
the set of variables live at the points immediately before and after block B, respectively. These
equations can also be derived by first defining the transfer functions of individual statements and
composing them to create the transfer function of a basic block. Define
1. defB as the set of variables defined (i.e., definitely assigned values) in B prior to any use of that
variable in B, and useB as the set of variables whose values may be used in B prior to any definition of
the variable.
Example 9 . 1 3 : For instance, block B2 in Fig. 9.13 definitely uses i. It also uses j before any
redefinition of j, unless it is possible that i and j are aliases of one another. Assuming there are no
aliases among the variables in Fig. 9.13, then uses2 = {i,j}- Also, B2 clearly defines i and j.
Assuming there are no aliases, defB2 = as well.
As a consequence of the definitions, any variable in useB must be considered live on entrance to block
B, while definitions of variables in defB definitely are dead at the beginning of B. In effect,
membership in defB "kills" any opportunity for a variable to be live because of paths that begin at B.
Thus, the equations relating def and use to the unknowns IN and OUT are defined as follows:
.
The first equation specifies the boundary condition, which is that no variables are live on exit from the
program. The second equation says that a variable is live coming into a block if either it is used before
redefinition in the block or it is live coming out of the block and is not redefined in the block. The third
equation says that a variable is live coming out of a block if and only if it is live coming into one of its
successors.
The relationship between the equations for liveness and the reaching-defin-itions equations should be
noticed:
Both sets of equations have union as the meet operator. The reason is that in each data-flow
schema we propagate information along paths, and we care only about whether any path with desired
properties exist, rather than whether something is true along all paths.
• However, information flow for liveness travels "backward," opposite to the direction of control flow,
because in this problem we want to make sure that the use of a variable x at a point p is transmitted to
all points prior to p in an execution path, so that we may know at the prior point that x will have its
value used.
INPUT: A flow graph with def and use computed for each block.
OUTPUT: m[B] and O U T [ £ ] , the set of variables live on entry and exit of each block B of the flow
graph.
.
6. Available Expressions
An expression x + y is available at a point p if every path from the entry node to p evaluates x + y, and
after the last such evaluation prior to reaching p, there are no subsequent assignments to x or y.5 For
the available-expressions data-flow schema we say that a block kills expression x + y if it assigns (or
may 5 N o te that, as usual in this chapter, we use the operator + as a generic operator, not necessarily
standing for addition.
Note that the notion of "killing" or "generating" an available expression is not exactly the same as that
for reaching definitions. Nevertheless, these notions of "kill" and "generate" behave essentially as they
do for reaching definitions.
The primary use of available-expression information is for detecting global common subexpressions.
For example, in Fig. 9.17(a), the expression 4 * i in block Bs will be a common subexpression if 4 * i is
available at the entry point of block B3. It will be available if i is not assigned a new value in block B2,
or if, as in Fig. 9.17(b), 4 * i is recomputed after i is assigned in B2.
We can compute the set of generated expressions for each point in a block, working from beginning to
end of the block. At the point prior to the block, no expressions are generated. If at point p set S of
.
expressions is available, and q is the point after p, with statement x = y+z between them, then we form
the set of expressions available at q by the following two steps.
Note the steps must be done in the correct order, as x could be the same as y or z. After we reach the
end of the block, S is the set of generated expressions for the block. The set of killed expressions is all
expressions, say y + z, such that either y or z is defined in the block, and y + z is not generated by the
block.
E x a m p l e 9.15 : Consider the four statements of Fig. 9.18. After the first, b + c is available. After the
second statement, a — d becomes available, but b + c is no longer available, because b has been
redefined. The third statement does not make b + c available again, because the value of c is
immediately changed.
After the last statement, a — d is no longer available, because d has changed. Thus no expressions are
generated, and all expressions involving a, b, c, or d are killed.
We can find available expressions in a manner reminiscent of the way reach-ing definitions are
computed. Suppose U is the "universal" set of all expressions appearing on the right of one or more
statements of the program. For each block B, let IN[B] be the set of expressions in U that are available
at the point just before the beginning of B. Let OUT[B] be the same for the point following the end
of B. Define e.genB to be the expressions generated by B and eJnills to be the set of expressions
in U killed in B. Note that I N , O U T , e_#en, and eJkill can all be represented by bit vectors. The
following equations relate the unknowns
.
T he above equations look almost identical to the equations for reaching definitions. Like reaching
definitions, the boundary condition is OUT [ ENTRY ] = 0, because at the exit of the E N T R Y node,
there are no available expressions.
The most important difference is that the meet operator is intersection rather than union. This operator is
the proper one because an expression is available at the beginning of a block only if it is available at the
end of all its predecessors. In contrast, a definition reaches the beginning of a block whenever it reaches
the end of any one or more of its predecessors.
The use of D rather than U makes the available-expression equations behave differently from those of
reaching definitions. While neither set has a unique solution, for reaching definitions, it is the solution
with the smallest sets that corresponds to the definition of "reaching," and we obtained that solution by
starting with the assumption that nothing reached anywhere, and building up to the solution. In that
way, we never assumed that a definition d could reach a point p unless an actual path
propagating d to p could be found. In contrast, for available expression equations we want the solution
with the largest sets of available expressions, so we start with an approximation that is too large and
work down.
It may not be obvious that by starting with the assumption "everything (i.e., the set U) is available
everywhere except at the end of the entry block" and eliminating only those expressions for which we
can discover a path along which it is not available, we do reach a set of truly available expressions. In
the case of available expressions, it is conservative to produce a subset of the exact set of available
expressions. The argument for subsets being conservative is that our intended use of the information is
to replace the computation of an available expression by a previously computed value. Not knowing an
expres-sion is available only inhibits us from improving the code, while believing an expression is
available when it is not could cause us to change what the program computes.
.
Example 9 . 1 6 : We shall concentrate on a single block, B2 in Fig. 9.19, to illustrate the effect of
the initial approximation of OUT[B2] on IN [ B 2 ] - Let G and K abbreviate e.genB2 and e-killB2,
respectively. The data-flow equations for block B2 are
INPUT: A flow graph with e-kills and e.gens computed for each block B. The initial block is B1.
OUTPUT: IN [5] and O U T [ 5 ] , the set of expressions available at the entry and exit of each block B
of the flow graph.
.