Introduction to Data-Flow
Analysis
1 The Data-Flow Abstraction
2 The Data-Flow Analysis Schema
3 Data-Flow Schemas on Basic
Blocks
4 Reaching Definitions
5 Live-Variable Analysis
6 Available Expressions
7 Summary
8 Exercises for Section 9.2
All the optimizations introduced in Section 9.1 depend on data-flow analysis. "Data-flow
analysis" refers to a body of techniques that derive information about the
flow of data along program execution paths. For example, one way to implement
global common subexpression elimination requires us to determine whether two
textually identical expressions evaluate to the same value along any possible
execution path of the program. As another example, if the result of an
assignment is not used along any subsequent execution path, then we can
eliminate the assignment as dead code. These and many other important questions
can be answered by data-flow analysis.
1. The Data-Flow Abstraction
Following Section 1.6.2, the execution of a program can be viewed as a
series of transformations of the program state, which consists of the values of
all the variables in the program, including those associated with stack frames
below the top of the run-time stack. Each execution of an intermediate-code
statement transforms an input state to a new output state. The input state is
associated with the program point before
the statement and the output state is associated with the program point after the statement.
When we analyze the behavior of a program, we must consider all the
pos-sible sequences of program points ("paths") through a flow graph
that the pro-gram 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. However, to begin our study, we shall concentrate on
the paths through a single flow graph for a single procedure.
Let us see what the flow graph tells us about the possible execution
paths.
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 E>2 , then
the program point after the last statement of By 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
Pi is the point immediately preceding a statement
and pi+i is the point
immediately following that same statement, or
pi is the end of some block and pi+1 is the beginning of a successor block.
In
general, there is an infinite number of possible execution paths through a
program, and there is no finite upper bound on the length of an execution path.
Program analyses summarize all the possible program states that can occur at a
point in the program with a finite set of facts. Different analyses may choose
to abstract out different information, and in general, no analysis is
necessarily a perfect representation of the state.
Example 9
. 8 : Even the simple program in Fig. 9.12 describes an unbounded number of
execution paths. Not entering the loop at all, the shortest com-plete execution
path consists of the program points (1,2,3,4,9) . The next shortest path executes one
iteration of the loop and consists of the points ( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 3 ,
4 , 9 ) . We know that, for example, the first time program point (5) is executed, the value of a is 1 due to definition d1. We say that d i reaches point (5) i n the f i r s t
iteration. I n subsequent iterations, ^ 3 reaches point (5) and the value of a
is 243.
In
general, it is not possible to keep track of all the program states for all
possible paths. 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.
Thus, we see that the same information may be summarized differently,
de-pending on the purpose of the analysis. •
2. The Data-Flow Analysis Schema
In each application of
data-flow analysis, 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.
We 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
se-mantics 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,
Control – Flow Constraints
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 con-
straints
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.
3. Data-Flow Schemas on Basic Blocks
While a
data-flow schema technically involves data-flow values at each point in the
program, we can save time and space by recognizing that what goes on inside a
block is usually quite simple. Control flows from the beginning to the end of
the block, without interruption or branching. Thus, we can restate the schema
in terms of data-flow values entering and leaving the blocks. We denote the
data-flow values immediately before and immediately after each basic block B by m[B]
and 0 U T [ S ] , respectively. The constraints involving m[B] and 0UT[B] can be derived from those involving w[s]
and OUT[s] for the various statements s in B as follows.
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 rewrit-ten
by substituting IN[B] and OVT[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 com-putes.
This issue is discussed briefly in the box on "Conservatism" and more
extensively in Section 9.3.4. In the
following subsections, we discuss some of the most important examples of problems
that can be solved by data-flow analysis.
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 . 3 Intuitively, 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.
A
definition of a variable x is a
statement that assigns, or may assign, a value to x. Procedure parameters, array accesses, and indirect references
all may have aliases, and it is not easy to tell if a statement is referring to
a particular variable x. Program
analysis must be conservative; if we do not
3 N o te
that the path may have loops, so we could come to another occurrence of d along the path, which does not
"kill" d.
Detecting Possible Uses Before
Definition
Here is how we use a solution to the reaching-definitions problem to
detect uses before definition. The trick is to introduce a dummy definition for
each variable x in the entry to the
flow graph. If the dummy definition of x
reaches a point p where x might be used, then there might be an
opportunity to use x before
definition. Note that we can never be abso-lutely certain that the program has
a bug, since there may be some reason, possibly involving a complex logical
argument, why the path along which p is
reached without a real definition of x can
never be taken.
know
whether a statement s is assigning a
value to x, we must assume that it may assign to it; that is, variable x after statement s may have either its original value before s or the new value created by s.
For the sake of simplicity, the rest of the chapter assumes that we are dealing
only with variables that have no aliases. This class of variables includes all
local scalar variables in most languages; in the case of C and C++, local variables whose addresses
have been computed at some point are excluded.
Example 9
. 9 : Shown in Fig. 9.13 is a flow graph
with seven definitions. Let us focus on
the definitions reaching block B2 • All the definitions in block B1 reach the
beginning of block B2. The definition d§: j = j - 1 in block B2 also reaches the beginning of block B2,
because no other definitions of j can be found in the loop leading back to B2. This definition, however, kills the
definition d2: j = n, preventing it from
reaching B% or J34 . The statement d4: i
= i+1 in B2 does not reach the beginning
of B2 though, because the variable i is always redefined by d^: i = u3 .
Finally, the definition d 6 : a = u2 also reaches the beginning of block B2.
By
defining reaching definitions as we have, we sometimes allow inaccuracies.
However, they are all in the "safe," or "conservative,"
direction. For example, notice our assumption that all edges of a flow graph can
be traversed. This assumption may not be true in practice. For example, for no
values of a and b can the flow of control actually reach statement 2 in the following program fragment:
if (a == b)
statement 1; else if (a == b) statement 2;
To decide
in general whether each path in a flow graph can be taken is an undecidable
problem. Thus, we simply assume that every path in the flow graph can be
followed in some execution of the program. In most applications of reaching
definitions, it is conservative to assume that a definition can reach a point
even if it might not. Thus, we may allow paths that are never be traversed in
any execution of the program, and we may allow definitions to pass through
ambiguous definitions of the same variable safely.
Conservatism in Data-Flow
Analysis
Since all data-flow schemas compute approximations to the ground truth
(as defined by all possible execution paths of the program), we are obliged to
assure that any errors are in the "safe" direction. A policy decision
is safe (or conservative) if it never allows us to change what the program computes. Safe policies may,
unfortunately, cause us to miss some code improvements that would retain the
meaning of the program, but in essen-tially all code optimizations there is no
safe policy that misses nothing. It would generally be unacceptable to use an
unsafe policy — one that sped up the code at the expense of changing what the
program computes.
Thus, when designing a data-flow schema, we must be conscious of how the
information will be used, and make sure that any approximations we make are in
the "conservative" or "safe" direction. Each schema and
application must be considered independently. For instance, if we use reaching
definitions for constant folding, it is safe to think a definition reaches when
it doesn't (we might think x is not a
constant, when in fact it is and could have been folded), but not safe to think
a definition doesn't reach when it does (we might replace x by a constant, when the program would at times have a value for x other than that constant).
Transfer Equations for
Reaching Definitions
We shall
now set up the constraints for the reaching definitions problem. We start by
examining the details of a single statement. Consider a definition
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.
As
discussed in Section 9.2.2, 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.
Example 9
. 1 0 : The gen set for the basic block
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.
Control - Flow Equations
Next, we
consider the set of constraints derived from the control flow between basic
blocks. Since a definition reaches a program point as long as there exists at
least one path along which the definition reaches, O U T [ P ] C m[B] whenever there is a control-flow
edge from P to B. However, since a definition cannot reach a point unless there is
a path along which it reaches, w[B]
needs to be no larger than the union of the reaching definitions of all the
predecessor blocks. That is, it is safe to assume
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.
Iterative
Algorithm for Reaching Definitions
We assume
that every control-flow graph has two empty basic blocks, an ENTRY node, which
represents the starting point of the graph, and an EXIT node to which all exits
out of the graph go. Since no
definitions reach the beginning of the graph, the transfer function for the
ENTRY block is a simple constant
function that returns 0 as an answer.
That is, O U T [ E N T R Y ] = 0.
The
reaching definitions problem is defined by the following equations:
These
equations can be solved using the following algorithm. The result of the
algorithm is the least fixedpoint of
the equations, i.e., the solution whose assigned values to the IN ' s and OUT's is contained in the
corresponding values for any other solution to the equations. The result of the
algorithm below is acceptable, since any definition in one of the sets IN or OUT surely must reach the point
described. It is a desirable solution, since it does not include any
definitions that we can be sure do not reach.
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. •
Intuitively,
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).
Then we
consider B = B2 and compute
This
computation is summarized in Fig. 9.15. For instance, at the end of the first
pass, OUT [ 5 2 ] 1 = 001 1100, reflecting the fact that d4 and d5 are
generated in B2, while d3 reaches the beginning of B2 and is not killed in B2.
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.
To solve a backward problem, instead of initializing O U T [ E N T R Y ]
, we initialize I N [EXIT ] . Sets I N and
O U T have their roles interchanged, and use and def substitute for gen
and kill, respectively. As for reaching definitions, the solution to the
liveness equations is not necessarily unique, and we want the so-lution with
the smallest sets of live variables. The algorithm used is essentially a
backwards version of Algorithm 9.11.
Algorithm 9 . 1 4 : Live-variable analysis.
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.
METHOD: Execute the program in Fig. 9.16. •
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.
assign) x or y and does not subsequently recompute x + y. A block
generates expression x + y if it definitely evaluates x + y and does not
subsequently define x or y.
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.
Add to S the expression y + z.
Delete from S any expression involving variable x.
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
Algorithm 9 . 1 7 : Available
expressions.
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.
METHOD: Execute the algorithm of Fig. 9.20. The explanation of the steps
is similar to that for Fig. 9.14. •
Figure 9.20: Iterative algorithm
to compute available expressions
7. Summary
In this section, we have discussed three instances of data-flow
problems: reach-ing definitions, live variables, and available expressions. As
summarized in Fig. 9.21, the definition of each problem is given by the domain
of the data-flow values, the direction of the data flow, the family of transfer
functions, the boundary condition, and the meet operator. We denote the meet
operator generically as A.
The last row shows the initial values used in the iterative algorithm.
These values are chosen so that the iterative algorithm will find the most
precise solution to the equations. This choice is not strictly a part of the
definition of the data-flow problem, since it is an artifact needed for the
iterative algorithm. There are other ways of solving the problem. For example,
we saw how the transfer function of a basic block can be derived by composing
the transfer functions of the individual statements in the block; a similar
compositional approach may be used to compute a transfer function for the
entire procedure, or transfer functions from the entry of the procedure to any
program point. We shall discuss such an approach in Section 9.7.
8. Exercises for Section 9.2
Exercise 9 . 2 . 1 : For the flow graph of Fig. 9.10 (see the exercises for Sec-tion 9.1),
compute
The gen and kill sets for
each block.
The IN and O U T sets for
each block.
Exercise 9 . 2 . 2 : For the flow graph of Fig. 9.10, compute the e_#en, eJkill, I N , and O U T sets for available expressions.
Exercise 9 . 2 . 3 : For the flow graph of Fig. 9.10, compute the def, use, INJ and O U T sets for live variable analysis.
! Exercise 9 . 2 . 4: Suppose V is the set of complex
numbers. Which of the following
operations can serve as the meet operation for a semilattice on V?
a) Addition: (a + ib) L (c + id) = (a + b) + i(c + d).
b) Multiplication: (a + ib) L (c + id) = (ac — bd) + i(ad +
be).
Why the Available-Expressions
Algorithm Works
We need to explain why starting all O
U T ' s except that for the entry block with U, the set of all expressions, leads to a conservative solution to
the data-flow equations; that is, all expressions found to be available really are available. First, because
intersection is the meet operation in this
data-flow schema, any reason that an expression x + y is found not to be available at a point will propagate
forward in the flow graph, along all possible paths, until x + y is recomputed and becomes available again. Second, there are
only two reasons x + y could be
unavailable:
x + y is killed in block B because x or
y is defined without a subse-quent computation of x + y. In this case, the first time we apply the transfer
function fs, x + y will be removed from OVT[B}.
2. x + y is never computed along some path. Since x + y is never in OUT [ ENTRY ] , and
it is never generated along the path in question, we can show by induction on
the length of the path that x + y is
eventually removed from I N ' s and O U T ' s along that path.
Thus, after changes subside, the solution provided by the iterative
algo-rithm of Fig. 9.20 will include only truly available expressions.
c) Componentwise minimum: (a + ib) L (c + id) = min(«, c) + i mm(b, d).
d) Componentwise maximum: (a + ib) L (c + id) — max(a, c) + imax(6, of).
Exercise 9 . 2 . 5 : We claimed
that if a block B consists of n statements, and the ith. statement has gen and
kill sets gerii and kilk, then the transfer function for block B has gen and
kill sets gens and kills given by
Prove this claim by induction on n.
Exercise 9 . 2 . 6 : Prove by
induction on the number of iterations of the for-loop of lines (4) through (6)
of Algorithm 9.11 that none of the IN ' s or OUT ' s ever shrinks. That is,
once a definition is placed in one of these sets on some round, it never
disappears on a subsequent round.
! Exercise 9 . 2 . 7 : Show the correctness of
Algorithm 9.11. That is, show that
If definition d is put in IN [B] or OUT[2?], then there is a path from d to the beginning or end of block B, respectively, along which the
variable defined by d might not be
redefined.
b) If definition d is not put in w[B]
or O U T [ . B ] , then there is no path from d to the beginning or end of
block B, respectively, along which the variable defined by d might not be redefined.
Exercise 9 . 2 . 8: Prove the following about Algorithm 9.14: a) The I N ' s and
OUT's never shrink.
b) If variable x is put in m[B] or 0UTp9], then there is a path
from the beginning or end of block B,
respectively, along which x might be
used.
If variable x is not put in IN[JB] or
OUTp9], then there is no path from the beginning or end of block B, respectively, along which x might be used.
Exercise 9 . 2 . 9 :
Prove the following about Algorithm 9.17:
The I N ' s and OUT's never grow; that is, successive values of these
sets are subsets (not necessarily proper) of their previous values.
If expression e is removed from IN[B]
or OUTpH], then there is a path from the entry of the flow graph to the
beginning or end of block B, respectively,
along which e is either never computed, or after its last computation, one of
its arguments might be redefined.
If expression e remains in IN[B] or OUTpB], then along every path
from the
entry of the flow graph to the beginning or end of block B, respectively, e is computed, and
after the last computation, no argument of e could be redefined.
Exercise 9 . 2 . 1 0 : The
astute reader will notice that in Algorithm 9.11 we could have saved some time by initializing OUTpB] to gens for all blocks B. Likewise,
in Algorithm 9.14 we could have initialized m[B]
to gens- We did
not do so for uniformity in the treatment of the subject, as we shall see in
Algorithm 9.25. However, is it possible to initialize OUTpB] to e^gens in Algorithm 9.17? Why or why not?
Exercise 9 . 2 . 1 1 : Our
data-flow analyses so far do not take advantage of the semantics of conditionals. Suppose we find at the end of a basic
block a test such as
if (x < 10) goto . . .
How could we use our understanding of what the test x < 10 means to improve our knowledge of reaching definitions?
Remember, "improve" here means that we eliminate certain reaching
definitions that really cannot ever reach a certain program point.
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.