Compiler Design
Compiler Design
Compiler Design
Analysis and Transformation
123
Sebastian Hack
Programming Group
Universitt des Saarlandes
Saarbrcken, Germany
Helmut Seidl
Fakultt fr Informatik
Technische Universitt Mnchen
Garching, Germany
Reinhard Wilhelm
Compiler Research Group
Universitt des Saarlandes
Saarbrcken, Germany
ISBN 978-3-642-17547-3
DOI 10.1007/978-3-642-17548-0
ISBN 978-3-642-17548-0
(eBook)
Preface
vi
Preface
Preface
vii
Helmut Seidl
Reinhard Wilhelm
Sebastian Hack
General literature
The list of monographs that give an overview of static program analysis and abstract
interpretation is surprisingly short. The book by Matthew S. Hecht [Hec77],
summarizing the classical knowledge about data-flow analysis is still worth reading.
The anthology edited by Steven S. Muchnick and Neil D. Jones [MJ81], which was
published only a few years later, contains many original and influential articles about
the foundations of static program analysis and, in particular, the static analysis of
recursive procedures and dynamically allocated data structures. A similar collection
of articles about the static analysis of declarative programs was edited by Samson
Abramsky and Chris Hankin [AH87]. A comprehensive and modern introduction is
offered by Flemming Nielson, Hanne Riis Nielson and Chris Hankin [NNH99].
Several comprehensive treatments of compilation contain chapters about static
analysis [AG04, CT04, ALSU07]. Steven S. Muchnicks monograph Advanced
Compiler Design and Implementation [Muc97] contains an extensive treatment.The Compiler Design Handbook, edited by Y.N. Srikant and Priti Shankar
[SS03], offers a chapter about shape analysis and about techniques to analyze objectoriented programs.
Ongoing attempts to prove compiler correctness [Ler09, TL09] have led to an
increased interest in the correctness proofs of optimizing program transformations.
Techniques for the systematic derivation of correct program transformations are
described by Patrick and Radhia Cousot [CC02]. Automated correctness proofs of
optimizing program transformations are described by Sorin Lerner [LMC03,
LMRC05, KTL09].
ix
Inhalt
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
7
8
11
16
27
32
40
43
54
67
83
89
97
102
108
114
Interprocedural Optimization . . . . . . . . . . . .
2.1
Programs with Procedures . . . . . . . . . . .
2.2
Extended Operational Semantics . . . . . .
2.3
Inlining . . . . . . . . . . . . . . . . . . . . . . . .
2.4
Tail-Call Optimization . . . . . . . . . . . . .
2.5
Interprocedural Analysis . . . . . . . . . . . .
2.6
The Functional Approach . . . . . . . . . . .
2.7
Interprocedural Reachability . . . . . . . . .
2.8
Demand-Driven Interprocedural Analysis
2.9
The Call-String Approach . . . . . . . . . . .
2.10 Exercises. . . . . . . . . . . . . . . . . . . . . . .
2.11 Literature . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
115
115
117
121
123
124
125
131
132
135
137
139
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
xi
xii
Inhalt
.
.
.
.
.
.
.
.
.
.
141
142
143
146
147
149
155
159
166
170
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
171
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
175
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Chapter 1
1.1 Introduction
This section presents basic techniques to improve the quality of compiler-generated
code. The quality metric need not be a priori fixed. It could be the execution time, the
required space, or the consumed energy. This book, however, is primarily concerned
with methods to improve the execution time of programs.
We now give several examples of how to improve the execution time of programs.
One strategy to improve the efficiency of programs is to avoid superfluous computations. A computation may be superfluous when it has already been performed, and
when a repetition would provably always produce the same result. The compiler can
avoid this recomputation of the same result if it takes care to store the result of the
first computation. The recomputation can then be avoided by accessing this stored
value.
The execution time of a program can be also reduced if some of the computations
can already be done at compile time. Constant folding replaces expressions whose
value is already known at compile time by this value. This optimization supports the
development of generic programs, often called program families. These are parametrized in a number of variables and thus can be instantiated to many different
variants by supplying different combinations of parameter values. This is good and
effective development practice, for instance, in the embedded-systems industry. One
generic power-train control program may be instantiated to many different versions
for different car engines. Constant folding eliminates the loss in efficiency that could
result from such a programming style.
Checks for run-time errors can be eliminated if it is clear that they would always
fail, that is, if these errors would provably never happen. A good example is the check
for index out of bounds. It checks the indices of arrays against their lower and upper
bounds. These checks can be avoided if the indices provably always lie within these
bounds.
Another idea to improve the efficiency of programs is to move computations
from more frequently executed program parts into less frequently executed parts.
H. Seidl et al., Compiler Design, DOI: 10.1007/978-3-642-17548-0_1,
Springer-Verlag Berlin Heidelberg 2012
1.1 Introduction
itive formulations of algorithms. It is then the duty of the compiler to generate efficient
target programs.
Optimizing program transformations ought to preserve the semantics of the program,
as defined through the semantics of the programming language in which the program
is written.
Example 1.1.2 Consider the transformation:
y f() + f();
==
y 2 f();
The idea behind the optimization is to save the evaluation of the second call of the
function f. However, the program resulting from this transformation is only equivalent
to the original program if the second call to f is guaranteed to produce the same result
and if the call does not produce a side effect. This last condition is not immediately
clear for functions written in an imperative language.
So-called program optimizations are not correct if they change the semantics of the
program. Therefore, most optimizing transformations have an associated applicability condition. This is a sufficient condition for the preservation of the semantics of
programs. Checking the satisfaction of these applicability conditions is the duty of
static program analysis. Such analyses need to be automatic, that is, run without user
intervention, as they will have to be performed by the compiler.
A careful treatment of the issue of semantics preservation needs several proofs.
First, a proof is needed that the applicability condition is, in fact, a sufficient condition
for semantics preservation. A second proof is needed that the analysis that is to
determine the applicability is correct, will never give wrong answers to the question
posed by the applicability condition. Both proofs refer to an operational semantics
as their basis.
Several optimizations are effective across several classes of programming languages. However, each programming language and also each class of programming
languages additionally require specific optimizations, designed to improve the efficiency of particular language constructs. One such example is the compile-time
removal of dynamic method invocations in object-oriented programs. A static method
call, which replaces a dynamic call, can be inlined and thus opens the door for further
optimizations. This is very effective since methods in object-oriented programs are
often rather small. In Fortran, on the other hand, inlining does not play a comparably large role. For Fortran, the parallelization or vectorization of nested loops has
greater impact.
The programming language, in particular its semantics, also has a strong influence on the efficiency and the effectiveness of program analyses. The programming
language may enforce restrictions whose validation would otherwise require an enormous effort. A major problem in the analysis of imperative programs is the determination of dependencies between the statements in programs. Such dependencies
restrict the compilers possibility to reorder statements to better exploit the resources
of the target machine. The unrestricted use of pointers, as in the C programming language, makes this analysis of dependencies difficult due to the alias-problem created
through pointers. The more restricted use of pointers in Java eases the corresponding
analysis.
Example 1.1.3 Let us look at the programming language Java. Inherently inefficient language constructs are the mandatory checks for indices out of array bounds,
dynamic method invocation, and storage management for objects. The absence of
pointer arithmetic and of pointers into the stack increases the analyzability of Java
programs. On the other hand, dynamic loading of classes may ruin the precision of
Java analyses due to the lack of information about their semantics and their implementation. Further tough challenges for an automatic static analysis are offered by
language constructs such as exceptions, concurrency, and reflection, which still may
be useful for the Java programmer.
We have stressed in the preface that sound static program analysis has become
a verification technology. It is therefore interesting to draw the connection to the
problem of proving the correctness of Java programs. Any correctness proof needs
a formally specified semantics of the programming language. Quite some effort went
into the development of such a semantics for Java. Still, Java programs with a formal
correctness proof are rather rare, not because of a principal impossibility, but due to
the sheer size of the necessary effort. Java just has too many language constructs,
each with its non-trivial semantics.
For this reason, we will not use Java as our example language. Instead we use a
small subset of an imperative programming language. This subset is, on the one
hand, simple enough to limit the descriptional effort, and is, on the other hand, realistic enough to include essential problems of actual compilers. This programminglanguage fragment can be seen as an intermediate language into which source programs are translated. The int variables of the program can be seen as virtual registers.
The compiler backend will, during register allocation, assign physical registers to
them as far as such physical registers are available. Such variables can also be used
to store addresses for indirect memory accesses. Arithemtic expressions represent
computations of, in our fragment, int values. Finally, the fragment contains an abitrarily large array M, into which int values can be stored and from which they can
be retrieved. This array can be imagined as the whole (virtual) memory allocated to
a program by the operating system.
The separation between variables and memory may, at first glance, look somewhat
artificial. It is motivated by the wish to avoid the alias problem. Both a variable x
and a memory-access expression M[] denote containers for values. The identity of a
memory cell denoted by M[e] is not directly visible because it depends on the value of
the expression e. In general, it is even undecidable whether M[e1 ] and M[e2 ] denote
the same memory cell. This is different for variables: A variable name x is the only
name by which the container associated with x can be accessed. This is important for
many program analyses: If the analysis is unable to derive the identity of the memory
cell denoted by M[e] in a write access then no assumptions can be made about the
1.1 Introduction
contents of the rest of memory. The analysis looses much precision. The derivation
of assumptions about the contents of containers associated with variables is easier
since no indirect access to their containers is possible.
Our language fragment has the following constructs:
variables :
arithmetic expressions :
assignments :
reading access to memory :
writing access to memory :
conditional statement :
unconditional jump :
x
e
x e
x M[e]
M[e1 ] e2
if(e) s1 else s2
gotoL
Note that we have not included explicit loop constructs. These can be realized by
conditional and unconditional jumps to labeled program points. Also missing so far
are functions and procedures. This chapter is therefore restricted to the analysis and
optimization of single functions.
Example 1.1.4 Let us again consider the function swap() of Example 1.1.1. How
would a compiler translate the body of this function into our language fragment?
The array a can be allocated into some section of the memory M. Accesses to array
components need to be translated into explicit address calculations. The result of a
schematic, nonoptimized translation could be:
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10 :
11 :
12 :
13 :
A1 A0 + 1 i;
//
A0 = &a[0]
//
R1 = a[i]
R1 M[A1 ];
A2 A0 + 1 j;
//
R2 = a[ j]
R2 M[A2 ];
if (R1 > R2 ) {
A0 + 1 j;
A3
t
M[A3 ];
A0 + 1 j;
A4
A0 + 1 i;
A5
M[A5 ];
R3
M[A4 ] R3 ;
A0 + 1 i;
A6
M[A6 ] t;
}
//
We assume that variable A0 holds the start address of the array a. Note that this
code makes explicit the inherent inefficiencies discussed in Example 1.1.1. Which
optimizations are applicable to this code?
Optimization 1: 1 R == R
The scaling factor generated by an automatic (and schematic) translation of array
indexing can be dispensed with if this factor is 1 as is the case in the example.
A2 = A3 = A4
In addition, the memory accesses M[A1 ] and M[A5 ] as well as the accesses M[A2 ]
and M[A3 ] will deliver the same values:
M[A1 ] = M[A5 ]
M[A2 ] = M[A3 ]
Therefore, the variables R1 and R3 , as well as the variables R2 and t also contain the
same values:
R2 = t
R1 = R3
If a variable x already contains the value of an expression e whose value is required
then xs value can be used instead of reevaluating the expression e. The program can
be greatly simplified by exploiting all this information:
A1 A0 + i;
R1 M[A1 ];
A2 A0 + j;
R2 M[A2 ];
if (R1 > R2 ) {
M[A2 ] R1 ;
M[A1 ] R2 ;
}
The temporary variable t as well as the variables A3 , A4 , A5 , and R3 are now superfluous and can be eliminated from the program.
The following table lists the achieved savings:
+
*
load
store
>
Before
After
6
6
4
2
1
6
2
0
2
2
1
2
1.1 Introduction
A:
B:
z 1;
y M[5];
x1 y + z ;
...
x2 y + z ;
It seems like at program point B, the expression y + z will be evaluated a second time
yielding the same value. This is true under the following conditions: The occurrence
of y + z at program point B is always evaluated after the one at program point A,
and the values of the variables y and z have the same values before B that they had
before A.
Our conclusion from the example is that for a systematic treatment of this optimization we need to be able to answer the following questions:
Will one evaluation of an expression always be executed before another one?
Does a variable always have the same value at a given program point that it had at
another program point?
To answer these types of questions, we need several things: an operational semantics,
which defines what happens when a program is executed, and a method that identifies
redundant computations in programs. Note that we are not so ambitious as to attempt
to identify all redundant computations. This problem is undecidable. In practice,
the method to be developed should at least find some redundant computations and
should never classify as redundant a computation that, in fact, is not redundant.
A section of the control-flow graph for the body of the function swap is shown in
Fig. 1.1. Sometimes, we omit an edge label ;. A conditional statement with condition
e in a program has two corresponding edges in the control-flow graph. The one
labeled with NonZero(e) is taken if the condition e is satisfied. This is the case
when e evaluates to some value not equal to 0. The edge labeled with Zero is taken
if the condition is not satisfied, i.e., when e evaluates to 0.
Computations are performed when paths of the control-flow graph are traversed.
They transform the program state. Program states can be represented as pairs
s = (, )
start
A1 A0 + 1 i
R1 M [A1 ]
A2 A0 + 1 j
R2 M [A2 ]
Zero (R1 > R2 )
stop
A3 A0 + 1 j
The function maps each program variable to its actual value, and the function
maps each memory address to the actual contents of the corresponding memory cell.
For simplicity, we assume that the values of variables and memory cells are integers.
The types of the functions and , thus, are:
: Vars int value of variables
: N int memory contents
An edge k = (u, lab, v) with source vertex u, target vertex v and label lab defines
a transformation [[k]] of the state before the execution of the action labeling the edge
to a state after the execution of the action. We call this transformation the effect of the
edge. The edge effect need not be a total function. It could also be a partial function.
Program execution in a state s will not execute the action associated with an edge
if the edge effect is undefined for s. There may be two reasons for an edge being
undefined: The edge may be labeled with a condition that is not satsified in all states,
or the action labeling the edge may cause a memory access outside of a legal range.
The edge effect [[k]] of the edge k = (u, lab, v) only depends on its label lab:
[[k]] = [[lab]]
The edge effects [[lab]] are defined as follows:
[[; ]] (, ) = (, )
[[NonZero(e)]] (, ) = (, )
[[Zero(e)]] (, ) = (, )
[[e]] } , )
[[x e]] (, ) = ( {x
[[x M[e]]] (, ) = ( {x
([[e]])} , )
[[e2 ]]} )
[[M[e1 ] e2 ]] (, ) = (, {[[e1 ]]
if [[e]]
=0
if [[e]] = 0
10
An empty statement does not change the state. Conditions NonZero(e) and Zero(e),
represent partial identities; the associated edge effects are only defined if the conditions are satisfied, that is if the expression e evaluated to a value not equal to or
equal to 0, resp. They do, however, not change the state. Expressions e are evaluated by an auxiliary function [[e]], which takes a variable binding of the programs
variables and calculates es value in the valuation . As usual, this function is
defined by induction over the structure of expressions. This is now shown for some
examples:
[[x + y]] {x
7, y
1} = 6
[[(x = 4)]] {x
5}
= 0 = 1
The operator denotes logical negation.
An assignment x e modifies the -component of the state. The resulting
holds the value [[e]] for variable x, that is, the value obtained by evaluating e
in the old variable binding . The memory M remains unchanged by this assignment. The formal definition of the change to uses an operator . This operator modifies a given function such that it maps a given argument to a given new
value:
d
if y x
d}(y) =
{x
(y) otherwise
A load action, x M[e], is similar to an assignment with the difference that the
new value of variable x is determined by first calculating a memory address and then
loading the value stored at this address from memory.
The store operation, M[e1 ] e2 , has the most complex semantics. Values of
variables do not change. The following sequence of steps is performed: The values
of the expressions e1 , e2 are computed. e1 s value is the address of a memory cell at
which the value of e2 is stored.
We assume for both load and store operations that the address expressions deliver
legal addresses, i.e., values > 0.
Example 1.3.1 An assignment x x + 1 in a variable binding {x
5} results in:
[[x x + 1]] ({x
5}, ) = (, )
where:
= {x
5} {x
[[x + 1]] {x
5}}
= {x
5} {x
6}
= {x
6}
We have now established what happens when edges of the control-flow graph are
traversed. A computation is (the traversal of) a path in the control-flow graph leading
11
12
k = (u, lab, v). The set of assignments available at the target of k is determined
according to:
[[; ]] A = A
A = A
[[NonZero(e)]] A = [[Zero(e)]]
(A\Occ(x)) {x e} if x
Vars(e)
[[x e]] A =
A\Occ(x)
otherwise
[[x M[e]]] A = A\Occ(x)
[[M[e1 ] e2 ]] A = A
where Occ(x) denotes the set of all assignments in which x occurs either on the left or
in the expression on the right side. An empty statement and a condition do not change
the set of available assignments. Executing an assignment to x means evaluating the
expression on the right side and assigning the resulting value to x. Therefore, all
assignments that contain an occurrence of x are removed from the available-set.
Following this, the actual assignment is added to the available-set provided x does
not occur in the right side. The abstract edge effect for loads from memory looks
similar. Storing into memory does not change the value of any variable, hence, A
remains unchanged.
The abstract effects, which were just defined for each type of label, are composed
to an abstract effect [[]] for a path = k1 . . . kn in the following way:
[[]] = [[kn ]] . . . [[k1 ]]
The set of assignments available at the end of a path from the entry point of the
program to program point v is therefore obtained as:
[[]] = [[kn ]] (. . . ([[k1 ]] ) . . .)
Applying such a function associated with a path can be used to determine which
assignments are available along the path. However, a program will typically have
several paths leading to a program point v. Which of these paths will actually be
taken at program execution may depend on program input and is therefore unknown
at analysis time. We define an assignment x e to be definitely available at a
program point v if it is available along all paths leading from the entry node of the
program to v. Otherwise, x e is possibly not available at v. Thus, the set of
assignments definitely available at a program point v is:
A [v]
{[[]] | : start v}
where start v denotes the set of all paths from the entry point start of the
program to the program point v. The sets A[v] are called the merge-over-all-paths
(MOP) solution of the analysis problem. We temporarily postpone the question of
13
A1 A + 7
A1 A + 7
B1 M [A1 ]
B1 M [A1 ]
B2 B1 1
B2 B1 1
A2 A + 7
A2 A1
M [A2 ] B2
M [A2 ] B2
how to compute these sets. Instead, we discuss how the analysis information can be
used for optimizing the program.
Transformation RE:
An assignment x e is replaced by an assignment x y, if y e is definitely
available at program point u just before this assignment, i.e., y e is contained in
the set A [u]. This is formally described by the following graph rewrite rule:
u
y e A [u]
xe
u
xy
14
15
0
y1
Zero(x > 1)
5
NonZero(x > 1)
2
y xy
3
xx1
4
A[0]
A[1] (A[0]\Occ(y)) {y 1}
A[1] A[4]
A[2] A[1]
A[3] A[2]\Occ(y)
A[4] A[3]\Occ(x)
A[5] A[1]
The abstract edge effects of our analysis, however, are total functions. They do not
know about infeasibility. Such a path would be considered in forming the intersection
in the definition of definite availability and may pollute the information if this path
does not contain an assignment available on all other paths.
A second reason is the following: Assume that the assignment x y + z is
available at program point u, and that there exists an edge k = (u, y e, v) leaving
u. Assume further that the value of e is always the one that y has at u. In this case,
the transformation replacing y + z by x would still be correct although x y + z
would no longer be recognized as available at v.
An important question remains: How are the sets A [u] computed? The main idea
is to derive from the program a system of inequalities that characterizes these values:
A[start]
A[v]
[[k]] (A[u])
The first inequality expresses the assumption that no assignments are available at
the entry of the program. Further, each edge k leading from a node u to a node v
generates an inequality of the second kind. [[k]] (A[u]) are the assignments that
are propagated as available along the edge k = (u, lab, v), either since they were
available at u and survived [[k]] or since they were made available by [[k]]. This set
is at most available at v since other edges may target v along which these assignments
might not be available.
Example 1.4.3 Let us consider the program implementing the factorial function as
in Fig. 1.3. We see that the system of inequalities can be produced from the controlflow graph and the abstract edge transformers in a straightforward way. The only
assignment whose left-side variable does not also occur on the right side is y 1.
The complete lattice for the analysis of available assignments therefore consists of
only two elements, and {y 1}. Correspondingly, Occ(y) = {y 1} and
Occ(x) = hold.
16
Figure 1.4 shows a trivial solution of this system of inequalities. In this case, this
is the only solution. In general, there could be several solutions. In the availableassignment analysis, we are interested in largest sets. The larger the sets, the more
assignments have been shown to be available, and the more optimizations can be
performed. In consequence, we consider an analysis more precise that identifies
more assignments as available.
In this case, the largest solution is the best solution. The question is, does a best
solution always exist? If yes, can it be efficiently computed? We generalize the
problem a bit to be able to systematically answer the question as to the existence
of best solutions of systems of inequalities and as to their efficient computation.
This general treatment will provide us universal algorithms for solving virtually all
program-analysis problems in this book.
The first observation is that the set of possible values for the unknowns A[v]
forms a partial order with respect to the subset relation . The same holds for the
superset relation . These partial orders have the additional property that each subset
of X has a least upper bound and a greatest lower bound, namely the union and the
intersection, respectively, of the sets in X . Such a partial order is called a complete
lattice.
A further observation is that the abstract edge transformers [[k]] are monotonic
functions, that is, they preserve the ordering relation between values:
[[k]] (B1 ) [[k]] (B2 ) if B1 B2
17
aa
a b b a = a = b
a b b c = a c
reflexivity
antisymmetry
transitivity
The sets we consider in this book consist of information at program points about
potential or definite program behaviors. In our running example, such a piece of
information at a program point is a set of available assignments. The ordering relation
indicates precision. By convention, less means more precise. More precise in the
context of program optimizations should mean enabling more optimizations. For the
available-assignments analysis, more available assignments means potentially more
enabled optimizations. So, the ordering relation is the superset relation .
We give some examples of partial orders, representing lattices graphically as
directed graphs. Vertices are the lattice elements. Edges are directed upwards and
represent the relation. Vertices not connected by a sequence of edges are incomparable by .
1. The set 2{a,b,c} of all subsets of the set {a, b, c} together with the relation :
a, b, c
a, b
a, c
b, c
-1
for all x X
18
Not every subset of a partially ordered set has an upper bound, let alone a least upper
bound. The set {0, 2, 4} has the upper bounds 4, 5, . . . in the partially ordered set Z
of integer numbers, with the natural order , while the set {0, 2, 4, . . .} of all even
numbers has no upper bound.
A partial order D is a complete lattice if each subset
X D possesses a least
upper bound. This least upper bound is represented as X . Forming the least upper
bound of a set of elements is an important operation in program analysis. Let us
consider the situation that several edges of the control-flow graph have the same target
node v. The abstract edge effects associated with these edges propagate different
information towards v. The least upper bound operator then can be applied to combine
the incoming information in a sound way to a value at v.
Each element is an upper bound of the empty set of elements of D. The least
upper bound of the empty set, therefore, is less than or equal to any other element
of the complete lattice. This least element is called the bottom element of the lattice.
The set of all elements of a complete lattice also possesses an upper bound. Each
complete lattice therefore also has a greatest element, , called the top element. Let
us consider the partial orders of our examples. We have:
1. The set D = 2{a,b,c} of all subsets of the basic set {a, b, c} and, in general, of
each base set together with the subset relation is a complete lattice.
2. The set Z of the integer numbers with the partial order is not a complete lattice.
3. The set Z together with the equality relation = is also not a clomplete lattice.
A complete lattice, however, is obtained if an extra least element, , and an
extra greatest element, , is added:
-2
-1
This lattice Z
= Z {, } contains only a minimum of pairs in the ordering
relation. Such lattices are called flat.
In analogy to upper and least upper bounds, one can define lower and greatest lower
bounds for subsets of partially ordered sets. For a warm-up, we prove the following
theorem:
Theorem 1.5.1 Each subset X of a complete lattice D has a greatest lower bound
X.
19
Fig. 1.5 The least upper bound and the greatest lower bound for a subset X
greater than or equal to the least upper bound of U , i.e., g x. Since x was an
arbitrary element, g is in deed a lower bound of X .
Since g is an upper bound of U and therefore greater than or equal to each element
in U , i.e., u g for all u U , g is the greatest lower bound of X , which completes
the proof.
Figure 1.5 shows a complete lattice, a subset, and its greatest lower and least upper
bounds. That each of its subsets has a least upper bound makes a complete lattice
out of a partially ordered set. Theorem 1.5.1 says that each subset also has a greatest
lower bound.
Back to our search for ways to determine solutions for systems of inequalities!
Recall that the unknowns in the inequalities for the analysis of available assignments
are the sets A[u] for all program points u. The complete lattice D of values for
these unknowns is the powerset lattice 2Ass , where the partial order is the superset
relation .
All inequalities for the same unknown v can be combined into one inequality by
applying the least upper bound operator to the right sides of the original inequalities.
This leads to the form:
A[start]
= start
A[v]
{[[k]] (A[u]) | k = (u, lab, v) edge} for v
This reformulation does not change the set of solutions due to
x d1 . . . x dk iff x
{d1 , . . . , dk }
20
f i (x1 , . . . , xn )
i = 1, . . . , n
composition
( f 1 f 2 ) x = (a1 a2 ) x b1 b2
union
i = 1, . . . , n
()
21
where the functions f i : Dn D that define the right sides of the inequalities are
monotonic. We exploit that Dn is a complete lattice if D is one. We combine the n
functions f i to one function f : Dn Dn to simplify the presentation of the underlying problem. This function f is defined through f (x1 , . . . , xn ) = (y1 , . . . , yn ),
where yi = f i (x1 , . . . , xn ). It turns out that this constructions leads from monotonic
component functions to a monotonic combined function. This transformation of the
problem has reduced our problem to one of finding a least solution for a single
inequality x f x, however in the slightly more complex complete lattice Dn .
The search proceeds in the following way: It starts with an element d that is as
small as possible, for instance, with d = = (, . . . , ), the least element in Dn .
In case d f d holds, a solution has been found. Otherwise, d is replaced by f d
and tested for being a solution. If not, f is applied to f d and so on.
Example 1.5.2 Consider the complete lattice D = 2{a,b,c} with the partial order
= and the system of inequalities:
x1 {a} x3
x2 x3 {a, b}
x3 x1 {c}
The iterative search for a least solution produces the results for the different iteration
steps as they are listed in the following table:
x1
x2
x3
1
2
3
4
{a} {a, c} {a, c} ditto
{a}
{c} {a, c} {a, c}
We observe that at least one value for the unknowns increases in each iteration until
finally a solution is found.
We convince ourselves of the fact that this is the case for any complete lattice
given that right sides of equations are monotonic. More precisely, we show:
Theorem 1.5.2 Let D be a complete lattice and f : D D be a monotonic function.
Then the following two claims hold:
1. The sequence , f , f 2 , . . . is an ascending chain, i.e., it holds that f i1
f i for all i 1.
2. If d = f n1 = f n then d is the least element d satisfying d f (d ).
Proof The first claim is proved by induction: For i = 1, the first claim holds since
f 11 = f 0 = is the least element of the complete lattice and therefore less
than or equal to f 1 = f . Assume that the claim holds for i 1 1, i.e.,
f i2 f i1 holds. The monotonicity of the function f implies:
f i1 = f ( f i2 ) f ( f i1 ) = f i
22
We conclude that the claim also holds for i. Therefore, the claim holds for all i 1.
Let us now regard the second claim. Assume that
d = f n1 = f n
Then d is a solution of the inequality x f x. Let us further assume we have another
solution d of the same inequality. Thus, d f d holds. It suffices to show that
f i d holds for all i 0. This is again shown by induction. It is the case for
i = 0. Let i > 0 and f i1 d . The monotonicity of f implies
f i = f ( f i1 ) f d d
since d is a solution. This proves the claim for all i.
Theorem 1.5.2 supplies us with a method to determine not only a solution, but even
the least solution of an inequality, assuming that the ascending chain f i eventually
stabilizes, i.e., becomes constant at some i. It is therefore sufficient for the termination
of our search for a fixed point that all ascending chains in D eventually stabilize.
This is always the case in finite lattices.
The solution found by the iterative method is the least solution not only of the
inequality x f x, but is also the least solution of the equality x = f x, i.e., it is
the least fixed point of f . What happens if not all ascending chains of the complete
lattice eventually stabilize? Then the iteration may not always terminate. Nontheless,
a least solution is guaranteed to exist.
Theorem 1.5.3 (KnasterTarski) Each monotonic function f : D D on a
complete lattice D has a least fixed point d0 , which is also the least solution of
the inequality x f x.
Proof A solution of the inequality x f x is also called a post-fixed point of f .
Let P = {d D | d f d} be the set of post-fixed points of f . We claim that the
greatest lower bound d0 of the set P is the least fixed point of f .
We first prove that d0 is an element of P, i.e., is a post-fixed point of f . It is clear
that f d0 f d d for each post-fixed point d P. Thus f d0 is a lower bound of
P and is therefore less than or equal to the greatest lower bound, i.e., f d0 d0 .
d0 is a lower bound of P, and it is an element of P. It is thus the least post-fixed
point of f . It remains to prove that d0 also is a fixed point of f and therefore the
least fixed point of f .
We know already that f d0 d0 holds. Let us consider the other direction: The
monotonicity of f implies f ( f d0 ) f d0 . Therefore, f d0 is a post-fixed point
of f , i.e., f d0 P. Since d0 is a lower bound of P, the inequality d0 f d0
follows.
Theorem 1.5.3 guarantees that each monotonic function f on a complete lattice has
a least fixed point, which conicides with the least solution of the inequality x f x.
23
Example 1.5.3 Let us consider the complete lattice of the natural numbers augmented by , i.e., D = N {} together with the partial order . The function inc
defined by inc x = x + 1 is monotonic. We have:
inci = inci 0 = i i + 1 = inci+1
Therefore, this function has a least fixed point, namely, . This fixed point will not
be reached after finitely many iteration steps.
Theorem 1.5.3 can be applied to the complete lattice with the dual partial order
(instead of ). Thus, we obtain that each monotonic function not only has a least,
but also a greatest fixed point.
Example 1.5.4 Let us consider again the powerset lattice D = 2U for a base set U
and a function f with f x = x a b. This function is monotonic. It therefore has
a least and a greatest fixed point. Fixed-point iteration delivers for f :
f fk fk
0
U
1 b ab
2 b ab
With this newly acquired knowledge, we return to our application, which is to solve
systems of inequalities
xi f i (x1 , . . . , xn ),
i = 1, . . . , n
()
i = 1, . . . , n
In the instances of static program analysis considered in this book, we will frequently meet complete lattices where ascending chains eventually stabilize. In these
cases, the iterative procedure of repeated evaluation of right-hand sides according to
Theorem 1.5.2, is able to compute the required solution. This naive fixed-point iteration, however, is often quite inefficient.
Example 1.5.5 Let us consider again the factorial program in Example 1.4.3. The
fixed-point iteration to compute the least solution of the system of inequalities for
available assignments is shown in Fig. 1.6. The values for the unknowns stabilize
only after four iterations.
24
1 {y 1} {y 1}
2 {y 1} {y 1} {y 1}
4 {y 1}
5 {y 1} {y 1} {y 1}
ditto
Fig. 1.6 Naive fixed-point iteration for the program in Example 1.4.3
1
0
1 {y 1}
2 {y 1}
3
4
5
ditto
25
Let us have a closer look at round-robin iteration. The assignment D[xi ] D[xi ]
new; in our implementation does not just overwrite the old value of xi , but replaces
it by the least upper bound of the old and the new value. We say that the algorithm
accumulates the solution for xi during the iteration. In the case of a monotonic
function f i , the least upper bound of old and new values for xi is equal to the new
value. For a non-monotonic function f i , this need not be the case. The algorithm
is robust enough to compute an ascending chain of values for each unknown xi
even in the non-monotonic case. Thus, it still returns some solution of the system of
inequalities whenever it terminates.
The run time of the algorithm depends on the number of times the do-while loop
is executed. Let h be the maximum of the lengths of all proper ascending chains, i.e.,
one with no repetitions
d1 d2 . . . dh
in the complete lattice D. This number is called the height of the complete lattice D.
Let n be the number of unknowns in the system of inequalities. Round-robin iteration
needs at most h n rounds of the do-while loop until the values of all unknowns for
the least solution are determined and possibly one more round to detect termination.
The bound h n can be improved to n if the complete lattice is of the form 2U for
some base set U , if all functions f i are constructed from constant sets and variables
using only the operations and . The reason for this is the following: whether an
element u U is in the result set for the unknowns xi is independent of whether any
other element u is contained in these sets. For which variables xi a given element u
is in the result sets for xi can be determined in n iterations over the complete lattice
2{u} of height 1. Round-robin iteration for all u U is performed in parallel by
using the complete lattice 2U instead of the lattice 2{u} . These bounds concern the
worst case. The least solution is often found in far fewer iterations if the variables
are ordered appropriately.
Will this new iteration strategy also find the least solution if naive fixed-point
iteration would have found the least solution? To answer this question at least in the
monotonic case, we assume again that all functions f i are monotonic. Let yi(d) be
the ith component of F d and xi(d) be the value of D[xi ] after the dth execution of
the do-while loop of round-robin iteration. For all i = 1, . . . , n and d 0 we prove
the following claims:
(d)
(d)
26
Favorable:
Unfavorable:
y1
y1
Zero(x > 1)
5
NonZero(x > 1)
Zero(x > 1)
NonZero(x > 1)
3
y xy
y xy
2
xx1
xx1
1
0 {y 1} {y 1}
1 {y 1} {y 1} {y 1}
2
ditto
3 {y 1} {y 1}
4 {y 1}
5
Fig. 1.9 Round-robin iteration for the unfavorable order of Fig. 1.8
Claim 1 also entails that after d rounds the round-robin iteration computes values
at least as large as the naive fixed-point iteration. If the naive fixed-point iteration
terminates after round d, then the round-robin iteration terminates after at most d
rounds.
We conclude that round-robin iteration is never slower than naive fixed-point
iteration. Nevertheless, round-robin iteration can be performed more or less cleverly. Its efficiency substantially depends on the order in which the variables are
reevaluated. It is favorable to reevaluate a variable xi on which another variable x j depends before this variable. This strategy leads to termination with a
least solution after one execution of the do-while loop for an acyclic system of
inequalities.
Example 1.5.7 Let us consider again the system of inequalities for the determination
of available assignments for the factorial program in Example 1.4.3. Figure 1.8 shows
a favorable and an unfavorable order of unknowns.
In the unfavorable case, iteration needs four rounds for this program, as shown in
Fig. 1.9.
27
{[[]] d0 | : start v}
The mapping I is called the merge over all paths solution (in short: MOP solution)
of the analysis problem. On the other hand, we can put up a system of inequalities
which locally describes how information is propagated between nodes along the
edges of the control-flow graph:
I[start] d0
I[v]
[[k]] (I[u])
According to the theorems of the last section, this system has a least solution. And if
the complete lattice D has finite height, this least solution can be computed by means,
e.g., of round-robin iteration. The following theorem clarifies the relation between
the least solution of the inequalities and the MOP solution of the analysis.
Theorem 1.6.1 (Kam and Ullman 1975) Let I denote the MOP solution of an
instance of the monotonic framework and I the least solution of the corresponding
system of inequalities. Then for each program point v,
I[v] I [v]
holds. This means that for each path from program entry to v, we have:
I[v] [[]] d0 .
()
28
Proof We prove the claim () by induction over the length of . For the empty path
, i.e., = , we have:
[[]] d0 = [[]] d0 = d0 I[start]
Otherwise is of the form = k for an edge k = (u, lab, v). According to the
induction hypothesis, the claim holds for the shorter path , that is, [[ ]] d0 I[u].
It follows that:
[[]] d0 = [[k]] ([[ ]] d0 )
[[k]] (I[u])
since [[k]] is monotonic
I[v]
since I is a solution
Theorem 1.6.1 is somewhat disappointing. We would have hoped that the least solution was the same as the MOP solution. Instead, the theorem tells us that the least
solution is only an upper bound of the MOP solution. This means that, in general,
the least solution may be not as precise as the MOP solution and thus exhibit less
opportunities for optimization as the MOP. Still, in many practical cases the two
solutions agree. This is, in particular, the case if all functions [[k]] are distributive.
A function f : D1 D2 is called
distributive, if f ( X ) =
{ f x | x X } holds for all nonempty subsets
X D;
strict, if f = ;
totally distributive, if f is distributive and strict.
Example 1.6.1 Let us consider the complete lattice D = N {} with the canonical
order . The function inc defined by inc x = x + 1 is distributive, but not strict.
As another example, let us look at the function
add : (N {})2 (N {})
where add (x1 , x2 ) = x1 + x2 , and where the complete lattice (N {})2 is
component-wise ordered. We have:
add = add (0, 0) = 0 + 0 = 0
Therefore, this function is strict. But it is not distributive, as the following counterexample shows:
add ((1, 4) (4, 1)) = add (4, 4) = 8
= 5 = add (1, 4) add (4, 1)
29
Example 1.6.2 Let us again consider the powerset lattice D = 2U with the partial
order . For all a, b U the function f defined by f x = x a b is distributive
since
( X ) a b = {x a | x X } b
= {x a b | x X }
= { f x | x X}
for each nonempty subset X D. The function f is, however, strict only if b =
holds.
Functions f of the form f x = (x a) b have similar properties on the powerset
lattice D = 2U with the reversed order . For this partial
means
order, distributivity
that for each nonempty subset X 2U it holds that f ( X ) = { f x | x X }.
There exists a precise characterization of all distributive functions if their domain is
an atomic lattice. Let A be a complete lattice. An element a A is called atomic if
a
= holds and the only elements a A with a a are the elements a =
and a = a. A complete lattice A is called atomic if each element d A is the least
upper bound of all atomic elements a d in A.
In the complete lattice N {} of Example 1.6.1, 1 is the only atomic element.
Therefore, this lattice is not atomic. In the powerset lattice 2U , ordered by the subset
relation , the atomic elements are the singleton sets {u}, u U . In the powerset
lattice with the same base set, but the reversed order , the atomic elements are the
sets (U \{u}), u U . The next theorem states that for atomic lattices distributive
functions are uniquely determined by their values for the least element and for the
atomic elements.
Theorem 1.6.2 Let A and D be complete lattices where A is atomic. Let A A be
the set of atomic elements of A. It holds that
1. Two distributive functions f, g : A D are equal if and only if f () = g()
and f (a) = g(a) for all a A.
2. Each pair (d, h) such that d D and h : A D define a distributive function
f d,h : A D by:
f d,h (x) = d
{h(a) | a A, a x},
x A
Proof We only prove the first claim. If the functions f and g are equal they agree on
and the atomic elements of A. For the opposite direction, we regard an arbitrary
element x A. For x = holds f (x) = g(x) according to our assumption. For
x
= , the set A x = {a A | a x} is not empty. It follows that:
f (x) =
f ( Ax )
= { f (a) | a A, a x}
= {g(a) | a A, a x} = g(x)
which was to be proved.
30
There is an important theorem for program analyses with distributive edge effects:
Theorem 1.6.3 (Kildall 1972) Assume that every program point v is reachable from
the programs entry point. Assume further that all edge effects [[k]] : D D are
distributive. The least solution I of the system of inequalities agrees with the MOP
solution I , i.e.,
I [v] = I[v]
for all program points v.
Proof Because of Theorem 1.6.1 it suffices to show that I[v] I [v] holds for all
v. Since I is the least solution of the system of inequalities it suffices to show that
under the given circumstances, I is a solution, that is, satisfies all inequalities. For
the entry point start of the program, we have:
I [start] =
{[[]] d0 | : start start} [[]] d0 d0
I [v] =
= [[k]] (I [u])
The next to last equality holds since the set { | : start u} of all paths from
the entry point of the program start to u is not empty, and since the abstract edge
effect [[k]] is distributive. We conclude that I satisfies all inequalities. This proves
the claim.
The following example shows that in Theorem 1.6.3, the assumption that all
program points are reachable is necessary.
Example 1.6.3 Regard the control-flow graph of Fig. 1.10. As complete lattice we
choose D = N {} with the canonical order . As single edge effect we choose
31
7
0
inc
1
the distributive function inc. For an arbitrary starting value at the entry point, we
have
I[2] = inc (I[1])
= inc 0
=1
On the other hand, we have:
I [2] =
=0
since there is no path from the entry point 0 to program point 2. It follows that the
MOP solution is different from the least solution.
It is not critical to assume that all program points are reachable. Unreachable program
points can be easily identified and then removed without changing the semantics of
the program.
Conclusion 1.6.1 We gather all the observations about monotonic analysis frameworks.
The MOP solution of a monotonic analysis framework is always less or equal to
the least solution of the corresponding system of inequalities.
If all edge effects are distributive and every program point is reachable from the
entry point of the program, the MOP solution coincides with teh least solution.
Round-robin iteration can be used to determine the least solution if all ascending
chains in the complete lattice have finite lengths.
Let us apply these observations to the analysis of the availability of expressions
in variables. In this analysis, the complete lattice D = 2Ass is a finite powerset lattice
with the order . The value for the entry point of the program is d0 = , and the
abstract edge effects [[k]] are functions f of the form
f x = (x a)\b = (x a) b
for b = Ass\b. Example 1.6.2 shows that all such functions are distributive. Thus,
round-robin iteration for correpsonding systems of inequalities computes the MOP
solution, provided that all program points are reachable from the entry point of the
program.
We conclude the section about the removal of redundant computations. The transformation we presented has several disadvantages:
32
x y + 2;
y 5;
x y + 3;
The value of program variable x at program points 0 and 1 is not of any importance.
It is overwritten before being used. We therefore call variable x dead at this program
point. The first assignment to x can be removed because the value of x before the
second assignment is irrelevant for the semantics of the program. We also call this
assignment dead. These notions are now made precise.
33
xy+2
0
y5
1
xy+3
2
Let us assume that, after program execution, the values of the variables from some
set X Vars are still needed. This set X can be empty, in case all variables are only
used within the program under analysis. However, the analysis to be presented can
also be applied to individual procedure bodies. Returning from a procedure does not
necessarily mean leaving the whole program. This means that accesses to globally
visible variables may still happen. The set X should, in this case, be defined as the
set of global variables.
The following definitions use the terms definition and use, well known in the
compiler literature. A definition of a variable x is a statement which may change the
value of x. In our small example language, the only definitions are assignments and
loads where the left sides are x. A use of a variable x is an occurrence where the value
of x is read. The sets of variables used and defined at an edge in the control-flow
graph can be derived from the statement labeling the edge. For a label lab, they are
determined by:
Lab
Used
Defined
;
NonZero(e)
Vars(e)
Zero(e)
Vars(e)
x e
Vars(e)
{x}
x M[e]
Vars(e)
{x}
M[e1 ] e2 Vars(e1 ) Vars(e2 )
34
at each program point are easily determined. For the program points of the example,
they are:
Live Dead
0 {y} {x}
1 {x, y}
2 {y} {x}
3 {x, y}
How can at each program point the set of live variables be computed? In principle,
we proceed in the same way as we did for available assignments. The domain of
possible values is L = 2Vars . Instead of providing a value for the entry point of the
program, however, we now provide a value for the exit point, namely, the set X of
variables which are live when exiting the program. Also, we provide for each edge
an abstract edge effect. Since the liveness of variables at a program point does not
depend on the history but on the future, the abstract effect of the edge k = (u, lab, v)
is a function [[k]] that determines the set of variables possibly live at u, given a set
of variables possibly live at v. Again, the abstract edge effect only depends on the
label lab of the edge. this means that [[k]] = [[lab]] , where
[[;]] L
[[NonZero(e)]] L
[[x e]] L
[[x M[e]]] L
[[M[e1 ] e2 ]] L
=L
= [[Zero(e)]] L = L Vars(e)
= (L\{x}) Vars(e)
= (L\{x}) Vars(e)
= L Vars(e1 ) Vars(e2 )
35
live variables mean larger sets of dead variables, which means more opportunities for
optimization. The function L represents the MOP solution of our analysis problem.
Program analyses are called forward analyses if the value at a program point
depends on the paths reaching this program point from program entry. Program
analyses are called backward analyses if the value at a program point depends on the
paths leaving that program point and reaching program exit. Liveness of variables
therefore is a backward analysis. Available assignments, in contrast, is a forward
analysis.
Transformation DE:
Let us assume that we are given the MOP solution L . At each program point v,
this is the set L [v]. Its complement contains only variables that are definitely dead
at v, i.e., dead along all program paths starting at v. Assignments to these variables
are superfluous and can be removed by the following transformation DE, for Dead
variable assignment Elimination:
x
L [v]
xe
v
;
v
Memory accesses whose results are not needed could also be removed in analogy
to the removal of assignments. This could, however, change the semantics of the
program if it were to remove an illegal access to memory, whichdepending on the
semantics of the programming languagemay produce a side effect such as raising
an exception. Useless accesses to memory are therefore not removed.
Transformation DE is called dead-code elimination. Correctness of this transformation is again shown in two steps:
1. The abstract edge effects are shown to correctly implement the definition of
liveness.
2. The transformation is shown to preserve the semantics of programs.
We again consider the second step only. An important insight is that to show
semantics preservation it is not necessary to show that the value of each variable at
each program point remains invariant under the transformation. In fact, this is not
the case here. For the applicability of the transformation, however, it suffices that the
observable behavior of the original and the transformed program are the same. The
only question is: what is potentially observable? Here, we demand that the program
points traversed by program execution are the same, and that in each step the contents
of memory coincide as well as the values of the variables in the set X at the end of
program execution. Claim 2 then is that the value of a variable, dead at some program
point v, does not influence the observable behavior of the program. To prove this, we
consider the state s at v and the computation of a path starting at v and reaching
program exit. Remember, the state is a pair consisting of a concrete variable binding
and a memory. We show by induction over the length of program paths :
36
(L) Let s be a state that differs from s only in the values of dead variables. The state
transformation [[]] is also defined for s , and the states [[ ]] s and [[ ]] s agree
up to the values of dead variables for all prefixes of .
The invariant (L) entails that two states at a program point v definitely lead to the
same program behavior if they only disagree in the values of variable that are dead
at v. To prove the correctness of the transformation it suffices to show that only the
values of dead variables may be different during the execution of the original and
the transformed program.
The computation of the set L [u] of variables possibly live at program point u
works analogously to the way sets of definitely available assignments were computed.
We set up an appropriate system of inequalities. Recall that, opposed to the availableassignments analysis where we fixed a start value at program entry start, we now fix a
value X at program exit stop which consists of all variables which are live at program
exit. Also, each edge k = (u, lab, v) has an associated inequality that delivers the set
of possibly live variables at u, given the set of possible live variables at v. Remember
that the inequalities in the available-assignment analysis were oriented the other way
around. The resulting system of inequalities is:
L[stop] X
L[u] [[k]] (L[v])
Thus, the difference in the systems of inequalities for forward and backward analyses
only consists in the exchange of start and stop and in the reverted orientation of the
edges.
The complete lattice in which the system of inequalities will be solved is finite.
This means that all ascending chains will eventually stabilize. The abstract edge
effects are monotonic. Round-robin iteration can thus be used to determine the least
solution L. In fact, the abstract edge effects are even distributive, which means that
this least solution is the same as the MOP solution L provided that the program exit
stop is reachable from each program point (cf. Theorem 1.6.3).
Example 1.7.3 We consider again the program for the factorial function assuming that it obtains its input and returns its output through memory cells, more precisely through the cells M[I ] and M[R]. No variables are assumed to be live at program exit. The control-flow graph and the system of inequalities derived from it are
shown in Fig. 1.12. The system of inequalities closely corresponds to the control-flow
graph. After all, the system of equations was extracted from the control-flow graph.
However, it does not need to be explicitly constructed. Instead, fixed-point iteration
could traverse the control-flow graph, executing the abstract edge effects associated
with the traversed edges. The fixed-point iterator would be something like a driver.
Another analysis can be conducted by executing it with the edges effects of this new
analysis.
Round-robin iteration delivers the solution after only one round, given the right
ordering of the unknowns:
37
0
x M [I]
L[0] (L[1]\{x}) {I}
1
y1
Zero(x > 1)
6
NonZero(x > 1)
3
y xy
M [R] y
7
L[1]
L[2]
L[3]
L[4]
L[5]
L[6]
L[2]\{y}
(L[6] \ { x}) (L[3] {x})
(L[4]\{y}) {x, y}
(L[5]\{x}) {x}
L[2]
L[7] \ { y, R}
L[7]
4
xx1
5
Fig. 1.12 The system of inequalities for possibly live variables for the factorial program
7
6
2
5
4
3
1
0
1
2
{y, R}
{x, y, R} ditto
{x, y, R}
{x, y, R}
{x, y, R}
{x, R}
{I, R}
We notice that no assignment in the factorial program has a dead left side. Therefore,
transformation DE does not modify this program.
The removal of assignments to dead variables can make other variables dead. This
is witnessed in Fig. 1.13. This example shows a weakness of the analysis for dead
variables: It may classify variables as live due to later uses in assignments to dead
variables. A removal of such an assignment and a subsequent reanalysis would discover new dead variables. This iterative application of transformation and analysis is
rather inefficient. In the example of live-variable analysis the repeated analysis can be
avoided by strengthening the analysis. Strengthening leads to possibly smaller sets
of possibly live variables. The new analysis works with a more restricted condition
for liveness. The new notion, true liveness, uses the notion, true use of a variable
on a path starting at a program point. A use in an assignment to a dead variable is
not considered a true use. This renders the definition of true liveness recursive: true
liveness depends on true use, which depends on true liveness.
Let us assume again that the values of variables in a set X are still used at program
exit. We call a variable x truly live along a path to program exit if x X and
contains no definition of x or if contains a true use of x, which occurs before any
38
{y, R}
{x, y, R}
{y, R}
{y, R}
M [R] y
xy+1
z 2x
{y, R}
xy+1
;
{y, R}
M [R] y
M [R] y
{y, R}
xy+1
{y, R}
z 2x
{y, R}
M [R] y
M [R] y
39
=L
= [[Zero(e)]] L = L Vars(e)
= (L\{x}) ((x L) ? Vars(e) : )
= (L\{x}) ((x L) ? Vars(e) : )
= L Vars(e1 ) Vars(e2 )
Y ) = (x Y ) ? b : c
= ( {x
y | y Y }) ? b : c
= c {(x y) ? b : c | y Y }
= c { f y | y Y}
40
Liveness:
True liveness:
xx1
{x}
xx1
T x+1
2
T x+1
2
yT
3
T x+1
2
yT
3
M [R] y
4
;
3
M [R] T
4
M [R] T
4
Note that the functions of Theorem 1.7.1 are closed under composition, least upper
bounds, and greatest lower bounds (Exercise 11).
The least solution of systems of inequalities for true liveness agree with the MOP
solutions due to the distributivity of the abstract edge effects. We must, however,
require that program exit stop is reachable from each program point.
It is interesting to note that the analysis of true liveness discovers more superfluous
assignments than repeated analysis of plain liveness and dead-code elimination.
Example 1.7.5 Figure 1.15 shows a loop in which a variable is modified that is only
used in the loop. Plain liveness analysis cannot determine that this variable is dead,
while true-liveness analysis is able to do so.
41
1
T x+1
{T }
2
yT
{y, T }
3
M [R] y
{y, T }
the same value. This renders variable y dead at program point 2, such that the compiler
can eliminate the assignment to y. The resulting program still contains variable T ,
but variable y is eliminated.
For this kind of transformation, the compiler needs to know how the value of
an expression is propagated by copy actions between variables. Such an analysis,
therefore, is called copy propagation. Consider a variable x. The analysis maintains
at each program point a set of variables guaranteed to contain the actual value of this
variable. The use of a variable containing a copy of x can be replaced by a use of x.
Let V = {V Vars | x V } be the complete lattice of all sets of program
variables containing x, ordered by the superset relation . It is intuitively clear that
larger sets of variables guaranteed to contain the value of x will offer a greater chance
for this optimization.
At program entry, only variable x is guaranteed to contain its own value. The start
value of our analysis at program entry, therefore, is V0 = {x}. The abstract edge
effects again only depend on the edge labels. We define:
= {x}
[[x e]] V
[[x M[e]]] V = {x}
V {z} if y V
=
[[z y]] V
V \{z} if y
V
= V \{z}
[[z r ]] V
if x
z, r
Vars
42
the same value as x. The compiler may, therefore, replace accesses to z by accesses to
x. We introduce the substitution V[u] to define the corresponding transformation:
V[u] z =
x
z
if z Vx [u]
otherwise
u
NonZero(e)
NonZero(V[u] (e))
Here, V[u] (e) denotes the application of the substitution V[u] to the expression e.
Example 1.8.2 It is time to have a look at a slightly bigger example to observe the
cooperation of the different transformations.
In Example 1.4.2 we considered the implementation of the statement a[7]; in
our example language and showed how the second computation of the expression
A + 7 could be replaced by an access to variable A1 . Figure 1.18 shows on the left
side the results of transformation RE. The application of transformation CE replaces
the use of variable A2 by a use of variable A1 . The result of the transformation
is the control-flow graph in the middle. The application of the transformation CE
43
A1 A + 7
A1 A + 7
A1 A + 7
B1 M [A1 ]
B1 M [A1 ]
B1 M [A1 ]
B2 B 1 1
B2 B1 1
B2 B1 1
A2 A1
A2 A1
M [A2 ] B2
M [A1 ] B2
M [A1 ] B2
1
x7
x 7;
if (x > 0)
M [A] B;
Zero (x > 0)
NonZero (x > 0)
3
M [A] B
4
;
renders variable A2 dead. So, an application of transformation DE in the last step can
eliminate the assignment to A2 . The inserted empty statement can later be removed
in some clean-up step.
44
1
x7
Zero (x > 0)
NonZero (x > 0)
3
M [A] B
M [A] B
4
;
The question is, do such inefficiencies occur in real programs? The answer is yes.
There are several reasons why constant folding might find much to optimize. It is good
programming style to use named constants to make programs easily modifyable. The
compiler then is expected to propagate the constants through the program and fold
expressions away where possible. Often, a program is written for many configurations of parameters. The automotive industry develops such generic programs, also
called program families, which can be instantiated for many different types of cars,
e.g. those with four or those with five gears, just by setting the named constants to
different values.
Also, many programs are not written by programmers, but are generated from
other programs. These generated programs tend to contain such inefficiencies. The
compiler itself may produce constant subexpressions during the translation process,
e.g., when it translates accesses to data structures such as arrays, or as the result of
other program transformations.
Constant folding is a special case of partial evaluation of programs, which is
the essence of compilation according to A. Ershov, one of the pioneers of compiler
design. Partial evaluation performs computations on statically known parts of the
programs state already at compile time. In this book, we are only concerned with
constant folding. Our goal is to develop an analysis that computes for each program
point v the following information: Which value does each program variable have
when program control reaches v? It should be clear that a variable, in general, will
have different values at a program point for different executions of the program or
even during the same execution of a program, when control reaches this program point
several times. The analysis can, at best, find the cases in which for every execution
of the program and every time control reaches the program point a variable has the
same value. We call this analysis constant propagation. As a side effect, this analysis
also determines whether each program point is potentially reachable.
We construct the complete lattice for this analysis in two steps. In the first step,
we design a partial order for the possible values of variables. To do this, we extend
the set of integer numbers by an element , which represents an unknown value.
Z = Z {}
and x y iff y = or x = y
45
-2
-1
Figure 1.21 shows this partial order. The partial order Z by itself is not yet a complete
lattice, since it lacks a least element. In a second step, we construct the complete
lattice of abstract variable bindings by defining
D = (Vars Z ) = (Vars Z ) {},
i.e., D is the set of all functions mapping variables to abstract values, extended by an
additional value, as the unique least element. We say an abstract binding D
=
knows the value of a variable x if D x Z. If, however, D x
/ Z, that is, if D x = ,
then the value of x is unknown. The value for x means that the analysis could not
determine one single value for x, perhaps, since x was found to have several distinct
values in the course of execution.
The new element is associated with every program point that, according to the
current fixed-point iteration is not yet known to be reachable. If the solution found
by the fixed-point iteration still has program points with associated value , these
points cannot be reached by any program execution. We define an order on this set
of abstract states by:
D1 D2
iff
The abstract variable binding denoting not yet reachable is considered as smaller
than any other abstract state. The idea is that later, the corresponding program point
still may turn out to be reachable and thus receive any abstract variable assignment
= . An abstract binding D1
= is possibly better, i.e., less than or equal to another
= , if it agrees with D2 on the values of all variables that D2 knows,
binding D2
but possibly knows more values of variables than D2 . Intuitively, an abstract variable
binding that knows values of more variables may lead to more optimizations and
thus is better information. Going up in the partially ordered set D = (Vars Z )
thus means forgetting values of variables.
We want to show that D together with this order is a complete lattice. Consider
a subset X D. Without loss of generality, we may assume that
X . We have
then X (Vars Z ).
From X = follows X = D. Therefore, D has a least upper bound for
X . For X
= , the least upper bound X = D is given by:
z if f x = z for all f X
Dx =
{ f x | f X} =
otherwise
46
This shows that every subset X of D has a least upper bound and that D is a complete
lattice. For each edge k = (u, lab, v) we construct an abstract edge effect [[k]] =
[[lab]] : D D, which simulates the concrete computation. Since unreachability
should be preserved by all abstract edge effects, we define all abstract effects as strict,
i.e., [[lab]] = holds for all edge labels lab.
Now let D
= be an abstract variable binding. We need an abstract evaluation
function for expressions to define the abstract edge effects. This function determines the value of an expression as far as possible for the given information in D.
The abstract evaluation has to handle the situation that the precise value of a given
expression cannot be determined in the given abstract variable binding D. This means
that the expression should be evaluated to . The abstract evaluation of expressions
works like the concrete evaluation of expressions as long as all operands of the operators in the expression are concrete values. To handle the case of an operand ,
the concrete arithmetic, Boolean, and comparison operators, , are replaced by the
corresponding abstract operators, , which are also able to deal with operands.
For binary operators, , we define:
a b =
if a = or b =
a b otherwise
2, y
}
We get:
47
1
x7
Zero (x > 0)
NonZero (x > 0)
3
M [A] B
1
2
3
4
5
{x
}
{x 7}
{x 7}
{x 7}
{x 7} = {x 7}
[[x M[e]]] D = D {x
}
[[M[e1 ] e2 ]] D = D
The operator changes a function at a given argument to a given value.
We assume that no values of variables are known at the start of program execution.
| x Vars} for
Therefore, we select the abstract variable binding D = {x
:DD
Example 1.9.3 The least solution of the system of inequalities of our introductory
example is shown in Fig. 1.22.
48
a1 a2
==
x a2
==
(a1 ) (a2 )
{a} if a
Z if a =
We extend the description relation for values of program variables to one between
concrete and abstract variable bindings. For simplicity, it gets the same name, .
This description relation (Vars Z) (Vars Z ) is defined by:
D iff D
= x D x (for all x Vars)
This definition of implies that there exists no concrete variable binding such that
. Therefore, the concretization maps to the empty set. maps each abstract
variable binding D
= to the set of all concrete variable bindings that know for
each variable x either D x Z or an arbitrary value, if D x = .
D = { | x : ( x) (D x)}
49
1, y
7} {x
, y
7}
The simple constant propagation, we consider here, ignores the values in memory.
We can therefore describe program states (, ) just by abstract variable bindings,
which only describe . Overall, the description relation is defined by:
(, ) D iff D
The concretization returns:
D=
if D =
{(, ) | D} otherwise
We want to prove that each path of the control-flow graph maintains the description
relation between concrete and abstract states. The claim is:
(K ) If s D holds, and if [[]] s is defined then ([[]] s) ([[]] D) holds.
The following diagram visualizes this claim:
[[]]
s1
[[]]
D1
50
(x y) (x y ),
if x x y y
Claim () then follows by induction over the structure of expressions e. The claim
about the relation between concrete and abstract operators has to be shown for each
operator individually. For constant propagation with our simple definition of , this
is certainly fulfilled.
Overall, we wanted to prove that each edge k = (u, lab, v) maintains the description relation between concrete and abstract states. Stated differently, this means
that the concrete and the abstract edge effects are compatible with the description
relation. Let us return to this proof. The proof goes by case distinction according to
the label lab of the edge.
We assume that s = (, ) D and that D
= .
Assignment, x e: We have:
[[x e]] s
= (1 , )
[[x e]] D = D1
where
1 = {x
[[e]] }
where
D1 = D {x
[[e]] D}
The claim (1 , ) D1 follows from the compatibility of the concrete and the
abstract expression evaluation with the description relation .
Read, x M[e]: We have:
[[x M[e]]] s
= (1 , )
[[x M[e]]] D = D1
where
1 = {x
([[e]] )}
where
D1 = D {x
}
{[[]] D | : start v} ,
51
0
1
x 10
2
Zero(x > 1)
6
NonZero(x > 1)
3
y xy
M [R] y
7
4
xx1
5
x y
x y
1 10
10
2 10 1
3 10 1
4 10 10
9 10
y1
ditto
where D x = for all x Vars holds. Invariant (K ) implies for all initial states s
and all paths reaching program point v:
([[]] s) (D [v])
Solving the associated system of inequalities leads to an approximation of the MOP
solution.
Example 1.9.4 Consider the factorial program, this time with a given initial value for
variable x. Figure 1.23 shows the result of the analysis. We know that, with a given
initial value, the whole computation of the factorial of that value could be executed at
compile time. Our static analysis on the other hand, does not identify this possibility.
The reason is that constant propagation determines values of variables at program
points that are the same each time execution reaches that program point. The values
of the variables x and y, however, change within the loop.
In conclusion, we note that constant propagation computes with concrete values
as far as they can be determined statically. Expressions consisting of only known
values can be evaluated by the compiler. In general, though, constant propagation
will only be able to determine a subset of the concrete variable bindings. The fixedpoint iteration to determine the least solution of the system of inequalities always
terminates. With n program points and m variables it takes at most O(m n) rounds.
Example 1.9.4 shows that the iteration often terminates faster. There is one caveat,
though: the edge effects for constant propoagation are not all distributive. As a
counterexample consider the abstract edge effect for the assignment x x + y
together with the two variable bindings:
52
D1 = {x
2, y
3} and D2 = {x
3, y
2}
On the one hand, we have:
5, y
3} {x
5, y
2}
[[x x + y]] D1 [[x x + y]] D2 = {x
= {x
5, y
}
, y
}
= {x
, y
}
Therefore
[[x x + y]] D1 [[x x + y]] D2
= [[x x + y]] (D1 D2 )
violating the distributivity property.
The least solution D of the system of inequalities thus in general delivers only an
upper approximation of the MOP solution. This means:
D [v] D[v]
for each program point v. Being an upper approximation, D[v] still describes the
result of each computation along a path that ends in v:
([[]](, ))D[v],
whenever [[]] (, ) is defined. Therefore, the least solution is safe information,
which the compiler can use to check the applicability of program transformations.
Transformation CF:
The first use of information D consists in removing program points that are identified as unreachable. The following transformation performs the removal of dead
code:
D[u] =
u
53
Furthermore, the compiler may remove all condition edges that might lead to a
reachable node, but whose abstract edge effect delivers :
u
[[lab]] (D[u]) =
lab
The next two rules simplify condition edges whose conditions deliver a definite,
i.e., non- value. Having a definite value means that this edge will be taken in all
executions.
= D[u] = D
[[e]] D = 0
Zero (e)
= D[u] = D
[[e]] D
{0, }
NonZero(e)
= D[u] = D
u
x e
xe
where the expression e results from evaluating the expression e in the abstract
variable binding D .
c if [[e]] D = c
=
e =
e if [[e]] D =
The simplification of expressions at other edges works similarly.
Constant folding as explained so far is always applied to maximal expressions in
statements. It can also be extended to subexpressions:
x + (3 y)
y (x + 3)
{x
,y
5}
=========
{x
,y
5}
=========
x + 15
5 (x + 3)
Our analysis can be improved to better exploit the information contained in conditions.
Example 1.9.5 Consider the following example program:
54
Zero (x = 7)
NonZero (x = 7)
Zero (x = 7)
NonZero (x = 7)
1
y x+3
y 10
if (x = 7)
y x + 3;
Without knowing the value of x before the if statement, the analysis can derive that
x has the value 7 when control enters the then part.
Conditions testing the equality of variables with values can be exploited particularly
well.
if [[x = e]] D = 0
[[NonZero (x = e)]] D =
D1 otherwise
where we define:
(D x [[e]] D)}
D1 = D {x
55
l1
u1
l2
u2
i 0;
B : if (i < 42) {
if (0 i i < 42) {
A1 A + i;
M[A1 ] i;
i i + 1;
} else goto error;
goto B;
}
The condition of the outer loop makes the inner bounds check superfluous. It will
never trigger the jump to program point error. The inner bounds check can therefore
be eliminated.
Interval analysis generalizes constant propagation by replacing the domain Z for
the values of variables by a domain of intervals. The set of all intervals is given by:
I = {[l, u] | l Z {}, u Z {+}, l u}
l stands for lower and u for upper. According to this definition, each interval represents a nonempty set of integers. There exists a natural order on intervals: :
[l1 , u 1 ] [l2 , u 2 ]
iff
l2 l1 u 1 u 2
56
[l1 , u1 ] [l2 , u2 ]
l1
l2
u1
u2
[l1 , u1 ] [l2 , u2 ]
Fig. 1.26 Two intervals, in the middle, and their least upper bound, on top, and their greatest lower
bound, on the bottom
important difference, though, between the partial order Z and the partial order I.
The partial order Z has only finite strictly ascending chains while I has ascending
chains that never stabilize, for instance, the following:
[0, 0] [0, 1] [1, 1] [1, 2] . . .
The natural description relation between integer values and integer intervals is
given by:
z [l, u]
iff
lzu
This description relation leads to the following concretization function:
[l, u] = {z Z | l z u}
Example 1.10.2 We have:
[0, 7] = {0, . . . , 7}
[0, ] = {0, 1, 2, . . .}
Interval analysis needs to calculate with intervals. These calculations are expressed
in terms of abstract versions of the arithmetic, Boolean and comparison operators.
The sum of two intervals should contain all values that result when any two values
from the argument intervals are added. We therefore define:
[l1 , u 1 ] + [l2 , u 2 ] = [l1 + l2 , u 1 + u 2 ]
+ _ =
+ + _ = +
Note that the value of + never need to be computed.
where
57
where
a = min{l1l2 , l1 u 2 , u 1l2 , u 1 u 2 }
b = max{l1l2 , l1 u 2 , u 1l2 , u 1 u 2 }
Example 1.10.3 We check the plausibility of this definition of interval multiplication
by inspecting a few examples.
[0, 2] [3, 4] = [0, 8]
[1, 2] [3, 4] = [4, 8]
[1, 2] [3, 4] = [6, 8]
[1, 2] [4, 3] = [8, 4]
To define division on intervals is really problematic! Let [l1 , u 1 ] / [l2 , u 2 ] = [a, b] .
If 0 is not contained in the denominator interval we can define:
a = min{l1 /l2 , l1 /u 2 , u 1 /l2 , u 1 /u 2 }
b = max{l1 /l2 , l1 /u 2 , u 1 /l2 , u 1 /u 2 }
However, if 0 is contained in the denominator interval, that is: l2 0 u 2 , a runtime error cannot be excluded. The semantics of our example language does not
state what happens in the case of such a run-time error. We assume for simplicity
that any value is a legal result. We therefore define for this case:
[a, b] = [, +]
Besides abstract versions of the arithmetic operators, we need abstract versions of
comparison operators. The abstract version of the comparison for equality is quite
different from the natural equality of intervals. Abstract comparisons of intervals
can have the values true, false, or unknown Boolean value which describes both true
and false. According to the semantics of our example language, the value false is
represented by 0, while the value true (when returned by a Boolean operator) should
be represented by 1. The corresponding intervals are [0, 0] and [1, 1]. Accordingly,
58
the unknown Boolean value true false is represented by the interval [0, 0][1, 1] =
[0, 1].
The value true results for two identical singleton intervals. The result must be
false for two disjoint intervals because the comparison can never deliver true for any
pair of values from the argument intervals. truefalse must be the result in the case
of nondisjoint, nonsingleton intervals because there are pairs of identical values in
the argument intervals, but also pairs of nonidentical values.
[1, 1] if l1 = u 1 = l2 = u 2
[l1 , u 1 ] = [l2 , u 2 ] = [0, 0] if u 1 < l2 u 2 < l1
[0, 1] otherwise
Example 1.10.4 We use some examples to convince ourselves that this definition
makes sense:
[42, 42]= [42, 42] = [1, 1]
[1, 2] = [3, 4] = [0, 0]
[0, 7] = [0, 7] = [0, 1]
We now treat just one more comparison operator, namely the operation <. We have:
[1, 1] if u 1 < l2
[l1 , u 1 ] < [l2 , u 2 ] = [0, 0] if u 2 l1
[0, 1] otherwise
59
iff
D
=
x Vars : ( x) (D x).
iff
if
Next to define are the abstract edge effects for interval analysis. They also look quite
like the ones for constant propagation, apart from the fact that they now calculate
over interval domains:
[[;]] D
[[x e]] D
[[x M[e]]] D
[[M[e1 ] e2 ]] D
=
=
=
=
D
D {x
[[e]] D}
D {x
}
D
if [0, 0] = [[e]] D
[[NonZero (e)]] D =
D otherwise
if [0, 0]
[[e]] D
[[Zero (e)]] D =
D if [0, 0] [[e]] D
if D
= . Here, denotes the interval [, ].
We assume, like in the case of constant propagation, that nothing is known about
the values of variables at the entry to the program. This is expressed by associating
the largest lattice element, = {x
D=
if [0, 0] = [[e]] D
D1 otherwise
60
0
i0
1
0
1
2
3
4
5
6
7
8
l
u
+
0 42
0 41
0 41
0 41
0 41
1 42
42 42
Fig. 1.27 The least solution of the interval analysis of Example 1.10.1
if e (x = e1 )
(D x) ([[e1 ]] D)}
D {x
D1 = D {x
D {x
D1 = D {x
D
if e (x = e1 )
Note that greatest lower bounds of intervals are used here. These greatest lower
bounds are defined in this context, because otherwise the abstract evaluation of the
condition would have returned an interval not subsuming [0, 0].
Let us regard the program of Example 1.10.1. Its control-flow graph and the least
solution of the system of inequalities for the interval analysis of variable i are shown
in Fig. 1.27.
The partial order I has ascending chains that never stabilize. It is therefore not
clear how to determine the least solution of the system of inequalities for interval
analysis. In our example, fixed-point iteration would terminate, but only after 43
rounds. Other programs, though, can be constructed where round-robin iteration for
interval analysis would not terminate.
61
Apparently, we need new techniques to deal with complete lattices that have
infinite ascending chains. The inventors of abstract interpretation, Patrick and Radhia
Cousot, also invented the necessary techniques, widening and narrowing. Their first
publication presented interval example with widening as an example.
The idea of widening is to speed fixed-point iteration, albeit at the cost of a
possibly reduced precision. The measure to speed up the iteration guarantees that
each abstract value of an unknown can only undergo finitely many changes.
One idea to widening for interval analysis is not to allow arbitrary enlargements of
intervals. No enlargements from finite to finite intervals are admitted. An admissible
ascending chain of intervals could look like the following:
[3, 17] [3, +] [, +]
Let us formalize the general approach of widening. Let
xi f i (x1 , . . . , xn ) , i = 1, . . . , n
be again a system of inequalities over a complete lattice D. We consider the accumulating system of equations associated with this system of inequalities:
xi = xi f i (x1 , . . . , xn ) , i = 1, . . . , n
A tuple x = (x1 , . . . , xn ) Dn is a solution of the system of inequalities if and
only if it is a solution of the associated accumulating system of equalities. The
reformulation of the system of inequalities as an accumulating system of equations
alone does not solve our problem. Fixed-point iteration for the accumulating system
as for the original system may not necessarily terminate. Therefore, we replace the
operator of the accumulating system by a widening operator, which can be used
to enforce termination. As a result, we obtain the system of equations:
xi = xi f i (x1 , . . . , xn ) , i = 1, . . . , n
The new operator must satisfy:
v1 v2 v1 v2
The values accumulated for an unknown xi during a fixed-point iteration for the system with widening therefore will grow at least as fast as the values for the fixed-point
iteration for the system without widening. Round-robin iteration for the modified
system, if it terminates, still computes a solution of the accumulating system of
equations and therefore also for the original system of inequalities.
We apply the general method of widening to interval analysis and the complete
lattice DI = (Vars I) . A widening operator for this complete lattice is
defined by:
D = D = D
62
and for D1
=
= D2
(D1 D2 ) x = (D1 x) (D2 x)
where
u]
such
that
[l1 , u 1 ] [l2 , u 2 ] = [l,
if l1 l2
l
l= 1
otherwise
if u 1 u 2
u1
u=
+ otherwise
The widening operator for variable bindings is based on a widening operator for
intervals. During fixed-point iteration, the left operand operand is the old value while
the right operand is the new value. Therefore, the operator treats its two arguments
differently and is, thus, not commutative.
Example 1.10.6 Here are some iteration steps:
[0, 2] [1, 2] = [0, 2]
[1, 2] [0, 2] = [, 2]
[1, 5] [3, 7] = [1, +]
The widening operator, in general, does not deliver the least upper bound, but only
some upper bound. The values of the unknowns therefore may grow faster. A practical widening operator should be chosen in a way that guarantees that the resulting
ascending chains eventually stabilize so that fixed-point iteration terminates. The
widening operator that we have presented, guarantees that each interval can grow at
most two times. Therefore, the number of iteration steps for round-robin iteration
for a program with n program points to O(n #Vars).
In general, the starting point is a complete lattice with infinite ascending chains
together with a system of inequalities over this lattice. In order to determine some
(hopefully nontrivial) solution for this system, we first rewrite it into an equivalent
accumulating system of equations. Then we replace the least-upper bound operation
of the accumulation with a widening operator. This operator speeds up iteration and
enforces termination of the fixed-point iteration by admitting only a finite number of
changes to the values of the unknowns.
The design of such widening operators is black magic. On the one side, the
widening operator needs to radically lose information in order to guarantee termination. On the other hand, it should keep enough relevant information such that the
results of the analysis still have some value. Figure 1.28 shows round-robin iteration
for the program of Example 1.10.1. The iteration terminates rather quickly as we
have hoped, but with a disappointing result. The analysis loses all knowledge of
upper bounds. An elimination of the index out of bounds check is not possible.
Apparently, information is thrown away too generously. We therefore need to
improve on this naive procedure. Some thinking reveals that the widening operator
should be applied more economically. It is not necessary to apply it for each unknown
63
0
i0
1
A1 A + i
4
M [A1 ] i
5
ii+1
6
0
1
2
3
4
5
6
7
8
l
u
l
u l u
+ +
0
0
0 +
0
0
0 +
0
0
0 +
0
0
0 + ditto
0
0
0 +
1
1
1 +
42 +
42 +
0
i0
1
I1 = {1}
I2 = {2}
oder
4
M [A1 ] i
5
ii+1
6
Fig. 1.29 Feedback vertex set for the control-flow graph of Example 1.10.1
at each program point and still guarantee termination. It suffices to apply widening
at least once in each cycle of the control-flow graph.
A set I of nodes in a directed graph G is called feedback vertex set if it contains
at least one node of each directed cycle in G. Round-robin iteration still terminates
if widening is applied only at the nodes of a feedback vertex set of the control-flow
graph.
Example 1.10.7 This idea is tried out at our program from Example 1.10.1.
Figure 1.29 shows example sets I1 and I2 of nodes each of which contain one
64
node in the (unique) directed cycle of the program. For widening placed at node
1, round-robin iteration yields:
1
l
u
l
u l u
+ +
0
0
0 +
0
0
0 41
0
0
0 41
0
0
0 41 ditto
0
0
0 41
1
1
1 42
42 +
0
1
2
3
4
5
6
7
8
In fact, it is almost the least solution that is obtained. The only information lost
is the upper bound for loop variable i at program points 1 and 8.
For widening placed at the node 2, we obtain:
1
0
1
2
3
4
5
6
7
8
l
u
l
u
l
u l u
+ + +
0
0
0
1
0 42
0
0
0 + 0 +
0
0
0 41
0 41
0
0
0 41
0 41 ditto
0
0
0 41
0 41
1
1
1 42
1 42
42 + 42 +
42 42
The analysis using this feedback vertex set obtains better information about variable
i at program points 1 and 8, but loses so much information at program point 2 that
it can no longer derive the nonreachability of program point 7.
This example shows that the restriction of widening to some relevant program points
may improve the precision of the analysis considerably. The example also shows that
is not always clear where to apply widening to obtain the most precise information.
A complementary technique is now presented, narrowing.
Narrowing is a technique to gradually improve a possibly too imprecise solution.
As for widening, we first develop the general approach for arbitrary systems of
inequalities and then turn to how narrowing can be applied to interval analysis.
Let x be some solution to the system of inequalities
xi f i (x1 , . . . , xn ) ,
i = 1, . . . , n
65
Let us assume further that the right-hand sides f i are all monotonic and that F is the
associated function Dn Dn . The monotonicity of F implies:
x F x F2 x . . . Fk x . . .
This iteration is called narrowing. Narrowing has the property that all tuples F i x that
are obtained after some iteration steps are solutions to the system of inequalities. This
also holds for narrowing by round-robin iteration. Termination is not a problem any
more: iteration can be stopped whenever the obtained information is good enough.
Example 1.10.8 Consider again the program of Example 1.10.1 where narrowing is
applied to the result produced by naive widening. We obtain:
0
0
1
2
3
4
5
6
7
8
0
0
0
0
0
1
42
42
u
l
u
l
u
+ + +
+ 0 + 0 42
+ 0 41
0 41
+ 0 41
0 41
+ 0 41
0 41
+ 0 41
0 41
+ 1 42
1 42
+
+ 42 + 42 42
In our example, the narrowing, following the widening, completely compensates for
the widenings loss of information. This can not always be expected. It is also possible
that narrowing needs a long time. It even may not terminate, namely if the lattice
has infinite descending chains. This is the case for the interval lattice. Termination,
though, can be enforced by accelerated narrowing. Let us assume that we are given
some solution of the system of inequalities
xi f i (x1 , . . . , xn ) , i = 1, . . . , n
We consider the following system of equations:
xi = xi f i (x1 , . . . , xn ) , i = 1, . . . , n
We start with a possibly too large solution. To improve, that is to shrink, the values
for the unknowns, the contributions of the right sides are used.
Let H : Dn Dn be the function defined by H (x1 , . . . , xn ) = (y1 , . . . , yn )
such that yi = xi f i (x1 , . . . , xn ). If all f i are monotonic we have:
Hi x = Fi x
for all i 0 .
66
,
Now the operator in the system of equations is replaced by a new operator
which possesses the following property:
a2 a1
a1 a2 a1
We call the new operator the narrowing operator. The new operator does not necessarily reduce the values as quickly as the greatest-lower-bound operator, but at least
returns values which are less or equal to the old values.
In the case of the interval analysis, a narrowing operator is obtained by allowing
interval bounds only to be improved by replacing infinite bounds with finite bounds.
This way, each interval can at most be improved at most twice. For variable bindings
D we define:
D = D
=
=
= D2
and for D1
D2 ) x = (D1 x)
(D2 x)
(D1
where
[l2 , u 2 ] = [l, u]
where
[l1 , u 1 ]
l if l1 =
l= 2
l
1 otherwise
u 2 if u 1 =
u=
u 1 otherwise
In the applications of the narrowing operator, the left operand is the value of the last
iteration step, while the right operand is the newly computed value. Therefore, the
narrowing operator does not treat both its operands in the same way and thus is not
necessarily commutative.
Example 1.10.9 Let us apply the accelerated narrowing with round-robin iteration
to the program of Example 1.10.1. We obtain:
0
l
u
l
u
l
u
0 + + +
1 0 + 0 + 0 42
2 0 + 0 41
0 41
3 0 + 0 41
0 41
4 0 + 0 41
0 41
5 0 + 0 41
0 41
6 1 + 1 42
1 42
7 42 +
8 42 + 42 + 42 42
We observe that no information is lost despite the application of accelerated
narrowing.
67
Widening, in principle, also works for nonmonotonic right sides in the system of
inequalities. However, narrowing requires monotonicity. Accelerated narrowing is
guaranteed to terminate if the narrowing operator admits only descending chains of
is defined in a way
bounded length. In the case of interval analysis, our operator
that each interval would be modified at most twice. This means that round-robin
iteration using this narrowing operator takes at most O(n #Vars) rounds, where n
is the number of program points.
68
also say that R1 is an alias for R2 and vice versa. An important question about
programs written in a language with dynamic memory allocation is whether two
pointer variables possibly have the same value at some program point, that is, whether
the program may be in a state in which the two pointers are aliases. This problem
is called the may-alias problem. Another question is whether two pointer variables
always have the same value at a program point. This problem correspondingly is
called the must-alias problem.
69
70
0
X new(2)
1
X new(2);
Y new(2);
X[0] Y ;
Y new(2)
2
X[0] Y
Y [1] 7;
3
Y [1] 7
4
X
Y
0
1
0
1
Fig. 1.31 Program state after the execution of the program of Fig. 1.30
0
R null;
A:
R null;
if (T =
null) {
H T;
T T [0];
H[0] R;
R H;
goto A;
Zero(T = null)
7
NonZero(T = null)
2
HT
3
T T [0]
4
H[0] R
5
RH
6
The program allocates two blocks. A pointer to the second block is stored at
address 0 of the first block. Value 7 is stored at address 1 of the second block.
Figure 1.31 shows the state of memory after the execution of this program.
Example 1.11.2 A somewhat more complex example is the program in Fig. 1.32,
which reverses a list pointed to by pointer variable T .
71
=
=
=
=
=
adresses
values
memory
states
The program state has an integer component, h, which keeps track of how many
blocks have already been allocated, that is, how often the statement new() has been
executed. The set of values also contains, in addition to the integer numbers, addresses
of memory blocks. In each state this is an address between ref 0 and ref h 1, i.e.,
the ith allocated block is associated with address ref i 1. Addresses are values of
pointer variables. Recall that pointers may only point to the beginning of memory
blocks and not inside blocks. A program state consists of a variable binding and a
memory. The memory associates a value with each cell in each allocated block.
Let (, h, ) State be a program state. The concrete edge effects for the new
statements are:
[[R new(e)]] (, h, ) = ( {R
ref h}, h + 1,
( {(ref h, i)
null | i [[e]] })
[[z R[e]]] (, h, ) = ( {z
( R, [[e]] )}, h, )
[[e2 ]] })
[[R[e1 ] e2 ]] (, h, ) = (, h, {( R, [[e1 ]] )
The most complex operation is the operation new(). According to our semantics, it
performs the following steps:
1. It computes the size of the new block;
1
Note that this roughly corresponds to a change from considering the memory component as a
contiguous array containing directly addressed objects to a managed memory component where
new blocks are dynamically allocated.
72
Y new(4);
X new(4);
After executing the left program fragment, X and Y have received the values
ref 1 and ref 2 while after executing the right program fragment, X and Y have
received the values ref 2 and ref 1. The two fragments, therefore, cannot be considered
as equivalent. In many cases, though, the semantics of a program is meant to be
independent of the precise values of addresses. In these cases, program states should
be considered as equal, if they are equalup to some permutation of addresses
appearing in the program states.
=
=
=
=
Nodes
2Addr
Addr Val
(Pointers Val ) Store
73
0
X new(2)
1
0 1
Y new(2)
0 {0, 1} {0, 1}
X[0] Y
1
2
3
{0} {0, 1}
{0} {1}
{0} {1} {1}
Y [1] 7
{0}
2
3
{1}
{1}
Fig. 1.33 The abstract states for the program of Example 1.11.1
Pointers Vars is the set of pointer variables. The abstract states ignore all int
values and the special pointer constant null. We will use the generic name (D, M)
for abstract states. Abstract states have a canonical partial order, derived from set
inclusion:
(D1 , M1 ) (D2 , M2 ) if ( R Pointers. D1 (R) D2 (R))
( u Addr . M1 (u) M2 (u))
Example 1.11.3 Let us regard again the program of Example 1.11.1. Figure 1.33
shows the abstract states for the different program points. The analysis does not lose
any information in this example since each edge allocating a new block is only visited
once, and since each block is assigned an address only once.
We have seen above that the points-to analysis we are about to design will, in
general, have imprecise information about where a pointer variable or a pointer
expression point to. Let us consider how this lack of information propagates: It
propagates from the right side of an assignment to the left side, from a pointer
component in memory to the left side of a read if the analysis has already collected
several possible values for this pointer component. It may increase when the analysis
accumulates information for all possible target addresses for a write to memory.
The abstract edge effects for the points-to analysis are:
[[(_, R1 R2 , _)]] (D, M)
[[(u, R new(e), _)]] (D, M)
[[(_, R1 R2 [e], _)]] (D, M)
[[(_, R1 [e1 ] R2 , _)]] (D, M)
=
=
=
=
(D {R1
D R2 }, M)
(D {R
{u}},
M)
0(D {R1
{M a | a D R2 }}, M)
(D, M {a
(M a) (D R2 ) | a D R1 })
74
the entry for a variable in an abstract variable binding is called destructive update.
Destructive update, although it may sound negative, leads to more precise information. In the presence of pointers, we resort to nondestructive updates since a pointer
variable or pointer expression at some program point may point to different memory cells when the program point is reached several times. Non-destructive update
accumulates all possibilities that cannot be excluded and may therefore lead to less
precise information.
For a read from a block in memory, the address is not necessarily known. To be
on the safe side, the new value of a pointer variable on the left side is defined as the
union of the contributions of all blocks whose abstract addresses the analysis has
collected for the right side.
For a write to memory, we need to take care of the case of multiple abstract
target addresses a, which may each correspond to a set of concrete target addresses.
Writing to memory can therefore not be recorded destructively, i.e., by overwriting.
Instead, the set of addresses forming the potential new abstract address is added to the
sets M a.
Without initializing new blocks the analysis would have to assume for each block
that it may contain any possible value. Only since the operation new() returns initialized blocks can the analysis produce any meaningful information about memory
contents. Alternatively, we could assume that a correct program execution would
never use the contents of an uninitialized memory cell as address. Program behavior
is exactly as in the case of a program where each cell of a newly allocated block is
initialized to null before the block is used.
A System of Inequalities
A system of inequalities is derived from the control-flow graph of a program based on
the abstract domain State and the abstract edge effects. Nothing is known about the
values of pointer variables before program execution. No blocks are, yet, allocated.
The initial abstract state is therefore (D , M ), where
D x = ,
D R = Addr ,
M a =
for all int-variables x, all pointer variables R, and all abstract addresses a.
Let P[v] for all program points v be the least solution of the system of inequalities.
This least solution associates with each program point v an abstract state P[v] =
(D, M) that delivers for each pointer variable R a superset of the abstract addresses
of memory blocks to which R may point when v is reached. In consequence, R is
not an alias of any other pointer variable R if (D R) (D R ) = .
Note that we ignore the concrete value null in the abstract state. The error of
dereferencing a null pointer can therefore not be detected nor the absence of such an
error be proved.
75
We would like to prove the correctness of the analysis. Disappointingly, this proof
is not possible with respect to the operational semantics we started with. This is due
to the fact that different program executions may perform the hth allocation of a
block at different edges. However, we have already complained about our operational
semantics being too detailed as it uses absolute addresses. The number h of an
allocation should have no semantical significance. One way out of this problem
is to use an auxiliary semantics, which is instrumented with additional, helpful
information. The auxiliary semantics does not just use the values ref h, h N0 ,
as concrete addresses. Instead, it uses:
Addr = {ref (u, h) | u Nodes, h N0 }
Thus, the instrumented concrete semantics keeps track of the source node of the edge
at which a new block is allocated. The addresses grouped at the edges this way can
be easily mapped to abstract addresses. First, a proof of correctness with respect to
the instrumented semantics needs to performed for the analysis. Then the equivalence
of the original and the instrumented semantics needs to be shown. Exercise 23 gives
the reader the opportunity to produce these two proofs.
An Efficient Implementation
The implementation of the flow-insensitive analysis merits some more consideration. We introduce one unknown P[R] per program variable R and one unknown
P[a] per abstract address a instead of considering the one global abstract state as a
whole.
76
0
X new(2)
1
Y new(2)
0 1
X[0] Y
3
Y [1] 7
4
Fig. 1.34 The result of the flow-insensitive analysis of the program of Example 1.11.1
An edge (u, lab, v) of the control-flow graph leads to the following inequalities:
Lab
R1
R
R1
R1 [e]
Inequalities
R2
new(e)
R2 [e]
R2
P[R1 ] P[R2 ]
P[R]
{u}
P[R1 ] {P[a] | a P[R2 ]}
P[a] (a P[R1 ]) ? P[R2 ] : for all a Addr
All other edges do not have an effect. In this system of inequalities, the inequalities
for assignments to pointer variables and read operations are no longer destructive.
We assume that all pointer variables are initialized with null at program start to be
able to compute nontrivial information for pointer variables. Alternatively, we could
assume that the first access will only happen after an initialization. The system of
inequalities has a least solution P1 [R], R Pointers, P1 [a], a Addr since the
right sides of the inequalities are monotonic functions over the set of addresses. This
least solution can again be determined by round-robin iteration.
In order to prove the correctness of a solution s State of the system of
inequalities, it suffices to show for each edge k of the control-flow graph that the
following diagram commutes:
[[k]]
s1
s
where is a description relation between concrete and abstract values. The system
of inequalities has the size O(k n), if k is the number of needed abstract addresses
and n is the number of edges in the control-flow graph. The values which the fixedpoint algorithm computes are sets of a cardinality less than or equal to k. Therefore,
77
0
X new(2)
1
Y new(2)
2
X[0] Y
3
Y [1] 7
4
Fig. 1.35 The equivalence classes of the relation for the program of Example 1.11.1
the values of each of the unknowns P1 [R] and P1 [a] can change at most k times.
Given the low precision of the flow-insensitive analysis this method is still rather
expensive. Also, for may alias analysis, one is not interested in in the sets P1 [R] or
P1 [a] themselves, but whether or nor their pairwise intersection is nonempty.
In order to do so, we consider two radical modifications of the flow-insensitive
points-to analysis. First, we replace the set Addr of abstract addresses with the set
of all expressions R[], R a pointer variable. The abstract address R[] then represents
all memory blocks possibly pointed at by R. Second, we no longer consider inclusions of abstract values but equivalences. Let Z = {R, R[] | R Pointers}. Two
elements from Z should be as equivalent, if they may represent variables or memory
blocks which may contain the same address. Accordingly, the idea is to compute an
equivalence relation on the set Z of variables and abstract addresses.
Example 1.11.5 Consider again the trivial program of Example 1.11.1. Figure 1.35
shows an equivalence relation for this example. The equivalence relation directly
indicates which pointer expressions possibly evaluate to the same addresses different
from null.
Let E be the set of equivalence relations over Z . We regard an equivalence relation
1 as less than or equal to another equivalence relation 2 , if 2 contains more
equivalences than 1 , that is, if 1 2 . E is a complete lattice with respect to this
order.
Like the preceding points-to analysis, the new alias analysis is flow-insensitive,
that is, one equivalence relation is computed for the whole program. As any equivalence relation, can be represented as the partition = {P1 , . . . , Pm } of pointer
variables and abstract addresses that are considered as equivalent. Let 1 and 2 be
equivalence relations and 1 and pi 2 be the associated partitions. Then 1 2
holds if and only if the partition 1 is a refinement of the partition 2 , that is, if each
equivalence class P1 1 is contained in an equivalence class P2 2 .
An individual equivalence class P Z of an equivalence relation should be
identified by a representative p P. For simplicity, we choose this representative
in Pointers whenever P Pointers
= . Let = {P1 , . . . , Pr } be a partition and pi
78
be the representative of the equivalence class Pi . The analysis we aim at needs the
following two operations over :
Pointers find (, p)
returns the representative of class Pi where p Pi
Partition union (, pi1 , pi2 ) returns {Pi1 Pi2 } {P j | i 1
= j
= i2 }
i.e., forms the union of the two represented classes.
If R1 , R2 Pointers are equivalent then we regard R1 [] and R2 [] as equivalent.
Therefore, the operation union will be applied recursively:
Partition union (, q1 , q2 ) {
pi1 find (, q1 );
pi2 find (, q2 );
if ( pi1 = pi2 ) return ;
else {
union (, pi1 , pi2 );
if ( pi1 , pi2 Pointers) return union (, pi1 [], pi2 []);
else return ;
}
}
The operation union as well as the derived operation union are monotonic on
partitions. The alias analysis using these operations iterates exactly once over the
edges of the control-flow graph and unifies the left and the right side when it encounters an edge at which pointers are changed:
{{R}, {R[]} | R pointer};
forall ((_, lab, _) edge) [[lab]] ;
Thereby, we have:
[[R1 R2 ]]
[[R1 R2 [e]]]
[[R1 [e] R2 ]]
[[lab]]
=
=
=
=
union (, R1 , R2 )
union (, R1 , R2 [])
union (, R1 [], R2 )
otherwise
Example 1.11.6 Consider again the program of Example 1.11.1. Figure 1.36 shows
the steps of the new analysis for this program.
Example 1.11.7 Let us also look at the result of the flow-insensitive alias analysis for
the program of Example 1.11.2 to reverse lists in Fig. 1.37. The result of the analysis
is not very precise: All pointer variables and all blocks may be aliases of each
other.
79
0
X new(2)
{{X}, {Y }, {X[]}, {Y []}}
1
Y new(2)
2
X[0] Y
3
Y [1] 7
Fig. 1.36 The flow-insensitive alias analysis for the program of Example 1.11.1
0
R null;
Zero(T = null)
7
NonZero(T = null)
2
HT
3
T T [0]
4
H[0] R
(4, 5)
(5, 6)
5
RH
6
The alias analysis iterates once over the edges. This is no accident. A second
iteration would not change the partition, see Exercise 24. This method computes the
least solution of the system of inequalities over partitions:
P2 [[lab]] P2 , (_, lab, _) edge of the control-flow graph
The correctness proof again assumes that all accesses to cells only happen after these
have been initialized. Let us now estimate the needed effort for the alias analysis.
Let k be the number of pointer variables and n be the number of edges in the controlflow graph. Each edge is considered exactly once. For each edge, there is at most one
call to the function union . Each call to union performs two calls of the function
find. The operation union and possibly also recursively the function union are only
called if these calls to find return representatives of two different equivalence classes.
At the beginning, there are 2k equivalence classes. Each call to union decreases the
80
1
0
Fig. 1.38 The partition = {{0, 1, 2, 3}, {4}, {5, 6, 7}} of the set {0, . . . , 7} represented by parent
links
number of equivalence classes. So, at most 2k 1 calls of the operation union are
possible and therefore at most O(n + k) calls of the operation find.
We need an efficient data structure to support the operations union and find. Such
union-find data structures are well-known in the literature. We present a particularly
simple implementation, invented by Robert E. Tarjan. A partition of a finite base set
U is represented as a directed forest:
For each u U there exists a parent link F[u].
An element u is a root in the directed forest if the parent link points from u to u,
i.e. if F[u] = u.
All nodes that may indirectly reach the same root through their parent links form an
equivalence class, whose representative is the root.
Figure 1.38 shows the partition {{0, 1, 2, 3}, {4}, {5, 6, 7}} of the base set U =
{0, . . . , 7}. The lower part shows the representation by an array F with parent links,
which are visualized above the array.
The operations find and union can be easily implemented in this representation.
find : To find the representative of the equivalence class of an element u it suffices
to follow the parent links starting at u until an element u is found whose parent
link points to u .
union : To form the union of the equivalence classes of two representatives u 1 and
u 2 the only action needed is to make the parent link of one of the elements point
to the other element. The result of applying the union operation to the example
partition of Fig. 1.38 is shown in Fig. 1.39.
The operation union only requires O(1) steps. The costs of the operation find,
however, are proportional to the length of the path from the element at the start of the
search to the root of the associated tree. This path can be very long in the worst case.
An idea to prevent long paths is to always hang the smaller tree below the bigger
one. Using this strategy in the example of Fig. 1.38, the operation union would set
81
1
0
5
6
Fig. 1.39 The result of applying the operation union(, 4, 7) to the partition of Fig. 1.38
Fig. 1.40 The result of applying the operation union(, 4, 7) to the partition of Fig. 1.38 considering the size of the involved equivalence classes
the parent link of the element 4 to 7 and not the other way round (Fig. 1.40). The
algorithm needs to account for the size of the equivalence classes in order to know
which class is the smaller and which is the bigger. This makes the costs of the union
operation slightly more expensive. Let n be the number of union operations that are
applied to the initial partition 0 = {{u} | u U }. The length of the paths to a
root is then at most O(log(n)). Accordingly, each find operation has costs at most
O(log(n)).
Amazingly enough, this data structure can be improved even further. To do this,
the algorithm redirects the parent links of all visited elements directly to the root
of the associated tree during a find operation. This increases the costs of each find
operation by a small constant factor, but decreases the costs of later find operations. Figure 1.41 shows how the paths to the root are shortened when this idea
is used.
82
3
2
7
4
7
4
5
0
5
0
4 5
The left tree has paths of length up to 4. A find inquiry for node 6 turns nodes
3, 7, 5 and 6 into direct successors of root 1. This shortens the paths in the example
to lengths of at most 2.
This implementation of a union-find data structure has the property that n union
operations and m find operations together only have costs O((n + m) log (n)),
where log is the inverse of the iterated exponentiation function: log (n) is the least
83
i = 1, . . . , n
is round-robin iteration. It is easily implementable and can nicely be manually simulated. However, this procedure has its disadvantages. First, it needs a whole round
to detect termination of the iteration. Second, it reevalutes all right sides f i for the
unknowns xi anew, although only the value of one variable might have changed since
the last round. Last, the runtime depends heavily on the used order of the variables.
A more efficient algorithm is the worklist algorithm. This procedure administers
the set of variables xi whose values might no longer satisfy their inequality in a data
structure W , the worklist. For a variable xi taken out of the worklist, the value of
its right side is computed using the actual values of the unknowns. The old value of
xi is replaced by a new value that subsumes the previous and the newly computed
value of xi if the newly computed value is not subsumed by the previous value. The
worklist has been shortened by taking out one element. In the case that the value of
xi has grown the inequalities whose right sides depend directly on the value of xi
might be no more satisfied. The left sides of these possibly violated inequalities are
remembered for a recomputation, i.e., inserted into the worklist W .
The implementation of this procedure uses for each variable xi the set I [xi ] of all
variables whose right side possibly depends directly on xi . These direct dependences
between variables are easily identified in the examples of program analyses presented
so far: In a forward analysis, the value for a program point u influences the value at
a program point v directly if there is an edge from u to v in the program control-flow
graph. Analogously, the value at v influences the value at u in a backwards analysis
if there is an edge from u to v. The precise determination of dependences may not
always be that easy. It may be more difficult if the right sides of constraints are only
given as semantic functions f i whose implementation is unknown.
In the description of the algorithm, we again distinguish between the unknowns
xi and their values. The values of variables are stored in the array D, which is
indexed with variables. The worklist W , on the other hand, administers unknowns
and not values. In our formulations of generic systems of inequalities, we have always
assumed that the right sides f i are functions of type Dn D, i.e. may possibly
depend on all variables. We now want to take into account that evaluating the right
side of a variable may access the values only of some other variables. Therefore, we
now consider right sides f i of the functionality
f i : (X D) D
84
85
D[x1 ] D[x2 ] D[x3 ]
x1 ,x2 ,x3
{a }
x2 ,x3
{ a}
{a }
a ,c
x1 ,x2
{a, c}
x3 ,x2
a ,c
{a, c}
a ,c
{a, c}
a ,c
x3
x2
The steps of the worklist algorithm applied to this system of inequalities is shown in
Fig. 1.42. The next variable xi to be taken out of the worklist is emphasized in the
actual worklist. Altogether six evaluations of right sides suffice. This would not be
beaten by a round-robin iteration.
The next theorem collects our observations about the worklist algorithm. To specify its precise runtime we recall that the height h of a complete lattice D is defined as
the maximal length of any strictly ascending chain of elements in D. The size | f i | of
a right side f i is defined as the number of variables that are possibly accessed during
the evaluation of f i . The sum of the sides of all right sides therefore is given by:
xi X
| fi | =
#I [x j ]
x j X
This equality results from the fact that each variable dependence x j xi is counted
exactly once in the sum on the left and also exactly once in the sum on the right side.
Accordingly,
the size of the system of inequalities in a set of unknowns X is defined
as the sum xi X (1 + #I [xi ]). Using this definition we find:
Theorem 1.12.1 Let S be a system of inequalities of size N over the complete lattice
D of height h > 0. We have:
1. The worklist algorithm terminates after at most h N evaluations of right sides.
2. The worklist algorithm produces a solution. It delivers the least solution if all
f i are monotonic.
Proof To prove the first claim, we observe that each variable xi can only change
its value at most h times. This means that the list I [xi ] of variables depending on
xi is added to the worklist at most h times. Therefore, the number of evaluations is
bounded from above by:
n
n + i=1
nh # I [xi ]
= n +
h i=1
# I [xi ]
n
(1 + # I [xi ])
h i=1
=hN
86
Of the second claim we only consider the statement about monotonic right sides. Let
D0 be an array which represents the least solution of the system of inequalities. We
first prove that we have at any time:
D0 [xi ] D[xi ]
Finally, we convince ourselves that after executing the body of the while loop,
all variables xi for which the corresponding inequality is actually violated, are contained in the worklist. This worklist is empty when the algorithm terminates. Hence
on termination, all inequalities must be satisfied, and the array D therefore represents
a solution. The least solution of the system of inequalities is an upper bound of this
solution. Consequently, the found solution must be equal to the least solution.
According to Theorem 1.12.1, the worklist algorithm finds a solution also in case
of nonmonotonic right sides. This solution is not necessarily a least solution. It is
just some solution. A similar behavior has been observed for round-robin iteration.
The worklist algorithm can be simplified if all right sides are monotonic. The accumulation at the recomputation of the values can then be replaced with
overwriting.
t D[xi ] t; == ;
For iterations using widening accumulation works again differently: In this case, the
widening operator is applied to the old and the new value instead of the least
upper bound.
t D[xi ] t;
==
t D[xi ] t;
==
t;
t D[xi ]
where the iteration of the while loop does not start with the value for each variable
but with a previously computed solution of the system of inequalities.
In practice, the worklist algorithm has proved very efficient. It still has two disadvantages:
The algorithm needs the direct dependences between the unknowns, that is, the
sets I [xi ]. These dependences were quite obvious in the examples so far. This,
however, is not the case in all applications.
The actual value of a required variable xi is accessed when the right side of an
unknown is evaluated, no matter if this is still a trivial value or an already computed
nontrivial value.
A better strategy would be to first try to compute a reasonably good value for a
variable x j before the value is accessed. To improve further, we extend the function
eval by monitoring: before function eval delivers the values of a variable x j it keeps
87
book of the variable xi for whose right side the value of x j is needed, that is, the
function eval adds xi to the set I [x j ]. Function eval therefore receives variable xi as
a first argument. Function eval should not return the actual values of x j , but the best
possible value for x j . Therefore, the computation of an as-good-as-possible value for
x j is triggeredeven before the variable dependence between x j and xi is recorded.
Altogther function eval turns into:
D eval (xi ) (x j ) { solve(x j );
I [x j ] {xi } I [x j ];
return D[x j ];
}
Function eval together with procedure solve recursively compute a solution. To
prevent an infinite recursion procedure solve manages a set stable. This set stable
contains all variables for which an evaluation of the corresponding right sides has
already been triggered and and not yet finished together with those variables for
which (relative to the actual values of variables in the set stable) the fixed-point has
already been reached. For the variables in the set stable procedure solve will not do
anything.
Set stable is initialized with the empty set at the start of the fixed-point iteration.
The program performing the fixed-point iteration looks as follows:
stable ;
forall (xi X ) D[xi ] ;
forall (xi X ) solve(xi );
where the procedure solve is defined as follows:
void solve (xi ){
stable) {
if (xi
stable stable {xi };
t f i (eval (xi ));
t D[xi ] t;
if (t
= D[xi ]) {
D[xi ] t;
W I [xi ]; I [xi ] ;
stable stable\W ;
forall (xi W ) solve(xi );
}
}
}
The call of procedure solve(xi ) directly terminates if the variable xi is already stable.
Otherwise, the variable xi is added to the set stable. After that, the right side f i of xi
is evaluated. Instead fo the actual variable binding, procedure solve uses the function
88
solve(x2 )
solve(x3 )
D[x1 ] = { a}
I[x1 ] = { x3 }
{ a}
D[x3 ] = { a, c }
I[x3 ] =
solve(x1 )
D[x1 ] = { a, c }
I[x1 ] =
solve(x3 )
ok
I[x3 ] = { x1 , x 2 }
{ a, c }
D[x2 ] = {a}
89
the computed new entries D[xi ] and I [xi ] as well as the calls of procedure solve
to treat variables in the set W . An ok in this column signals that a reevaluation of
the right side does not require a change of the actual value of the variable. A stable!
indicates that the variable for which the last call to solve was performed is stable,
such that the call should directly terminate. The column of function eval indicates
changes of the set I [x j ] and the returned values. The algorithm evaluates fewer right
sides than the worklist algorithm, although this example is very small.
The recursive fixed-point algorithm can be elegantly implemented in a programming
language such as OCaml, which has assignments on one side, and partial applications
of higher-order functions on the other side.
The recursive fixed-point algorithm is more complicated than the worklist algorithm.
It executes, in general, less evaluations of right sides. It does not need a precomputation of the variable dependences, and, even better, it also works when variable
dependences change during the fixed-point iteration. In addition, it has a property
that we will later exploit for interprocedural analysis in Sect. 2.5: The algorithm can
be modified such that not the values of all unknowns are computed. Rather, the evaluation of an unknown of interest, xi , can be started. Then only those unknowns whose
values are needed to evaluate the unknown xi are themselves evaluated. Fixed-point
iterators having this property are called local.
90
0
1
x M [a]
T x+1
T x+1
T x+1
x M [a]
T x+1
M [T ] 5
M [T ] 5
5
T x+1
6
M [x] T
M [x] T
xe
5
6
xe
xe
6 to the edge from 2 to 5 and thus avoid a redundant computation on the right path.
This program transformation results in the program on the right of Fig. 1.44.
We look for a transformation that places assignments x e at points in the program
such that
variable x is guaranteed to contain the value of e whenever the program executes
the assignment x e the next time, and
the insertion of new redundant computations are avoided.
Consider Fig. 1.13. In Fig. 1.45, x e is available along some, but not all paths,
synonymously called partially redundant at program point 5. The compiler, therefore,
cannot eliminate the two occurrences of x e on the paths starting at 5. We observe,
however, that x e is going to be executed on every computation starting at program
point 5, before the variable x is used or any variable occurring in this statement is
overwritten. We say, the assignment x e is very busy at program point 5. This also
is true for program points 1, 2, and 4. At program point 4, however, x e is already
available and therefore need not be computed once more. Instead, the compiler may
insert the assignment x e before program point 1. After this insertion, x e
is redundant at program points 2 and 5 as well as at the program points 6 an 8
91
1
xe
xe
5
6
xe
xe
5
6
xe
and thus allows to remove the assignments there. The situation looks different in
Fig. 1.46. There, the assignment x e is already redundant at program point 5 as
well as at program point 8. Therefore, already redundancy elimination succeeds in
removing the assignment x e at the edge from 8 to 9. Finally in Fig. 1.47, the
assignment x e is neither redundant nor very busy at program point 5. Therefore,
no optimization is possible.
The transformation therefore inserts an x e at the end of an edge e with
endpoint v, if two conditions are satisfied: First, the assignment should not already
be available along this edge e. Second, it should be very busy at v. This means that
the assignment x e is executed on all outgoing paths from v before the left side
of the assignment, x, is used, and before any of the variables of the statement is
modified.
92
{[[]] | : u stop}
where [[]] is the effect of path like in the other backwards analyses. Thus,
[[]] = [[k1 ]] . . . [[km ]]
for = k1 . . . km . The abstract edge effects [[ki ]] are all distributive. The least
solution, B, with respect to the chosen partial order is, therefore, equal to the MOP
solutionprovided that the program exit is reachable from any program point.
Example 1.13.2 Figure 1.48 shows the sets of very busy assignments for the program
of Example 1.13.1. The control-flow graph is acyclic in this case. This means that
round-robin iteration can compute these sets in one round.
Note that the reachability of program exit is of great importance for the result of the
backwards analysis for very busy assignments. Let us assume that program point v
does not reach program exit. The analysis would start with the set of all assignments
at all nodes without outgoing edges. Going backwards, it would remove only those
with a new definition of a variable in Vars(e) {x} until it reaches v. The remaining
assignments would all be considered very busy at v.
Example 1.13.3 Consider the program of Fig. 1.49.
93
0
3
1
x M [a]
T x+1
T x+1
M [T ] 5
5
T x+1
6
M [x] T
5 { T x + 1}
4
3 { T x + 1}
2 { T x + 1}
1
0
a M [7]
1
4
2
xa+b
The program admits only one, infinite computation. Program exit, node 4, is not
reachable from program point 1. Therefore, any assignment not containing variable
x is very busy at 1, even assignments that do not occur in the program.
94
v on which it was previously not available. This insertion makes the business sites
redundant such that they can be eliminated. In the motion view of the optimization,
the compiler takes the business sites and moves copies backwards into the paths on
which the movable assignment was not previously available.
The remaining open question for both views is at what edges to place the copies of
the movable assignment z e. The answer is that a placement at the new positions
should establish that y e is definitely available at all positions at which it has
been very busy before. In the control-flow graphs that we consider here, y e is
always very busy at the source program-point of an edge labeled with y e.
The optimization uses the strategy to place assignments as early as possible, maybe
even before the program entry point. This placement is constrained by correctness
and by efficiency considerations: Correctness reasons inhibit the movement of the
assignment y e over an edge leading from u to v at which y or a variable occurring
in e receive a new value. After such a move, y might have a different value at v, or the
evaluation of e in the moved assignment might result in a wrong value. For efficiency
reasons, the optimization may not move y e onto a path on which it originally
would not have been executed. Otherwise, an execution of this path would lead to a
potential runtime increase.
There is another argument why we are so cautious not to move move an assignment
onto a path that did not contain that assignment before. Depending on the semantics
of the programming language, the assignment may have side effects, e.g., throwing
an exception at a division by zero. Throwing an exception on a path of the optimizd
program where the corresponding path of the original program did not throw an
expection, violates the requirement of semantics preservation.
We now go through the different cases for the placement of movable assignments.
Let us first consider potential insertions at the entry point, start, of the program.
We introduce a new entry point and insert all assignments from B[start] before
start to make all assignments from B[start] definitely available at start. This is
realized by the first transformation rule.
Transformation PRE for the Start Node:
B[v]
v
We have taken the liberty to annotate an edge with a set of mutually independent
assignments, which can be executed in any order. To assure independence of two
y2 and that neither
assignments y1 e1 and y2 e2 B[u] one checks that y1
y1 occurs in e2 nor y2 in e1 .
Next, we consider a program point u which is nonbranching. This means that
u has exactly one outgoing edge whose label s either is a skip operation, with an
assignment or a memory access. This edge is assumed to lead to a program point v.
All assignments from B[v] should be placed at this edge besides the ones that are still
95
very busy at u, i.e., could be moved further, and the ones that were already available
at v, i.e., need not be placed here. This set is given by:
u
xe
;
v
u
ss
An edge labeled with the empty statement can receive the set ss of assignments
where ss is defined as: B[v]\(B[v] A[u]) = . No additional assignment needs to
be placed at this edge.
A movable assignment, x e with x
Vars(e), is moved to another place,
and the set ss of assignments is placed at its position where ss is obtained from the
definition of ss above by substituting the definitions of the abstract edge effects for
their names:
ss = B[v]\(B[v]\(Occ(x) Ass(e)) A[u]\Occ(x) {x e})
= (B[v] Occ(x)\{x e}) (B[v] Ass(e)\A[u])
Transformation PRE for Nonmovable Statements:
An edge labeled with a nonmovable statement s is replaced by a sequence of two
edges labeled with s and ss:
u
s
v
ss
v
The new label ss for an edge from u to v labeled with the nonmovable assignment
x e with x Vars(e) is:
ss = B[v] (Occ(x) Ass(e))\(A[u]\Occ(x))
= (B[v] (Occ(x)) (B[v] Ass(e)\A[u])
The set of assignments ss to place at a read operation x M[e] are defined analogously. For a write operation M[e1 ] e2 we obtain:
96
u
Zero(b)
v1
u
NonZero(b)
Zero(b)
NonZero(b)
v2
ss1
ss2
v1
where
v2
The given transformation rules for PRE make each assignment x e at all
program points available at which x e was very busy before the transformation.
Therefore, an assignment x e is in particular available at all program points where
it would have been computed in the original program.
Example 1.13.4 Figure 1.50 shows the analysis information for the program of
Example 1.13.1 together with the result of the optimization. In fact, one partially
redundant computation could be removed.
Let ss be the set of assignments that are very busy at v. To prove correctness one
shows that for all execution paths of the program from the original entry point of
the program to a program point v and all program states before program execution
it holds that:
[[ss]] ([[]] ) = [[k0 ]]
where k0 is the new edge leading to the original entry point of the program, and [[]]
and [[]] are the semantics of the program paths before and after the application
of the transformation. The validity of the claim can be proved by induction. For the
empty program path = it is trivially true. For a nonempty program path = k,
it follows from the induction hypothesis with a case distinction according to the label
of the last edge k of the path.
As the example indicates, the number of executions of the assignment x e has
increased on no path, but may have been decreased on some paths. It would be nice
97
0
1
x M [a]
3
T x +1
2
T x +1
M [T ] 5
5
6
M [x] T
0
1
2
3
4 {T
5
6 {T
7{T
T x +1 }
{ T x +1 }
x +1 }
{
{}
T x +1 }
x +1 }
x +1 }
Fig. 1.50 The result of the transformation PRE for the Example 1.13.1 together with the necessary
analyses
to prove this property of nonpessimization for all programs. However, we will not
do this here. The intuition on which the proof is based is that the assignment x e
becomes available at all program points in the transformed program at which it was
previously very busy and is now up to removal. Each copy of a movable assignment
can so be associated with at least one subsequent occurrence that is eliminated.
The elimination of partially redundant assignments also removes some totally
redundant assignments as a side effect. More assignments may become partially
available by the application of transformation PRE. It may, therefore, pay to
apply the transformation PRE repeatedly. Similar methods can be use to save on
memory accesses. An alias analysis then must be used to refine the dependences
between the set of memory accesses. Such analyses are described in Sect. 1.11.
An important aspect of the transformation PRE is that it supports the removal of
loop-invariant code from loops. This will be considered in the next section.
98
0
i0
0
i0
Zero(i < n)
T b+3
1
Zero(i < n)
NonZero(i < n)
2
NonZero(i < n)
T b+3
3
3
M [A + i] T
M [A + i] T
4
ii+1
i i+1
Figure 1.51 shows the control-flow graph of this program. The loop contains the
assignment T b + 3, which computes the same value on each iteration of the
loop and stores it in the variable T . Note that the assignment T b + 3 cannot
be moved before the loop, as indicated in Fig. 1.51 because it would be executed in
executions that would not enter the loop.
This problem does not occur with do-while loops. In a do-while loop, loop-invariant
code can be placed just infront of the loop. One way to avoid the placement problem
with invariant code in while loops therefore is to transform them into do-while loops
beforehand. The corresponding transformation is called loop inversion.
Example 1.14.2 Let us regard again the while loop of Example 1.14.1. The following
Fig. 1.52 shows the inverted loop on the left side. The inverted loop has an extra
occurrence of the loop condition guarding the first entry into the loop. Another
occurrence of the loop condition is at the end of the loop. Figure 1.53 shows the
analysis information for partial redundancy elimination. Figure 1.52 on the right
shows the application of transformation PRE. The loop-invariant code has been
moved to before the do-while loop.
We conclude that transformation PRE is able to move loop-invariant code out
of do-while loops. To treat while loops, these need to be transformed into do-while
loops. This is straightforward in most imperative and object-oriented programming
languages if the source code of the program is available. In C or Java, for example,
the while loop:
while (b) stmt
can be replaced by:
if (b) do stmt while (b);
99
0
i0
0
i0
1
NonZero(i < n)
Zero(i < n)
NonZero(i < n)
Zero(i < n)
T b+3
T b+3
3
M [A + i] T
M [A + i] T
4
ii+1
6
Zero(i < n)
i i+1
5
NonZero(i < n)
Zero(i < n)
NonZero(i < n)
Fig. 1.52 The inverted loop of Example 1.14.1 and the result of moving invariant code out of the
loop
A
0
1
2
3
4
5
6
{T b + 3 }
{T b + 3 }
{T b + 3 }
{ T b + 3}
Fig. 1.53 The partial-redundancy analysis-information for the inverted loop of Fig. 1.52
100
P
{0 }
{0 , 1 }
{0 , 1 , 2 }
3 {0 , 1 , 2 , 3}
4 {0 , 1 , 2 , 3 , 4}
0
1
2
3
4
1
2
{ 0 , 1 , 5}
Example 1.14.3 Regard the example of the while loop on the left side of Fig. 1.51.
Each program point of the loop body is predominated by program point 1. Edge
(6, ; , 1) therefore is a back edge.
We design a simple analysis to determine the set of predominators at the program
points of programs. It collects the set of program points traversed along each path.
The set of predominators for a program point v is obtained as the intersection of
these sets. As complete lattice we therefore choose:
P = 2Nodes , with the partial order
We define as abstract edge effects:
[[(u, lab, v)]] P = P {v}
for all edges (u, lab, v). Note that the labels of the edges play no role. Instead, the
analysis collects the endpoints of the edges. These abstract edge effects lead to the
following set of predominators: P [v] at program point v:
P[v] =
{[[]] {start} | : start v}
All abstract edges effects are distributive such that these sets can be determined as
least solution of the associated system of inequalities.
Example 1.14.4 Regard the control-flow graph for the example program of Example
1.14.1. Figure 1.54 shows the associated predominator sets. Figure 1.55 shows the
associated partial order .
As usual in the representation of partial orders, only the direct relations are represented. Transitivity stays implicit. The result apparently is a tree! This is by no
means an accident, as the following theorem shows.
101
P
0
1
2
{0}
{0 , 1 }
{0 , 1 , 2 }
3 {0 , 1 , 2 , 3}
4 {0 , 1 , 2 , 3 , 4}
{ 0 , 1 , 5}
Fig. 1.55 The predominator relation for the control-flow graph of Fig. 1.54. The direction goes
from top to bottom
Theorem 1.14.1 Each program point v has at most one immediate predominator.
Proof Assume that a program point v had two different direct predominators u 1 , u 2 .
Neither u 1 u 2 nor u 2 u 1 can be true. Therefore, not every path from entry
point of the program to u 1 nor every path from u 1 to v can contain program point
u 2 . Thus, there exists a path from the entry point to v that does not contain u 2 , and
u 2 cannot be a predominator of v. This is a contradiction to the assumption above.
So, v has at most one direct predominator. This proves the theorem.
The entry condition of the while loop is represented by a program point v with two
outgoing condition edges. v predominates all nodes in the loop body, in particular
the source node u of the back edge. Loop inversion consists in creating a new node
with copies of the condition edges emanating from it to the targets of the original
targets, and then redirecting the back edge from u towards the new node. This is the
next transformation:
Transformation LR:
v
Zero(e)
lab
v
Zero(e)
NonZero(e)
u
NonZero(e)
v P[u]
Zero(e)
u
NonZero(e)
lab
Loop inversion works for all while loops. There are, however, loops that cannot be
inverted in this way. One such, somewhat unusual, loop is shown in Fig. 1.56. Unfortunately, there exist quite normal loops that cannot be inverted by transformation LR.
Figure 1.57 shows one such loop. One would have to copy the whole path from the
back edge to the condition together with the condition edges to invert such a loop.
102
0
1
predominators:
3
1
2
This kind of loop can originate when a complex condition is evaluated in several
steps.
103
0
T x+1
1
2
M [x] T
3
4
2
T x+1
M [x] T
Fig. 1.59 The desired optimization for the program of Fig. 1.58
D\(Ass(e) Occ(x)) {x e} if x
Vars(e)
D\(Ass(e) Occ(x))
if x Vars(e)
where Ass(e) is the set of assignments to variables, occurring in e and Occ(x) is the
set of assignments in which x occurs. Using these conventions we define the rest of
the abstract edge effects:
104
u
ss
;
v
This edge receives all assignments that cannot be moved beyond its target node v, but
whose left side is live at v. The sequence of assignments, ss, to move there consists
of all x e D[u]\D[v] with x L[v].
Transformation PDE for Assignments:
The assignment y e cannot be moved if y Vars(e). In this case, the transformation is:
u
ss1
u
ye
ye
v
ss2
v
The sequence ss1 collects those useful assignments that cannot be delayed beyond
y e. The sequence ss2 collects those useful assignments that can be delayed along
this edge, but not beyond its target node. Therefore, ss1 is a sequence consisting
of the assignments x e D[u] (Ass(e) Occ(y)) with x in L[v]\{y}
Vars(e). Furthermore, ss2 is a sequence consisting of the assignments x e
D[u]\(Ass(e) Occ(y) D[v]) with x L[v].
An assignment y e satisfying y
Vars(e) can be delayed by the transformation:
105
u
ss1
ye
ss2
v
The sequence ss1 is defined in the same way as in the case of delayable assignments.
The sequence ss2 is defined analogously: It collects all useful assignments that can
be delayed along the edge, but not beyond its target edge. Possibly, the sequence ss2
could contain an occurrence of y e.
This means, ss1 is a sequence formed out of assignments x e D[u]
(Ass(e) Occ(y)) with x L[v]\{y} Vars(e). Furthermore, ss2 is a sequence of
assignments x e (D[u]\(Ass(e) Occ(y)) {y e})\D[v] with x L[v].
Transformation PDE for Conditional Branches:
u
ss0
u
Zero(b)
NonZero(b)
Zero(b)
NonZero(b)
v2
v1
ss1
ss2
v2
v1
The sequence ss0 consists of all useful assignments that are delayable at u, but that
cannot be delayed beyond the condition edges. The sequences ssi , i = 1, 2, on the
other hand, consist of all useful assignments that can be delayed beyond the condition
edges, but not beyond the target node vi .
This means that the sequence ss0 consists of all assignments x e D[u] with
x Vars(b), and the sequences ssi for i = 1, 2, consist of all assignments with
x e D[u]\(Ass(b) D[vi ]) and x L[vi ].
Transformation PDE for Load Operations:
u
ss1
u
y M [e]
y M [e]
v
ss2
v
106
M [e1 ] e2
v
ss2
v
Again sequences ss1 and ss2 of assignments are required that are placed before
and after the original statement, resp. The sequence ss1 consists of the assignments
x e D[u] (Ass(e1 ) Ass(e2 )), and the sequence ss2 of the assignments
x e D[u]\(Ass(e1 ) Ass(e2 ) D[v]) with x L[v].
According to our assumption, a set X of variables is live at program exit. The last
rule treats the case of a nonempty set X of live variables. Assignments to variables
in X that are delayable at program exit need to be placed just before program exit.
This is done by the following transformation rule:
Transformation PDE for Program Exit:
Let u be the exit point of the original program. Then a new program exit point is
introduced:
u
u
ss
1 {x, T } {T x + 1}
2 {x, T } {T x + 1}
3
The application of transformation PDE transforms the control-flow graph of Fig. 1.58
into the control-flow graph of Fig. 1.59.
107
0
T x+1
1
4
2
y M [T ]
0
T x+1
T x+1
y M [T ]
y M [T ]
Fig. 1.61 The inverted loop and the removal of partially dead code
108
Loop inversion
Alias analysis
Constant folding
Interval analysis
RE Removal of redundant computations
CE Copy propagation
DE Elimination of dead assignments
PRE Removal of partially redundant assignments
PDE Removal of partially dead assignments
1.16 Exercises
1. Available assignments
Regard the control-flow graph of the function swap of the introduction.
(a) Determine for each program point u the set A[u] of assignments available at u.
(b) Apply transformation RE to remove redundant assignments.
2. Complete lattices
Consider the complete lattice M of monotonic boolean functions with two variables:
1
xy
y
x
xy
0
(a) Determine the set of all monotonic functions that map M to the complete
lattice 2 = {0, 1} with 0 < 1.
(b) Determine the order on these functions.
3. Complete lattices Show the following claims:
(a) If D1 , D2 are complete lattices then so is
D1 D2 = {(x, y) | x D1 , y D2 }
1.16 Exercises
109
f x (y) = f (x, y)
f y (x)= f (x, y)
(x D1 )
(y D2 )
are monotonic.
4. Complete lattices
For a complete lattice D let h(D) = n be the maximal length of a proper ascending
chain d1 dn . Show that for complete lattices D1 , D2 it holds that:
(a) h(D1 D2 ) = h(D1 ) + h(D2 )
(b) h(Dk ) = k h(D)
(c) h([D1 D2 ]) = #D1 h(D2 ), where [D1 D2 ] is the set of functions
f : D1 D2 and #D1 is the cardinality of D1 .
5. Introduction of temporary variables for expressions
Introduce temporary variables Te for given expressions e, such that the value of
e is stored in Te after each evaluation of e.
(a) Define a program transformation that introduces these temporary variables.
Argue that this transformation does not change the semantics of programs.
(b) What influence does this transformation have on the removal of redundant
computations RE?
(c) For which expressions is this introduction of temporary variables profitable?
How can the number of variable-to-variable copies be decreased by a subsequent transformation?
(d) How can the transformation PRE profit from this introduction of temporary
variables?
6. Available memory reads
Extend the analysis and transformation of available assignments such that the
new analysis also determines the availability of memory reads x M[e].
7. Complete lattices
Let U be a finite set and D = 2U be the powerset over U, ordered by = .
Let F the set of all functions f : D D of the form
f x = (x a) b
with a, b D. Show:
(a) F contains the identity function, and it has a least and a greatest element;
(b) F is closed under composition, , and ;
(c) A (postfix-) operation * can be defined on F by:
f x =
j0
f jx
110
8. Fixed-point iteration
Consider a system of inequalities of the form:
xi f i (xi+1 ), where f i is monotonic, for i = 1, . . . , n
Show:
(a) The fixed-point iteration terminates after at most n iterations.
(b) One round of a round-robin iteration suffices given a suitable order of variables.
9. Dead variables
Define a program analysis that determines for each program point the set of dead
variables directly, that is, not as the complement of the set of live variables.
(a) Define the associated lattice.
(b) Define the associated abstract edge effects.
(c) Extend the analysis to an analysis of definite deadness.
How could one prove the correctness of the analysis?
10. Distributivity I
Let f 1 , f 2 : D D be two distributive functions over a complete lattice D.
Show:
(a) f 1 f 2 is also distributive;
(b) f 1 f 2 is also distributive.
11. Distributivity II
Prove Theorem 1.7.1.
12. Distributivity III
Consider the function
f (X ) = (a X )?A : B
with A, B U for some universe U .
(a) Show that f is distributive both w.r.t. the ordering and the ordering on
the set 2U , whenever B A.
(b) Show for the ordering that f is completely distributive, if B = .
(c) Show for the ordering that f is completely distributive, if A = U .
13. Optimization of function swap
Apply first the optimization RE, then the optimizations CE and DE to the example program swap!
14. Constant propagation: signs
Simplify constant propagation such that it only considers the signs of values.
(a) Define an appropriate partial order for this analysis.
(b) Define the description relation ?
(c) Define appropriate abstract operators on the abstract values.
1.16 Exercises
111
112
(d) Define abstract versions of the arithmetic operators + and for stripes.
(e) Use that to create an analysis in the style of constant propagation of the
stripes.
18. Interval operators
Define the abstract operations ! (negation) and
= (inequality).
19. Description relation for intervals
Prove that the abstract multiplication for intervals respects the description relation , i.e., show that z 1 I1 and z 2 I2 together imply that
(z 1 z 2 ) (I1 I2 )
20. Interval analysis: refined widening
Define a partial order on intervals that makes it possible to modify the lower as
well as the upper bounds at most r times.
Define the description relation for this abstract domain. Define a new widening.
21. Interval analysis: termination
Give an example program for which the interval analysis does not terminate
without widening.
22. Alias analysis
Consider the following program:
for (i 0; i < 3; i++) {
R new();
R[1] i;
R[2] l;
l x;
}
Apply the point-to analysis and the alias analysis of Sect. 1.11 to this program.
23. Alias analysis: semantics
In Sect. 1.11, we introduced an instrumented operational semantics to prove the
correctness of the points-to analyses. Prove that this instrumented semantics is
equivalent to the natural operational semantics for programs with dynamic
allocation of memory blocks. To do this, formalize an equivalence notion that
relates the different sets of addresses employed by the two semantics, though
not globally, but for each concrete program execution.
24. Alias analysis: number of iterations
Show that the least fixed-point is already reached in exactly one iteration over
all edges in the equality-based alias analysis of Sect. 1.11.
25. Points-to analysis with Stripes
The precision of point-to analyses can be improved by an accompanying analysis of stripes (see Exercise 17). considering blocks identified through abstract
addresses not monolithically, but distiguishing an access A[e1 ] from an access
A[e2 ] if the stripes corresponding to the index expressions e1 and e2 do not inter-
1.16 Exercises
113
sect. Assume, e.g., that the stripe corresponding to e1 is (1, 2), while the stripe
= 0 + 42 , these
corresponding to e2 is (0, 4). Since for all 1 , 2 Z, 1 + 21
accesses definitely go to different memory locations.
Assume for the following, that for every program point v, an assignment S[v]
from the int variables of the program to stripes is given. The goal is to design a
refined points-to analysis which for each abstract address l, maintains a modulus
b together with a mapping:
l : {0, . . . , b 1} 2Val
(a) Define a partial ordering on such descriptions of heaps. How can a description relation be defined? What is the least upper bound operation between
two such abstract heaps?
(b) Define abstract effects of the various statements. How do you interpret
the new statement? What about reads from and writes to memory? Design
accesses in such a way that the modulus b for an abstract location l is the
gcd of all possible accesses to l.
(c) Use your analysis to infer more precise may alias information for expressions
A[e] and A [e ] occurring at different program points.
26. Worklist iteration
Perform worklist iteration for the computation of available assignments for the
factorial program. Determine the number of executed evaluations of right sides.
27. Available memory look-ups
Assume that we are given an equivalence relation on pointer variables which
equates variables R1 , R2 , if they may point to the same block in memory.
(a) Provide a refinement of availability analysis of expressions in variables to
memory reads and memory writes, which takes the alias information into
account.
(b) Use this analysis to extend redundancy elimination to memory operations.
(c) Apply your transformation to the body of the swap function from the introduction!
(d) Can your idea be extended also to partial redundancy elimination? How?
(e) Would your transformation benefit from stripes information (see Exercise
25)? Why?
28. Loop-invariant code
Perform code motion of loop-invariant code in the following program:
for (i 0; i < n; i++) {
b a + 2;
T b + i;
M[T ] i;
if ( j > i) break;
}
114
1.17 Literature
The foundations of abstract interpretation were laid by Cousot and Cousot in 1977.
Interval analysis is described for the first time in Cousot and Cousot (1976). This
article also describes the widening and narrowing techniques. A precise interval analysis without widening and narrowing is described in Gawlitza and Seidl
(2007). Monotone analysis frameworks are introduced by Kam and Ullman in 1976,
1977. Program analyses with distributive edge effects are considered by Kildall in
1973. Giegerich et al. develop the strengthening of liveness analysis to true liveness
(1981).
The recursive fixed-point algorithm were formally proven correct in Coq (Hofmann et al. 2010a, b). The presentation of partial-redundancy elimination and partial
dead-code elimination follows work by Knoop, Rthing, and Steffen (Knoop et al.
1994a, b; Knoop 1998).
Karr (1976) and Granger (1991) present generalizations of constant propagation
to the analysis of linear equalities. Their approaches are extended to interprocedural
versions in Mller-Olm and Seidl (2004, 2005, 2007). Cousot and Halbwachs (1978)
introduce an analysis of linear inequalities between variables. Practical applications
in the analysis of C programs are extensively discussed by Simon in 2008.
Our rather brief overview only discusses very simple analyses of dynamic data
structures. The alias-analysis problem for the programming language C is a real
challenge, and is even more difficult if pointer arithmetic is considered. The simple
methods described here follow the works of Steensgaard (1996) and Anderson et al.
(2002). Fhndrich et al. present interprocedural extensions (2000) as do Liang et al.
(2001). Ramalingam (2002) deals extensively with loop structures in control-flow
graphs. He gives axiomatic and constructive definitions of loop structures. Sagiv et
al. develop elaborated techniques for the analysis of program with dynamic memory
allocation and linked structures in the heap (Sagiv et al. 1999, 2002). These analysis
are of high complexity, but very powerful. They may automatically derive statements
about the shape of linked data structures.
Chapter 2
Interprocedural Optimization
115
116
2 Interprocedural Optimization
f ()
4
Ab
main()
0
Zero(A 1)
b3
1
NonZero(A 1)
9
6
f()
b A1
7
2
M [17] ret
ret 1
f()
8
ret A ret
10
Each procedure f has one entry point, startf , to which control is passed from the
caller, and one exit point, stopf , from which control is passed back to the caller. We
forbid outgoing edges from exit points of procedures to ensure deterministic program
execution. Program execution starts with the call to a dedicated procedure main ().
Example 2.1.1 We consider the factorial function, implemented by a procedure f
and a procedure main calling f after setting the global variable b to 3.
main() {
b 3;
f();
M[17] ret;
}
f() {
A b;
if (A 1) ret 1;
else {
b A 1;
f();
ret A ret;
}
The global variables b and ret hold the actual parameter and the return value of f,
respectively. The formal parameter of f is simulated by the local variable A, which
as first action in the body of procedure f receives the value of the actual parameter
b.
Programs in our extended programming language can be represented by sets of
control-flow graphs, one control-flow graph per procedure. Figure 2.1 shows the two
control-flow graphs for the program of Example 2.1.1.
117
==
==
==
==
==
==
Glob and Loc denote the set of global and local variables of the program, and point
denotes the set of program points.
Stacks grow from the bottom upwards in our graphical representation. The stack
frame of the actual procedure is always on top. Sequences of stacks representing
progress of a computation are listed from left to right. The stack of a caller is immediately to the left of the stack of the callee. The execution of the factorial program
of Example 2.1.1 yields a sequence of call stacks (see Fig. 2.2).
Computations
The steps of a computation always refer to the actual procedure. In addition to the
steps already known from programs without procedures, we need the following new
cases:
118
2 Interprocedural Optimization
5
1
A3
A3
A1
A2
A2
A2
A3
A3
A3
10 A 1
8
A2
A2
10 A 2
A3
A3
A3
8
2
A3
10 A 3
2
Fig. 2.2 A sequence of call stacks for the program of Example 2.1.1
119
combine((Loc , Glob , ), (Loc
, Glob
, )) = (Loc , Glob
, )
main
0, 1
f 5, 6, 7
f 5, 6, 7
f 5, 6, 7
f 5, 9, 10
/f
8, 10
/f
8, 10
/f
8, 10
/f
2, 3
/main
where we have listed only the traversed program points instead of the full edges
(u, lab, v) to improve readability.
Same-Level Computations
A computation that leads from a configuration ((u, Loc ), Glob , ) to a config ),
uration ((v, Loc
Glob , ) is called same-level since in such a computation each
entered procedure is also exited, which means that the height of the stack at the
end of the computation is exactly the height it had at the beginning. This means
that the nested computation path contains for every opening parenthesis
f also the
corresponding closing parenthesis
/f. Since the same-level computation does not
consult any stack frame in the stack below the initial one, it gives rise to computations leading from a configuration ( (u, Loc ), Glob , ) to the configuration
),
( (v, Loc
Glob , ) for every call stack .
Let us assume that a same-level computation
sequence leadsfrom a configura
),
tion ((u, Loc ), Glob , ) to a configuration (v, Loc
Glob , . This means that
this computation starts at program point u and reaches program point v, possibly with intervening procedure calls, if the variables have the values as given by
= Loc Glob and the memorys content is described by . Here, denotes the
disjoint union of two functions. The value of the variables and the content of memory
after the computation is described by = Loc
Glob and . The same-level computation therefore defines a partial function [[ ]], which transforms (Loc , Glob , )
,
into (Loc
Glob , ). This transformation can be determined by induction over the
structure of the computation:
120
2 Interprocedural Optimization
[[ k]] = [[k]] [[ ]]
[[1
f 2
/f]] = H ([[2 ]]) [[1 ]]
for a procedure f
Implementation of Procedures
The given operational semantics is closely related to an actual implementation of
procedures. Therefore, the semantics is useful to estimate the effort needed by a
procedure call.
Steps to be done before entering a procedure body:
121
machine to have fast access to the values of local variables. The values of local variables of the caller may have to be stored to memory when control switches from the
caller to the callee, and then reloaded into registers when control returns from the
callee to the caller. While access to the values of local variables during the execution
of a procedure body is fast, saving and restoring the values may require considerable
effort.
2.3 Inlining
The first idea to reduce the overhead of procedure calls is to place a copy of the
procedures body at the call site. This optimization is called inlining.
Example 2.3.1 Consider the program:
abs () {
b1 b;
b2 b;
max ();
}
max () {
if (b1 < b2 ) ret b2 ;
else ret b1 ;
}
122
2 Interprocedural Optimization
main
abs
max
ing the local variables of the called procedure. Still, there exists the threat of code
explosion if a procedure is called multiple times. Even worse, complete inlining
for recursive procedures does not terminate. Recursion must therefore be identified
in procedure calls before inlining can be applied. This can be done using the call
graph of the program. The nodes in this graph are the procedures of the program. An
edge leads from a procedure p to a procedure q if the body of p contains a call to
procedure q.
Example 2.3.2 The call graphs for the programs of Examples 2.1.1 and 2.3.1 are
quite simple (see Fig. 2.3). In the first example, the call graph consists of the nodes
main and f, and procedure main calls procedure f, which calls itself. In the second
example, the call graph consists of the two nodes abs and max and the edge leading
from abs to max.
There are various strategies to decide for a procedure call whether to inline the
procedure or not:
Inline only leaf procedures; these are procedures without any calls.
Inline at all nonrecursive call sites; these are call sites that are not part of strongly
connected components of the call graph.
Inline up to a certain fixed depth of nested calls.
Other criteria are possible as well. Once a procedure call is selected for inlining the
following transformation is performed:
Transformation PI:
u
f()
v
u
Af = 0;
copy
of f
;
v
(A Loc)
2.3 Inlining
123
The edge labeled with the empty statement can be avoided if the stop node of f has
no outgoing edges. Initializations of the local variables are inserted into the inlined
procedure because our semantics requires these to receive initial values.
if (b2 1) ret b1 ;
else {
b1 b1 b2 ;
b2 b2 1;
f ();
}
The last action to be performed in the body of the procedure is a call. Such a call
is known as a last call. Last calls do not need a separate frame on the call stack.
Instead, they can be evaluated in the same frame as the caller. For that, the local
variables of the caller must be replaced by the local variables of the called procedure
f. Technically, this means that the call of procedure f is replaced by an unconditional
jump to the beginning of the body of f. If the last call is a recursive call then this tail
recursion is tranformed into a loop, i.e., into iteration.In the example, this looks like:
f () {
_f : if (b2 1) ret b1 ;
else {
b1 b1 b2 ;
b2 b2 1;
goto _f;
}
}
Transformation LC:
u
A = 0;
f()
f() :
f() :
(A Loc)
v
According to our semantics, the local variables must be initialized with 0 before the
procedures body can be entered.
Tail-call optimization is particularly important for declarative programming languages, which typically do not offer loops. Iterative computations have to be
124
2 Interprocedural Optimization
expressed by recursion, which is more expensive than iteration because of the allocation and deallocation of stack frames. An advantage of tail-call optimization over
inlining is that it avoids code duplication. Last-call optimization can also be applied
to nonrecursive tail calls. A somewhat disturbing feature of tail-call optimization
is that it introduces jumps out of one procedure into another one, a concept that is
shunned in modern high-level programming languages.
The reuse of the actual stack frame for the procedure called in a tail call is only
possible if the local variables of the caller are no longer accessible. This is the case in
our programming language. The programming language C, however, permits access
to local variables at arbitrary positions in the call stack through pointers. Similar
effects are possible if the programming language provides parameter passing byreference. To apply tail-call optimization, the compiler then has to ensure that the
local variables of the caller are indeed no longer accessible. An analysis determining
sets of possibly accessible variables is the subject of Exercise 2.
work () {
A b;
if (A) work ();
ret A;
}
125
work ()
main()
0
A M [0]
Ab
1
Zero(A)
8
NonZero(A)
Zero(A)
NonZero(A)
print ()
3
bA
4
work ()
work ()
10
ret A
11
5
ret 1 ret
6
Fig. 2.4 The control-flow graphs for Example 2.5.1
Figure 2.4 shows the control-flow graphs associated with this program. Copying
the value of global variable b into local variable A inside procedure work can be
avoided.
The problem with the generalization of the intraprocedural approach to copy propagation is that the interprocedural analysis no longer works on one control-flow graph,
but needs to cope with the effects of possibly recursively called procedures.
126
2 Interprocedural Optimization
To realize the analysis we need abstractions of the operations enter and combine
of the operational semantics that are specific for the analysis to be designed. The
abstract operation enter initializes the abstract value for the entry point of the
procedure using the analysis information available at the program point before
the procedure call. The operation combine combines the information obtained
at the exit of the procedure body, its second argument, with the information available upon procedure entry, its first argument. These operations therefore have the
functionality:
enter
: DD
combine : D2 D
The overall abstract effect of a call edge k then is given by:
[[k]] D = combine (D, [[f]] (enter D))
if [[f]] is the transformation associated with the body of procedure f.
combine (V1 , V2 ) = V2
Let us now consider an analysis of programs with local variables. The analysis
should determine for each program point the set of variables containing the value
last assigned to the global variable x.
During program execution, a copy of this value may be stored in a local variable
of the caller of a procedure. This local variable is not visible inside the callee. If the
callee assigns a new value to x the analysis cannot assume for any local variable of
the caller that it (still) contains xs value. To determine whether such a recomputation
may take place we add a new local variable ; xs value before the call is recorded in
it. is not modified during the call. The analysis checks whether after return from the
callee, is guaranteed to still hold the value that x had before the call. Technically
it means that function enter adds the local variable to the set of global variables
that had the value of x before the call. After return from the callee, the local variables
of the caller still contain the last computed value of x if is contained in the set
returned by the analysis of the callee. We define:
= V Glob {}
enter V
combine (V1 , V2 ) = (V2 Glob) (( V2 ) ? V1 Loc : )
127
where Loc = Loc{}. The complete lattice used for the analysis of interprocedural
copy propagation for a global variable x is:
V = {V Vars | x V }
ordered by the superset relation, , where Vars = Vars {}.
Abstract Edge Effects of Call Edges
In analogy to the concrete semantics we define, as a first step, for each complete
lattice D, for monotonic edge effects [[k]] , and for monotonic functions enter and
combine the abstract transformation effected by a same-level computation:
= [[k]] [[ ]]
for a normal edge k
[[ k]]
[[1
f 2
/f]] = H ([[2 ]] ) [[1 ]] for a procedure f
where the transformation
H : (D D) D D
is defined by
The abstract effect [[f]] for a procedure f should be an upper bound for the abstract
effects [[ ]] of each same-level computation for f, that is, each same-level computation starting at the entry point of f to its exit point. We use a system of inequalities
over the lattice of all monotonic functions in D D to determine or to approximate
the effects [[f]] .
[[startf ]]
[[v]]
[[v]]
[[f]]
Id
H ([[f]] ) [[u]]
[[k]] [[u]]
[[stopf ]]
128
2 Interprocedural Optimization
Theorem 2.6.1 can be proved by induction over the structure of same-level computations . The theorem guarantees that each solution of the system of inequalities
can be used to approximate the abstract effects of procedure calls.
Problems of the Functional Approach
There are two fundamental problems connected to this approach. First, the monotonic
functions in D D need to be effectively represented. However, functions occurring
here do not always have simple representations. In the case of finite complete lattices
D all occurring functions can, at least in principle, be represented by their value
tables. This is no longer possible if the complete lattice D is infinite as is the case
for constant propagation. In this case a further complication may arise, namely that
of infinite ascending chains, which never stabilize.
Let us return to our example of interprocedural copy propagation. In this case,
the following observation helps: The complete lattice V is atomic. The set of atomic
elements in V is given by:
{Vars \{z} | z Vars \{x}}
Furthermore, all occurring abstract edge effects are not only monotonic, but are even
distributive with respect to . Instead of considering all monotonic functions in
V V it suffices to calculate in the sublattice of distributive functions.
Distributive functions over an atomic lattice D have compact representations. Let
A D be the set of atomic elements in D. According to Theorem 1.7.1 in Sect. 1.7
each distributive function g : D D can be represented as
g(V ) = b
{h(a) | a A a V }
129
h1
h2
{b, ret, }
Vars
{b, }
{b, A, } {b, A, }
Vars
{b, A, ret} {b, A, ret} {b, A, ret}
In a first round of a round-robin iteration, program point 7 is associated with the
identity function; program points 8, 9, and 10 with the function g1 ; and program
point 11 with the composition g2 g1 . This last function is given by:
g2 (g1 (C)) = C {A, ret} =: g3 (C)
It delivers the first approximation for the body of function work. Thereby, we obtain
for a call to function work:
combine (C, g3 (enter (C))) = C {ret} =: g4 (C)
In the second round of a round-robin iteration, the new value at program point 10 is
the function g1 g4 g1 where
g4 (g1 (C)) = C {A, ret} and consequently:
= g1 (C)
g1 (C) g4 (g1 (C)) = C {A}
as in the last iteration. In this example, the fixed point is reached in the first
iteration.
F) =
{H (g) | g F}
for each nonempty set F of distributive functions. We have then for each procedure
f and each program point v of f,
[[v]] =
{[[ ]] | Tv }
130
2 Interprocedural Optimization
Here Tv is the set of all same-level computations from the entry point start f of
procedure f to v.
The proof of this theorem is a generalization of the proof of the corresponding theorem for intrarocedural analyses. To easily apply Theorem 2.6.2, we need a
class of functions enter and combine as large as possible with the property that
transformation H becomes distributive. We observe:
Theorem 2.6.3 Let enter : D D be distributive and combine : D2 D have
the form:
combine (x1 , x2 ) = h 1 (x1 ) h 2 (x2 )
for two distributive functions h 1 , h 2 : D D. Then H is distributive, that is, it
holds that
{H (g) | g F}
H ( F) =
for each nonempty set F of distributive functions.
Proof Let F be a nonempty set of distributive functions. We then have:
H (
F) = h 1 h 2 ( F) enter
= h 1 h 2 ( {g enter | g F})
= h 1 ( {h 2 g enter | g F})
= {h 1 h 2 g enter | g F}
= {H (g) | g F}
The second equation holds because composition is distributive in its first argument,
and the third equation holds since composition is distributive in its second argument,
provided the first argument is a distributive function.
Let us recall the definitions of the two functions enter and combine for the propagation of copies. We had:
enter V
= V Glob {}
combine (V1 , V2 ) = (V2 Glob) ( V2 ) ? V1 Loc :
= ((V1 Loc ) Glob)
((V2 Glob) Loc ) (Glob ( V2 ) ? Vars : )
The function enter therefore is distributive. Likewise, the function combine can
be represented as the intersection of a distributive function of the first argument
with a distributive function of the second argument. Theorem 2.6.3 can therefore be
applied for the order . We conclude that the transformation H for copy propagation
is distributive for distributive functions. Therefore, the interprocedural coincidence
theorem (Theorem 2.6.2) holds.
131
enter (d0 )
enter (D[u])
(u, f(), v) call edge
combine (D[u], [[f]] (enter (D[u]))) (u, f(), v) call edge
[[k]] (D[u])
k = (u, lab, v) normal edge
Example 2.7.1 Let us regard again the program of Example 2.5.1. We obtain for
program points 0 to 11:
0 {b}
1 {b}
2 {b}
3 {b}
4 {b}
5 {b, ret}
6 {b}
7
{b, }
8 {b, A, }
9 {b, A, }
10 {b, A, }
11 {b, A, , ret}
We conclude that the global variable b can be used instead of the local variable A
inside procedure work.
If all program points are reachable the second phase of the interprocedural analysis
also satisfies a conincidence theorem.
Theorem 2.7.2 Let us assume that for each program point v there exists at least
one v-reaching computation. Let us further assume that all effects [[k]] : D D
132
2 Interprocedural Optimization
of normal edges as well as the transformation H are distributive. Then we have for
each program point v,
D[v] =
{[[ ]] d0 | Pv }
Here, Pv is the set of all v-reaching computations.
133
that is, whose values are demanded during the analysis. Technically, this means that
the analysis should constrain itself to those unknowns whose values are accessed
during the fixed-point iteration when computing the value D[main, enter (d0 )].
The demand-driven evaluation of a system of inequalities needs an adequate fixedpoint algorithm. The local fixed-point algorithm of Sect. 1.12 can be applied here! It
explores the variable space according to the possibly dynamic dependences between
variables. Assume that the initial abstract state for program entry is given by d0 D.
When started with an initial query for the value of D[main, enter d0 ], the local
fixed-point iteration will explore every procedure f only for that subset of abstract
states which are queried when trying to compute the value for the procedure exit of
main, when called with the abstract value enter d0 .
Let us demonstrate the demand-driven variant of the functional approach with an
example analysis. We choose the interprocedural constant propagation. The complete
lattice for this analysis is as before:
D = (Vars Z )
This complete lattice is of finite height, but not finite. Interprocedural constant propagation might, therefore, not terminate. The functions enter and combine for
constant propagation are given by:
if D =
D
{A
|
A
lokal}
otherwise
if D1 = D2 =
combine (D1 , D2 ) =
D1 {b
D2 (b) | b global} otherwise
enter D
Together with the intraprocedural abstract edge effects for constant propagation we
obtain an analysis that terminates for lattices of finite height D if and only if the
fixed-point algorithm needs to consider only finitely many unknowns D[v, a] and
D[ f, a].
Example 2.8.1 Let us consider a slight modification of the program of Example 2.5.1
with the control-flow graphs of Fig. 2.5. Let d0 be the variable binding:
, b
, ret
}
d0 = {A
This leads to the following sequence of evaluations:
134
2 Interprocedural Optimization
work ()
main()
0
7
Ab
A0
8
1
Zero(A)
NonZero(A)
Zero(A)
NonZero(A)
print ()
work ()
10
bA
ret A
11
work ()
5
ret 1 ret
6
Fig. 2.5 The control-flow graphs for Example 2.8.1
A b ret
0, d0
1, d0
2, d0
3, d0
4, d0
7, d1
8, d1
9, d1
10, d1
11, d1
5, d0
6, d0
main, d0
0
0
0 0
0
0 0
0 0
0 0 0
0 0 0
0 0 1
0 0 1
for d1 = {A
, b
0, ret
}. The right side of each unknown is evaluated
at most once in this example.
In the example, the analysis terminates after only one iteration. Only one copy
needs to be considered for each program point. In general, the analysis needs to
calculate with several copies. The lattice does not have infinite ascending chains.
Thus, the analysis terminates if each procedure is called only with finitely many
arguments during the iteration.
135
work ()
main()
0
7
Ab
A0
8
1
Zero(A)
NonZero(A)
Zero(A)
NonZero(A)
print ()
3
10
bA
enter
combine
ret A
enter
11
combine
5
ret 1 ret
6
Fig. 2.6 The interprocedural supergraph for the example of Fig. 2.4
136
2 Interprocedural Optimization
work ()
main()
0
7
A0
Ab
8
1
Zero(A)
NonZero(A)
Zero(A)
NonZero(A)
print ()
3
combine
10
bA
enter
ret A
enter
11
combine
5
ret 1 ret
6
enter (d0 )
enter (D[u])
combine (D[u], D[f])
[[lab]] (D[u])
D[stopf ]
Example 2.9.2 Let us again regard the program of Example 2.8.1. The inequalities
at the program points 5, 7, and 10 are:
D[5] combine (D[4], D[work])
D[7] enter (D[4])
D[7] enter (D[9])
D[10] combine (D[9], D[work])
Correctness of this analysis is proved with respect to the operational semantics. Constant propagation finds, in this example, the same results as full constant propagation.
The interprocedural supergraph, however, contains additional paths that the program
can, in fact, never take, so-called infeasible paths. These infeasible paths can impair
the precision of the results. Such an infeasible path is shown in Fig. 2.7.
Only one abstract value is computed for each program point. Termination of the
analysis is thus guaranteed if the used complete lattice has only ascending chains
that eventually stabilize.
137
For a comparison, let us have a look at the constraint system for call strings of
length 1. This system has the unknowns D[v, ] where v is a program point and
is a call string of length at most 1. Then we define:
D[start main , ]
D[start f , g]
D[v, ]
D[v, ]
D[f, ]
enter (d0 )
enter (D[u, ])
combine (D[u, ], D[f, g])
[[lab]] (D[u, ])
D[stopf , ]
Example 2.9.3 Let us regard again the program of Example 2.8.1. The inequalities
at the program points 5, 7, 10 are:
D[5, ] combine (D[4, ], D[work, main])
D[7, main] enter (D[4, ])
D[7, work] enter (D[9, work])
D[10, main] combine (D[9, main], D[work, work])
D[10, work] combine (D[9, work], D[work, work])
As for call strings of length 0, the number of unknowns of the constraint system is
independent of the complete lattice D of the analysis. Termination of the analysis is,
thus, guaranteed if the used complete lattice has only ascending chains that eventually
stabilize.Since the number of contexts which are distinguished is larger than for the
constraint system for call string 0, the analysis result is potentially more precise.
For constant propagation, e.g., it may find more interprocedural constants than an
analysis with call string 0.
2.10 Exercises
1. Parameter passing
Describe a general method using global variables for passing call-by-value parameters.
Show that global variables can also be used for returning results of a procedure
call.
2. Reference parameter
Extend the example programming language by the possibility to store the address
& A of a local variable A in another variable or in memory.
(a). Design a simple analysis to determine a superset of the local variables whose
address is taken and stored in another variable or in memory.
138
2 Interprocedural Optimization
f() {
x 1;
y 1;
f1 ();
}
main() {
n M[17];
f();
M[42] z;
}
2.11 Literature
139
2.11 Literature
A first approach to the static analysis of programs with procedures is contained
in the article by Cousot and Cousot (1977). Independent of this work, Sharir and
Pnueli present the functional and the call-string approaches in 1981. This article contains an interprocedural coincidence theorem for procedures without local variables.
A generalization for procedures with local variables is contained in Knoop and Steffen (1992). Fecht and Seidl present in 1999 a discussion of several local fixed-point
algorithms as they are applicable to Prolog programs.
An application of interprocedural analysis methods for the precise analysis of
loops is described by Martin et al. (1998).
An important optimization of object-oriented programs attempts to identify
data objects of fixed size that do not escape from a given method call. These do
not need to be allocated on the heap, but can be allocated directly on the stack
(Choi et al. 1999).
Inlining is very important for object-oriented programs because they often have
many small functions. These consist of only a few statements such that the effort
needed for a call is greater than for the method itself. However, aggressive inlining
finds its limits at dynamic method calls. The static type of an object may deviate
from the run-time type of the object. This means that the method actually applied
may be statically unknown. An interprocedural analysis is used to determine the
exact dynamic type of the object at the call site. For a fast static analysis, as used in
just-in-time compilers, simple, context-insensitive analysis such as the Rapid Type
Analysis (Bacon 1997) are used. It only considers the call sites of the program and
ignores all variables and assignments. The precision can be improved at the cost of
its efficiency by considering assignments to variables and their types (Sundaresan
et al. 2000).
Our list of program optimizations is by no means complete. We have not discussed
the reduction in operator strength (Paige and Schwartz 1977; Paige 1990; Sheldon
et al. 2003) or methods dedicated to programs working with arrays, and concurrent
programs.
Chapter 3
141
142
example, are tail calls. Ignoring the allocation of all values on the heap, including the
int values, the imperative program generated for the function fac could look like this:
int fac(int x) {
int a, a1 , b, b1
a 1; b x;
fac2 : if (b 1) return a;
else {
a1 a b; b1 b 1;
a a1 ; b b1 ;
goto fac2;
}
}
The intraprocedural optimizations for imperative programs described so far can,
therefore, also be used to improve functional programs. Assignments to dead variables can be removed. Constants or copies can be propagated. In the example, the
temporary variables a1 , b1 can be eliminated; these variables were introduced for the
evaluation of the arguments of the recursive applications of fac2.
In general, the control flow resulting from the translation of functional programs
into imperative programs is quite confusing, both for human readers and static analysis. Better results in the analysis and the optimization of functional programs can be
obtained if the specific properties and sources of inefficiencies of functional programs
are taken into account.
143
listed on the left sides of function definitions. Instead, functional abstraction is always
used: fun x1 fun x2 . . . fun xk . . .. We also omit function definitions by
cases since these can be equivalently formulated by match expressions. Furthermore,
we assume that all programs are well-typed.
A function max computing the maximum of two numbers looks as follows:
let max = fun x fun y if x1 < x2 then x2
else x1
144
The application of the let optimization to the second let returns the expression:
let x = 17
in let x = 4
in (fun y x + y) x
The variable x, now visible in the function as well as in its argument, represents the
value 4. The expression evaluates to the value 8.
There exist several possibilities to solve this problem. The simplest possibility,
which we will also use, consists in renaming the variables that are bound in e0 in
such a way that their new names are different from those of variables occurring free
in e1 . Renaming of this kind is called conversion.
Example 3.2.2 Consider again the expression of Example 3.2.1. The free variable
x of the function fun y x + y occurs as bound variable in the expression into
which the function is to be substituted. Renaming this occurrence of x results in the
expression:
let x = 17
in let f = fun y x + y
in let x = 4
in f x
The substitution of fun y x + y for f produces:
let x = 17
in let x = 4
in (fun y x + y) x
Evaluation of this expression produces the correct result, 21.
145
in the application (fun x e0 ) e1 will only be evaluated if and when the value of
x is accessed during the evaluation of e0 .
Eager evaluation of let expressions, as in OCaml, will evaluate the expression
e1 in any case. The evaluation of the whole let expression does not terminate if the
evaluation of e1 does not terminate. If x does not occur in the main expression e0
or only in a subexpression that is not evaluated, then evaluation of the transformed
expression may still terminate although the evaluation of the original expression
would not.
Example 3.2.3 Consider the program:
let rec f = fun x 1 + f x
in let y = f 0
in
42
With lazy evaluation, the program returns the value 42. With eager evaluation, the
program does not terminate since the evaluation of f 0 is started before the value 42
is returned. The variable y does not occur in the main expression. The application of
the let optimization would remove the evaluation of f 0. Consequently, the evaluation
of the optimized program would terminate and return 42.
One is perhaps not worried about an improved termination behavior. The situation is
different if the evaluation of e1 has side effects which are required. This cannot happen in our small example language. OCaml expressions may well raise exceptions
or interact with their environment, independently of whether their return value is
accessed or not. In this case the application of the transformation must be restricted
to expressions e1 that neither directly nor indirectly cause side effects. This is definitely the case with variables and expressions that directly represent values such as
functions.
Further optimizations become possible when let definitions are moved in front of
the evaluation of an expression.
((let x = e in e0 ) e1 )
= (let x = e in e0 e1 ),
if x is not free in e1
(let y = e1 in let x = e in e0 ) = (let x = e in let y = e1 in e0 ),
if x is not free in e1 and y is not free in e
(let y = let x = e in e1 in e0 ) = (let x = e in let y = e1 in e0 ),
if x is not free in e0
The applicability of these rules is not restricted if no side effects are involved. Even
the termination behavior does not change. The application of these rules may move
a let definition further to the outside creating chances, e.g., for the application of
the transformation Inlining presented in the next section. Further movements of let
definitions are discussed in Exercises 1, 2, and 3.
146
3.3 Inlining
As for imperative programs, inlining for functional programs is performed to save
the costs associated with function application. Inlining of a function f means that the
body of f is copied at the place of application. This is quite analogous to inlining
in imperative languages as treated in Sect. 2.3. A notable difference between the
two transformations is that our imperative core language simulated parameter and
result passing by copying values between global and local variables and possibly
also memory. We, therefore, assumed procedures to have no parameters. Under this
assumption, procedure inlining just means to replace the call to a procedure by a
copy of its body.
With functional languages, me must make passing of parameters explicit. Let us
assume that a function f is defined as let f = fun x e0 . Inlining replaces the
application f e1 by:
let x = e1 in e0
Example 3.3.1 Consider the program fragment:
let fmax = fun f fun x fun y
if x > y then f x
else f y
in let max = fmax (fun z z)
Applying inlining to the definition of max results in:
let max = let f = fun z z
in fun x fun y if x > y then f x
else f y
Inlining of f then delivers:
let max = let f = fun z z
in fun x fun y if x > y then let z = x
in z
else let z = y
in z
Applying the let optimizations for variables and useless constant definitions yields:
let max = fun x fun y if x > y then x
else y
3.3 Inlining
147
148
map. Such functions distill the algorithmic essence of a programming technique and
are instantiated for a particular application by supplying the required parameters,
including functional parameters.
Example 3.4.1 Consider the following program fragment:
let f = fun x x x
in let rec map = fun g fun y match y
with
[] [ ]
| x1 :: z g x1 :: map g z
in map f list
The actual parameter of the function application map f is the function fun x
x x. The function application map f list thus represents a function that squares all
elements of the list list. Note that we have as usual written the list constructor :: as
infix operator between its two arguments.
Let f be a recursive function and f v an application of f to an expression v that
represents a value, that is, either another function or a constant. Our goal is to introduce a new function h for the expression f v. This optimization is called function
specialization.
Let f be defined by let rec f= funx e. We define h by:
let h = let x = v in e
Example 3.4.2 Let us regard the program fragment of Example 3.4.1.
let h = let g = fun x x x
in fun y match y
with
[] [ ]
| x1 :: z g x1 :: map g z
The function map is recursive. Therefore, the body of the function h contains another
application of map. Specialization of map for this application would introduce a
function h1 with the same definition (up to renaming of bound variables) as h. Instead
of introducing this new function h1 , the application map g is replaced by the function
h. This replacement of the right side of a definition by its left side is called function
folding. Function folding in the example yields:
let rec h = let g = fun x x x
in fun y match y
with
[] [ ]
| x1 :: z g x1 :: h z
149
150
b | fun x e | c v1 . . . vk | (v1 , . . . , vk ) | 1 v | v1 2 v2
for basic values b, arbitrary expressions e, constructors c and unary and binary
operators 1 , 2 , respectively, that return basic values.
Let G E V be a relation between expressions from E and V . Expressions
e E are associated with the set [[e]]G of all value expressions that can be derived
from e using G. Each pair (e, t[e1 , . . . , ek ]) G for expressions e, e1 , . . . , ek E
and patterns t can be seen as the inequality
151
The sets [[e]]G , e E, are defined as the least solution of this system of inequalities.
Example 3.5.2 Let G be the relation
{(i, 2), (i, i + 1)} .
The set [[i]]G consists of all expressions of the form 2 or (. . . (2+1) . . .)+1. Note that
in this analysis, operator applications are treated in the same way as data constructors.
The set [[from (i + 1)]]G on the other hand is empty for
G = {(from (i + 1), i :: from (i + 1))}
A relation G can be seen as a regular tree grammar with the set of nonterminals
E and constants and function abstractions as 0-ary terminal symbols, and operators
and constructors as multiplace terminal symbols. For an expression e E, i.e., a
nonterminal, the set [[e]]G denotes the set of terminal expressions derivable from e
according to the grammar (see Exercises 4 and 5). The sets [[e]]G are, in general,
infinite. The relation G, however, is a finite representation of these sets, which makes
it possible to decide simple properties of the sets [[e]]G . The most important question
is whether [[e]]G contains a certain term v. This question can be easily answered if
v is a function abstraction fun x e . Each pair (e, u) from G has a right side u
that is either a constant, a function, or the application of a constructor or an operator.
Therefore, (fun x e ) [[e]]G holds if and only if (e, fun x e ) G.
Further examples of properties that can be easily decided are:
Is [[e]]G nonempty?
Is [[e]]G finite, and if yes, of which elements does this set consist?
The goal of our value analysis is to construct a relation G for a program such that
for each expression e of the program, the set [[e]]G contains all values to which
e may develop during runtime, relative to the bindings for the free variables in e.
The relation G E V is defined by means of axioms and derivation rules. For
convenience, we will not define the relation G itself but the relation which is
the relation G, extended with all pairs (v, v) for v V . Also for convenience, we
write the relation in infix notation. Axioms represent those relationships that hold
without preconditions:
vv
(v V )
i.e, expressions from the set V are related to themselves. We supply rules for each
program construct:
Function application. Let e (e1 e2 ). We have the rules:
152
e1 fun x e0
ev
e0 v
e1 fun x e0
xv
e2 v
e1 v
xv
ei v
xi v
e0 v
yv
If an alternative evaluates to a value, then the whole case distinction may evaluate
to that value, as long as the corresponding pattern cannot be statically excluded
for the evaluation of the expressions e0 . The analysis does not track the exact
values of operator applications. Therefore, basic values are always assumed to
be possible. This is different for patterns c y1 . . . yk . Such a pattern matches the
value of e0 only if e0 v holds, where c is the outermost constructor of v. In this
153
case v has the form v = c e1 . . . ek , and the values for ei are potential values for
the variables xi .
Conditional expressions. Let e if e0 then e1 else e2 . For i = 1, 2 we have:
ei v
ev
These rules are similar to the rules for match-expressions where basic values are
used as patterns.
Example 3.5.3 Consider again the program
let rec from = fun i i :: from (i + 1)
and first = fun l match l with x :: xs x
in first (from 2)
This program terminates only with lazy evaluation like in Haskell. In OCaml,
however, with eager evaluation, the application from 2 and with it the whole program
does not terminate. One possible derivation of the relation x2 would look as
follows:
fromfun i i :: from (i + 1)
firstfun l . . .
from 2i :: from (i + 1)
li :: from (i + 1)
x2
fromfun i i :: from (i + 1)
i2
We have left out occurrences of axioms vv. For e E let G(e) be the set of all
expressions v V , for which ev can be derived.
The analysis of this program delivers for the variables and function applications:
G(from)
= {fun i i :: from (i + 1)}
G(from (i + 1)) = {i :: from (i + 1)}
G(from 2)
= {i :: from (i + 1)}
G(i)
= {2, i + 1}
G(first)
= {fun l match l . . .}
= {i :: from (i + 1)}
G(l)
G(x)
= {2, i + 1}
G(xs)
= {i :: from (i + 1)}
G(first (from 2)) = {2, i + 1}
We conclude that the evaluation of the expressions from 2 and from (i + 1) never
delivers a finite value. On the other hand, the variable i will potentially be bound to
expressions with values 2, 2 + 1, 2 + 1 + 1, . . .. According to the analysis, the main
expression evaluates to one of the values 2, 2 + 1, 2 + 1 + 1, . . ..
154
The sets G(e) can be computed by fixed-point iteration. A more clever implementation does not calculate with sets of expressions, but propagates expressions v V
individually. When v is added to a set G(e), the applicability conditions for further
rules might be satisfied, which may add more expressions v to sets G(e ). This is
the idea of the algorithm of Heintze (1994).
Correctness of this analysis can be shown using an operational semantics for programs with delayed evaluation. We do not present this proof here, but like to mention
that the derivation rules for the relation ev are quite analogous to the corresponding
rules of the operational semantics, with the following notable exceptions:
Operators 1 , 2 on base types are not further evaluated;
At case distinctions depending on basic values, all possibilities are explored nondeterministically;
In case distinctions, the order of the patterns is not taken into account;
The computation of the return value of a function is decoupled from the determination of the potential actual parameters of the function.
The analysis described is also correct for programs of a programming language with
eager expression evaluation. For these, the precision of the analysis can be increased
by requiring additional constraints for rules applications:
The set [[e2 ]]G should not be empty at function applications with argument e2 ;
At let- and letrec-expressions, the sets [[ei ]]G for the right sides ei of locally
introduced variables should not be empty;
At conditional expressions, the sets [[e0 ]]G for the condition e0 should not be empty;
Analogously, at case distinctions match e0 . . ., the set [[e0 ]]G should not be empty.
In the rules for patterns c y1 . . . yk , the sets [[ej ]]G for j = 1, . . . , k for the value
155
156
mediate results, which actually could be avoided. Function length can directly be
implemented by:
let length = fold_left (fun a fun y a + 1) 0
This implementation avoids the creation of the auxiliary list, which contains one 1
for each element of the input.
The following rules allow for the elimination of some apparently superfluous auxiliary data structures.
comp (map f) (map g)
= map (comp f g)
comp (fold_left f a) (map g) = fold_left (fun a comp (f a) g) a
comp (filter p1 ) (filter p2 )
= filter (fun x if p2 x then p1 x
else false)
comp (fold_left f a) (filter p) = fold_left (fun a fun x if p x then f a x
else a) a
The evaluation of the left sides always needs an auxiliary data structure, while the
evaluation of the right sides does not. Applying such rules for eliminating intermediate data structures is called deforestation. Deforestation allows to optimize the
function length of Example 3.6.1. However, left sides and right sides are now no
longer equivalent under all circumstances. In fact, these rules may only be applied
if the functions f, g, p1 , p2 that occur have no side effects. Another problem of this
optimization consists in recognizing when it can be applied. Programmers often do
not use explicit function composition to sequentially apply functions. They instead
use directly nested function applications. An example is the definition of function
der in Example 3.6.1. For this case, the transformation rules should be written as:
map f (map g l)
= map (fun z f (g z)) l
fold_left f a (map g l) = fold_left (fun a fun z f a (g z)) a l
= filter (fun x if p2 x then p1 x
filter p1 (filter p2 l)
else false) l
fold_left f a (filter p l) = fold_left (fun a fun x if p x then f a x
else a) a l
Example 3.6.2 The application of these rules to the definition of function der of
Example 3.6.1 leads to:
157
158
map f (tabulate n g)
= tabulate n (comp f g)
= tabulate n (fun j f (g j))
fold_left f a (tabulate n g) = loop n (fun a comp (f a) g) a
= loop n (fun a fun j (f a (g j)) a
Here we have:
let loop
= fun n fun f fun a
let rec doit = fun a fun j if j n then a
else doit (f a j) ( j + 1)
in doit a 0
The tail-recursive function loop corresponds to a for loop: The local data are collected
in the accumulating parameter a while the functional parameter f determines how
the new value for a after the jth iteration is computed from the old values of a and
j. The function loop computes its result without a list as auxiliary data structure.
The applicability of the rules essentially depends on whether composition of the
functions fold_left f a, map f, filter p are recognized. This structure, however, may
occur rather indirectly in a given program. Subexpressions may be contained in let
definitions or may be passed as parameters to the appropriate position. Application
of the let optimizations of Sect. 3.2 may help here, or in more complex situations the
value analysis of Sect. 3.5.
The principle of deforestation can be generalized in different directions:
Other functions on lists can be considered besides the considered functions, like
the function rev, which reverses the order of the elements of a list, the tailrecursive version rev_map of the function map, and the function fold_right (see
Exercise 10).
The suppression of intermediate data structures is also possible for index-dependent
versions of the functions map and fold_left (see Exercise 11).
Let l denote the list [x0 ; . . . ; xn1 ] of type b list. The index-dependent version
of map receives as argument a function f of type int b c and returns for
l the list:
[f 0 x0 ; . . . ; f (n 1) xn1 ]
In analogy, the index-dependent version of the function fold_left receives as argument a function f of type int a b a, an initial value a of type a and
computes the value
f (n 1) (. . . f 1 (f 0 a x0 ) x1 . . .) xn1
The functions map and fold_left can, in full generality, be defined for user-defined
functional data types, although this is not too frequently done. At least in principle, the same optimizations can be applied as we presented them for lists (see
Exercise 12).
159
160
that forces the evaluation of the expression e1 whenever the value of e0 is needed.
The goal of the optimization is to replace as many let-expressions as possible by let#expressions without changing the termination properties of the program. Strictness
analysis determines the necessary information about the termination properties of
expressions. A k-place function f is called strict in its jth argument, 1 j k, if the
evaluation of the expression f e1 . . . ek does not terminate whenever the evaluation of
e j does not terminate. The evaluation of the jth argument e j can be forced without
changing the termination behavior if the function is strict in its jth argument. The
compiler may then replace f e1 . . . ek by
let# x = e j in f e1 . . . e j1 x e j+1 . . . ek
Analogously, the compiler can replace a let-expression let x = e1 in e0 by the
expression
let# x = e1 in e0
if the evaluation of e0 does not terminate whenever the computation of e1 does not
terminate.
The simplest form of a strictness analysis only distinguishes whether the evaluation of an expression does definitely not terminate or maybe terminates and delivers
a value. Let 2 be the finite lattice consisting of the two values 0 and 1, where 0 < 1.
The value 0 is associated with an expression whose evaluation does definitely not
terminate. The value 1 denotes possible termination. A k-place function f is described
by an abstract k-place function:
[[f]] : 2 . . . 2 2
The fact [[f]] 1 . . . 1 0 1 . . . 1 = 0 (0 in the jth argument) allows us to derive that
an application of function f definitely does not terminate if the evaluation of the jth
argument does not terminate. The function f, therefore, is strict in its jth argument.
We construct a system of equations to determine abstract descriptions f for all
functions f of the program. This needs the abstract evaluation of expressions as an
auxiliary function. This abstract evaluation is defined with respect to a value binding
for the free variables of base types and a mapping of functions to their actual
abstract descriptions:
=1
[[b]]
=x
[[x]]
[[1 e]]
= [[e]]
= [[e1 ]] [[e2 ]]
[[e1 2 e2 ]]
[[if e0 then e1 else e2 ]] = [[e0 ]] ([[e1 ]] [[e2 ]] )
= (f) ([[e1 ]] ) . . . ([[ek ]] )
[[f e1 . . . ek ]]
= [[e]] ( {x1
[[e1 ]] })
[[let x1 = e1 in e]]
= ([[e1 ]] ) ([[e]] ( {x1
1}) )
[[let# x1 = e1 in e]]
161
The abstract evaluation function [[.]] interprets constants as the value 1. Variables
are looked up in . Unary operators 1 are approximated by the identity function
since the evaluation of an application does not terminate whenever the evaluation of
the argument does not terminate. Binary operators are, analogously, interpreted as
conjunction. The abstract evaluation of an if-expression is given by b0 (b1 b2 ),
where b0 represents the abstract value of the condition and b1 , b2 represent the
abstract values for the two alternatives. The intuition behind this definition is that
when evaluating a conditional expression the condition needs to be evaluated in any
case while only one of the two alternatives must be evaluated. For a function application, the actual abstract value of the function is looked up in the function binding is
and applied to the values determined recursively for the argument expressions. For a
let-defined variable x in an expression e0 , first the abstract value for x is determined
and then the value of the main expression e0 with respect to this value. If the variable
x is let#-defined, it must be ensured that the overall expression obtains the abstract
value 0 when the value obtained for x is 0.
Example 3.7.3 Consider the expression e, given by
if x 0 then a
else fac2 (x 1) (a x)
b1 , a
b2 }. Mapping
For values b1 , b2 2, let be the variable binding = {x
associates the function fac2 with the abstract function fun x fun a x a.
The abstract evaluation of e produces the value:
[[e]] = (b1 1) (b2 ( fac2) (b1 1) (b2 b1 ))
= b1 (b2 (b1 b2 ))
= b1 b2
The abstract expression evaluation constructs for each function f = fun x1
. . . fun xk e defined in the program the equations
(f) b1 . . . bk = [[ei ]] {x j
b j | j = 1, . . . , k}
for all b1 , . . . , bk 2. The right side depends monotononically on the abstract values
(f). Therefore, this system of equations possesses a least solution, denoted by [[f]] .
Example 3.7.4 For the function fac2 of Example 3.7.2 one obtains the equations
[[fac2]] b1 b2 = b1 (b2 [[fac2]] b1 (b1 b2 ))
Fixed-point iteration successively delivers for [[fac2]] the abstract functions:
162
0
1
2
fun x fun a 0
fun x fun a x a
fun x fun a x a
Note that the occurring abstract functions have been represented by boolean expressions instead of by their value tables. We conclude that function fac2 is strict in both
its arguments. The definition of fac2 can thus be transformed into
let rec fac2 = fun x fun a if x 0 then a
else
let# x = x 1
in let# a = x a
in
fac2 x a
The analysis produces the precise results for this example. Correctness of the analysis follows since the abstract expression evaluation is an abstraction of the concrete
expression evaluation as provided by the denotational semantics of our functional
language. The denotational semantics uses a partial order for the integer numbers
that consists of the set of numbers Z together with a special value that represents a nonterminating evaluation where the order relation is given by z for
all z Z. The abstract denotational semantics which we have constructed, however, interprets basic values and operators over the lattice 2. Our description relation
between concrete and abstract values is
0 and z 1 for z Z
The proof that the given analysis always delivers correct values is by induction over
the fixed-point iteration.
In the following, the analysis is extended beyond values of base types to structured
data. So far, the only distinction made is whether a functions argument is totally
needed or not needed at all for computing the result. With structured data, functions
may access their arguments to different depths.
Example 3.7.5 The function
let hd = fun l match l with h :: t h
only visits the topmost list constructor of its argument and returns its first element.
The function length of Example 3.6.1, in contrast, needs all list constructors and the
empty list at the end of the argument list to compute its result.
We now consider programs that, besides basic values, manipulate lists and tuples.
Accordingly, we extend the syntax of expressions by admitting the corresponding
constructors for structured data.
163
164
for values b1 , b2 2. We conclude that the root constructor of the first argument is
definitely needed for the computation of the root constructor of the result.
In many applications, not only is the root constructor of the result value needed,
but the whole value. A strictness analysis concentrating on that property tries to find
out which arguments of a function are totally needed if the result of the function is
totally needed. This generalization of strictness on basic values to structured values
is called total strictness. The abstract value 0 now describes all concrete values that
definitely contain a , while 1 still describes all values.
The total-strictness properties of functions are again described by boolean functions. The rules for the evaluation of strictness for expressions without structured
data are again extended to constructors for tuples, lists, and match-expressions:
[[match e0 with [ ] e1 | h :: t e2 ]] = let b = [[e0 ]]
in b [[e1 ]]
[[e2 ]] ( {h
b, t
1}
1, t
b}
[[e2 ]] ( {h
[[match e0 with (x1 , . . . , xk ) e1 ]]
= let b = [[e0 ]]
b, x2 , . . . , xk
1})
in [[e1 ]] ( {x1
. . . [[e1 ]] ( {x1 , . . . , xk1
1, xk
b})
=1
[[[ ]]]
[[e1 :: e2 ]]
= [[e1 ]] [[e2 ]]
= [[e1 ]] . . . [[ek ]]
[[(e1 , . . . , ek )]]
= [[e]] ( {x1
[[e1 ]] })
[[let# x1 = e1 in e]]
In the analysis of total strictness, constructor applications need to be treated differently from how they were treated in the analysis of root-strictness. The application
of a data constructor is now interpreted as the conjunction of the abstract values
obtained for the components. For the evaluation of let#-expressions, we recall that
the eagerly evaluated expression is only evaluated to its root constructor. The value
obtained this way might contain without causing nontermination of the evaluation
of the whole expression. The abstract evaluation of a let#-expression is, therefore, not
different from the abstract evaluation of a let-expression. The decomposition of the
value of an expression by applying a match-construct has also changed: The abstract
value of the expression e0 , to which the pattern is matched, is not 0 if it evaluates to
the empty list. This case thus corresponds to the conjunction of the abstract values
of e0 and the abstract value of the expression for the case of an empty list. Two cases
must be considered if the expression e0 evaluates to a composed list.
If the expression e0 produces the value 1 only this value 1 can be assumed for all
components of the list.
If the abstract evaluation of e0 returns 0, either the first element or the rest of the
list must contain . So, either the local variable h or the local variable t must obtain
the value 0. Let b be the value of the expression e0 . These two cases can be compactly
combined by disjunction of the results obtained through abstract evaluation of the
expression for composed lists, where for the newly introduced local variables h, t
165
166
the value 1 (dont know) in the analysis of this local function. This may produce
imprecise information, but at least gives correct results.
Further, we constrained ourselves to k-place functions without functional arguments and results, and without partial applications. This is because the complete
lattice of the k-place monotonic abstract functions 2 . . . 2 possesses properly
ascending chains whose length is exponential in k, that is, the number of arguments.
This is still acceptable since the number of arguments is often not very large. The
number of elements of this lattice, however, is even doubly exponential in k. And
this number matters if higher order functions are to be analyzed. One way out of this
dilemma consists in abstracting the abstract-function domains radically by smaller
complete lattices. For example, one could use the boolean lattice 2 for function
domains: 0 represents the constant 0-function. This strong abstraction will not lead
to precise strictness information for programs that systematically employ higherorder functions. Some of the higher-order functions, however, may be removed by
function specialization as shown in Sect. 3.4 and thereby improve the chances for
strictness analysis.
Strictness analysis, as we have considered it, is only applicable to monomorphically typed functions and monomorphic instances of polymorphically typed functions.
In general, the programmer might have a hard time finding out when the compiler is able to determine and exploit strictness information, and when it fails. The
programming language Haskell therefore provides annotations, which allow the
programmer to force the evaluation of expressions whenever he considers it important
for efficiency reasons.
3.8 Exercises
1. let-Optimization
Consider the following equation:
(fun y let x = e in e1 ) = (let x = e in fun y e1 )
if y does not occur free in e.
(a) Give conditions under which the expressions on both sides are semantically
equivalent.
(b) Give conditions under which the application of this equation from left to right
may contribute to an increase in efficiency of evaluation.
2. letrec-Optimization
This exercise tries to extend the optimizations of let-expressions to optimizations
of letrec-expressions.
(a) Give rules how let-definitions can be moved out of letrec-expressions.
3.8 Exercises
167
(b) Give constraints under which your transformations are semantics preserving.
(c) Give constraints under which your transformations lead to an improvement
in efficiency.
(d) Test your optimizations with some example programs.
3. let-Optimization of if-expressions
What do you think of the following rules?
(if let x = e in e0 then e1 else e2 ) = (let x = e in if e0 then e1 else e2 )
(if e0 then let x = e in e1 else e2 ) = (let x = e in if e0 then e1 else e2 )
where x does not occur free in the expressions e0 , e1 , and e2 .
4. Regular tree grammar
A regular tree grammar is a tuple G = (N , T, P), where N is a finite set of
nonterminal symbols, T is a finite set of terminal constructors, and P is a set
of rules of the form At, and where t is a term that is built from nonterminal
symbols in N using constructors in T . The language LG (A) of the regular tree
grammar G for a nonterminal A is the set of all terminal expressions t derivable
from nonterminal A using rules from P. An expression is called terminal if no
nonterminal occurs in it.
Give regular tree grammars for the following set of trees:
(a) all lists (inner nodes : ::) with an even number of elements from {0, 1, 2};
(b) all lists with elements from {0,1,2} such that the sum of the elements is
even;
(c) all terms with inner nodes :: and leaves {0,1,2} or [ ] that are of type list list
int.
5. Tree grammar (cont.)
Let G be a regular tree grammar of size n and A a nonterminal symbol of G.
Show:
(a) LG (A)
= if and only if t LG (A) for a t of depth n;
(b) LG (A) is infinite if and only if t LG (A) for a t of depth d where
n d < 2n.
(In particular, define the size of a grammar in such a way that these claims
hold.)
6. Value analysis: case distinction
Modify the algorithm for the value analysis in such a way that it respects the
given order of the patterns in a case distinction.
7. Value analysis: exceptions
Consider the functional core language, extended by the constructs:
e ::= . . . | raise e | (try e with p1 e1 | . . . | pk ek )
168
3.8 Exercises
169
170
3.9 Literature
The calculus with -reduction and -conversion is the theoretical basis for functional programming languages. It is based on work about the foundations of mathematics by Church and Kleene from the 1930s. The book by Barendregt (1984) is
still the standard textbook presenting important properties of the -calculus.
Jones and Santos (1998) gives a survey of optimizations implemented in the
Haskell compiler. This description also treats optimizations of nested
let-expressions (Jones and Santos 1996).
fold/unfold-transformations are first treated in Burstall and Darlington (1977).
Inlining and function specialization are simple forms of partial evaluation of programs (Secher and Srensen 1999). Our value analysis follows the approach taken
by Heintze (1994). A type-based analysis of side effects is proposed by Amtoft et al.
(1997).
The idea to systematically suppress intermediate data structures was proposed by
Wadler (1990). An extension to programs with higher-order functions is described
in Seidl and Srensen (1998). The particularly simple variant for programs with
lists presented here was introduced by Gill et al. (1993). Generalizations to arbitrary
algebraic data structures are studied by Takano and Meijer (1995).
The idea to use strictness analysis to support a conversion from CBN to CBV
originates with Mycroft (1980). A generalization to monomorphic programs with
higher-order functions is presented by Burn et al. (1986). The method for the analysis
of total strictness for programs with structured data described here is a simplification
of the method of Sekar et al. (1990).
Not treated in this chapter are optimizations that are based on the representation
of functional programs in continuation-passing style. An extensive presentation of
this technique is given by Appel (2007).
References
P. Anderson, D. Binkley, G. Rosay, T. Teitelbaum, Flow insensitive points-to sets. Inf. Softw.
Technol. 44(13), 743754 (2002)
W.A. Appel, M. Ginsburg, Modern Compiler Implementation in C (Cambridge University Press,
Cambridge, 2004)
S. Abramsky, C. Hankin (Hrsg.), Abstract Interpretation of Declarative Languages (Ellis
Horwood, Chichester, 1987)
A.V. Aho, M.S. Lam, R. Sethi, J.D. Ullman, Compilers: Principles, Techniques, & Tools, 2nd
revised edn. (Addison-Wesley, New York, 2007)
T. Amtoft, F. Nielson, H.R. Nielson, Type and behaviour reconstruction for higher-order
concurrent programs. J. Funct. Program. 7(3), 321347 (1997)
W.A. Appel, Compiling with Continuations (Cambridge University Press, Cambridge, 2007)
D.F. Bacon, Fast and effective optimization of statically typed object-oriented languages. Ph.D.
thesis, Berkeley, 1997
H.P. Barendregt, The Lambda Calculus: Its Syntax and Semantics, Volume 103 of Studies in
Logic and the Foundations of Mathematics, revised edition (North Holland, Amsterdam,
1984)
R.M. Burstall, J. Darlington, A transformation system for developing recursive programs. J. ACM
24(1), 4467 (1977)
G.L. Burn, C. Hankin, S. Abramsky, Strictness analysis for higher-order functions. Sci. Comput.
Program. 7(3), 249278 (1986)
P. Cousot, R. Cousot, Static determination of dynamic properties of programs, in 2nd
International Symposium on Programming, pp. 106130. Dunod, Paris, France, 1976
P. Cousot, R. Cousot, Abstract interpretation: a unified lattice model for static analysis of
programs by construction or approximation of fixpoints, in 4th ACM Symposium on Principles
of Programming Languages (POPL), pp. 238252, 1977a
P. Cousot, R. Cousot, Static determination of dynamic properties of recursive procedures, ed. by
E.J. Neuhold (Hrsg.), in IFIP Conference on Formal Description of Programming Concepts,
pp. 237277. (North Holland, Amsterdam, 1977b)
P. Cousot, R. Cousot, Systematic design of program transformation frameworks by abstract
interpretation, in 29th ACM Symposium on Principles of Programming Languages (POPL),
pp. 178190, 2002
J.-D. Choi, M. Gupta, M. Serrano, V.C. Sreedhar, S. Midkiff, Escape analysis for Java. SIGPLAN
Not. 34(10), 119 (1999)
P. Cousot, N. Halbwachs, Automatic discovery of linear restraints among variables of a program,
in 5th ACM Symposium on Principles of Programming Languages (POPL), pp. 8497, 1978
171
172
References
T.H. Cormen, C.E. Leiserson, R.L. Rivest, C. Stein, Introduction to Algorithms, 3rd edn. (MIT
Press, Cambridge, 2009)
K.D. Cooper, L. Torczon, Engineering a Compiler (Morgan Kaufmann, Massachusetts, 2004)
M. Fhndrich, J. Rehof, M. Das, Scalable context-sensitive flow analysis using instantiation
constraints. SIGPLAN Not. 35(5), 253263 (2000)
C. Fecht, H. Seidl, A faster solver for general systems of equations. Sci. Comput. Program. (SCP)
35(2), 137161 (1999)
A.J. Gill, J. Launchbury, S.L.P. Jones, A short cut to deforestation, in Functional Programming
and Computer Architecture (FPCA), pp. 223232, 1993
R. Giegerich, U. Mncke, R. Wilhelm, Invariance of approximate semantics with respect to
program transformations. In GI Jahrestagung, pp. 110, 1981
P. Granger, Static analysis of linear congruence equalities among variables of a program, in
International Joint Conference on Theory and Practice of Software Development (TAPSOFT),
pp. 169192. LNCS 493 (Springer, Heidelberg, 1991)
T. Gawlitza, H. Seidl, Precise fixpoint computation through strategy iteration, in European
Symposium on Programming (ESOP), pp. 300315. LNCS 4421 (Springer, Heidelberg, 2007)
M.S. Hecht, Flow Analysis of Computer Programs (North Holland, Amsterdam, 1977)
N. Heintze, Set-based analysis of ML programs. SIGPLAN Lisp Pointers VII(3), 306317 (1994)
M. Hofmann, A. Karbyshev, H. Seidl, in Verifying a Local Generic Solver in Coq, ed. by R.
Cousot, M. Martel. Static Analysis, Volume 6337 of Lecture Notes in Computer Science
(Springer, Heidelberg, 2010), pp. 340355
M. Hofmann, A. Karbyshev, H. Seidl, What is a Pure Functional? ed. by S. Abramsky,
C. Gavoille, C. Kirchner, F. Meyer auf der Heide, P.G. Spirakis (Hrsg.), ICALP (2), Volume
6199 of Lecture Notes in Computer Science (Springer, Heidelberg, 2010), pp. 199210
S.L.P. Jones, W. Partain, A. Santos, Let-floating: moving bindings to give faster programs, in
International Conferece on Functional Programming (ICFP), pp. 112, 1996
L. Simon, P. Jones, A.L.M. Santos, A Transformation-based optimiser for Haskell. Sci. Comput.
Program. 32(13), 347 (1998)
M. Karr, Affine relationships among variables of a program. Acta Inf. 6, 133151 (1976)
G.A. Kildall, A unified approach to global program optimization, in ACM Symposium on
Principles of Programming Languages (POPL), pp. 194206, 1973
J. Knoop, Optimal Interprocedural Program Optimization, A New Framework and Its
Application, LNCS 1428. (Springer, Berlin, 1998)
J. Knoop, O. Rthing, B. Steffen, Optimal code motion: theory and practice. ACM Trans.
Program. Lang. Syst. 16(4), 11171155 (1994)
J. Knoop, O. Rthing, B. Steffen, Partial dead code elimination, in ACM Conference on
Programming Languages Design and Implementation (PLDI), pp. 147158, 1994
J. Knoop, B. Steffen, The interprocedural coincidence theorem, in 4th International Conference
on Compiler Construction (CC), pp. 125140. LNCS 541 (Springer, Heidelberg, 1992)
S. Kundu, Z. Tatlock, S. Lerner, Proving optimizations correct using parameterized program
equivalence, in ACM SIGPLAN Conference on Programming Language Design and
Implementation (PLDI), 2009
J.B. Kam, J.D. Ullman, Global data flow analysis and iterative algorithms. J. ACM 23(1),
158171 (1976)
J.B. Kam, J.D. Ullman, Monotone data flow analysis frameworks. Acta Inf. 7, 305317 (1977)
X. Leroy, Formal verification of a realistic compiler. Commun. ACM 52(7), 107115 (2009)
S. Lerner, T.D. Millstein, C. Chambers, Automatically proving the correctness of compiler
optimizations, in ACM SIGPLAN Conference on Programming Language Design and
Implementatio (PLDI), pp. 220231, 2003
S. Lerner, T. Millstein, E. Rice, C. Chambers, Automated soundness proofs for dataflow analyses
and transformations via local rules, in 32nd ACM Symp. on Principles of Programming
Languages (POPL), pp. 364377, 2005
References
173
D. Liang, M. Pennings, M.J. Harrold, Extending and evaluating flow-insensitive and contextinsensitive points-to analyses for Java, in ACM SIGPLAN-SIGSOFT Workshop on Program
Analysis For Software Tools and Engineering (PASTE), pp. 7379, 2001
F. Martin, M. Alt, R. Wilhelm, C. Ferdinand, Analysis of loops, in 7th International Conference
on Compiler Construction (CC), pp. 8094. LNCS 1383, Springer, 1998.
S.S. Muchnick, N.D. Jones (Hrsg.), Program Flow Analysis: Theory and Application (Prentice
Hall, Englewood Cliffs, 1981)
M. Mller-Olm, H. Seidl, Precise interprocedural analysis through linear algebra, in 31st ACM
Symposium on Principles of Programming Languages (POPL), pp. 330341, 2004.
M. Mller-Olm, H. Seidl, A generic framework for interprocedural analysis of numerical
properties, in Static Analysis, 12th International Symposium (SAS), pp. 235250. LNCS 3672
(Springer, Heidelberg, 2005)
M. Mller-Olm, H. Seidl, Analysis of modular arithmetic. ACM Trans. Program. Lang. Syst.
29(5), 2007
S.S. Muchnick, Advanced Compiler Design and Implementation (Morgan Kaufmann, Massachusetts, 1997)
A. Mycroft, The theory and practice of transforming call-by-need into call-by-value, in Symposium on Programming: Fourth Colloque International sur la Programmation, pp. 269281.
LNCS 83 (Springer, Heidelbeg, 1980)
F. Nielson, H.R. Nielson, C. Hankin, Principles of Program Analysis (Springer, Heidelberg,
1999)
R. Paige, Symbolic finite differencingPart I, in 3rd European Symposium on Programming
(ESOP), pp. 3656. LNCS 432 (Springer, Heidelberg, 1990)
R. Paige, J.T. Schwartz, Reduction in strength of high level operations, in 4th ACM Symposium
on Principles of Programming Languages (POPL), pp. 5871, 1977
G. Ramalingam, On loops, dominators, and dominance frontiers. ACM Trans. Program. Lang.
Syst. (TOPLAS) 24(5), 455490 (2002)
V. Sundaresan, L. Hendren, C. Razafimahefa, R. Valle-Rai, P. Lam, E. Gagnon, C. Godin,
Practical virtual method call resolution for Java, in 15th ACM SIGPLAN Conference on
Object-oriented Programming, Systems, Languages, and Applications (OOPSLA), pp. 264
280, 2000
A. Simon, Value-Range Analysis of C Programs: Towards Proving the Absence of Buffer
Overflow Vulnerabilities (Springer, Heidelberg, 2008)
J. Sheldon, W. Lee, B. Greenwald, S.P. Amarasinghe, Strength reduction of integer division and
modulo operations, in Languages and Compilers for Parallel Computing, 14th International
Workshop (LCPC). Revised Papers, pp. 254273. LNCS 2624 (Springer, Heidelberg, 2003)
M. Sharir, A. Pnueli, Two approaches to interprocedural data flow analysis, ed. by S.S.
Muchnick, N.D. Jones (Hrsg.). Program Flow Analysis: Theory and Application, pp. 189234
(Prentice Hall, Englewood Cliffs, 1981)
R.C. Sekar, S. Pawagi, I.V. Ramakrishnan, Small domains spell fast strictness analysis, in ACM
Symposium on Principles of Programming Languages (POPL), pp. 169183, 1990
M. Sagiv, T.W. Reps, R. Wilhelm, Parametric shape analysis via 3-valued logic, in 26th ACM
Symposium on Principles of Programming Languages (POPL), pp. 105118, 1999
M.Sagiv, T.W. Reps, R. Wilhelm, Parametric shape analysis via 3-valued logic. ACM Trans.
Program. Lang. Syst. (TOPLAS) 24(3), 217298 (2002)
H. Seidl, M.H. Srensen, Constraints to stop deforestation. Sci. Comput. Program. 32(13),
73107 (1998)
J.P. Secher, M.H. Srensen, On perfect supercompilation, in 3rd Int. Andrei Ershov Memorial
Conference: Perspectives of System Informatics (PSI), pp. 113127. LNCS 1755 (Springer,
Heidelberg, 1999)
Y.N. Srikant, P. Shankar (Hrsg.), The Compiler Design Handbook: Optimizations and Machine
Code Generation (CRC Press, Boca Raton, 2003)
174
References
B. Steensgaard, Points-to analysis in almost linear time, in 23rd ACM Symposium on Principles of
Programming Languages (POPL), pp. 3241, 1996
J.-B. Tristan, X. Leroy, Verified validation of lazy code motion, in ACM SIGPLAN Conference
on Programming Language Design and Implementation (PLDI), pp. 316326, 2009
A. Takano, E. Meijer, Shortcut deforestation in calculational form, in SIGPLAN-SIGARCHWG2.8 Conference on Functional Programming Languages and Computer Architecture
(FPCA), pp. 306313, 1995
P. Wadler, Deforestation: transforming programs to eliminate trees. Theor. Comput. Sci. 73(2),
231248 (1990)
Index
A
Abstract interpretation, 47
Algorithm, 23, 83, 87
recursive fixed-point, 88, 89, 114
round-robin, 23, 25, 27
worklist
Alias, 67
may, 67
must, 67
Alias analysis, 66
a conversion, 4
Analysis, 72
distributive framework, 30
flow-insensitive, 75
interprocedural, 124
monotonic framework, 26
points-to, 72
Antisymmetry, 17
Application
partial, 1
Approach
call string, 135
functional, 133
Array-bounds check, 4
Assignment
available, 138
between variables, 40
dead, 32
delayable, 103
partially dead, 102
partially redundant, 108
very busy, 132
B
Back edge, 99
Backward analysis, 34
b reduction, 143
Bottom, 18
Bound
greatest lower, 16, 18
least upper, 16, 17
upper, 17
C
C, 141, 144
Call, 117, 122, 135
last, 123
Call graph, 122
Call stack, 117, 124
Call string, 135
Chain
ascending, 166
descending, 65
stable ascending, 88
Closure, 159
Code
loop invariant, 97
Compilation
separate, 124
Computation
reaching, 120
redundant, 8
same level, 119
step, 8
Concretization, 47
Constant folding, 42
Constant propagation, 44, 42
interprocedural, 133
Control-flow graph, 8
interprocedural, 116
Copy propagation
interprocedural, 40, 124, 126
175
176
C (cont.)
Correctness
of constant propagation, 47
of interprocedural analysis, 127
of interval analysis, 59
of points-to analysis, 74
of the transformation PRE, 97
of transformation RE, 42
of transformation DE, 37
D
Data structure
intermediate, 155
union-find-, 79
Data-flow analysis, vi, 35
Dead-code elimination, 35
Deforestation, 15
Dependence, 68
Dependence analysis, 68
Description relation, 47
.NET instructions, 1
E
Edge effect
abstract, 11
concrete, 9
Element
atomic, 29
greatest, 18
least, 18
Equivalence class, 77
Evaluation
eager, 5, 19
lazy, 141, 144, 159
multiple, 7
partial, 44
Expression
abstract evaluation of, 45
available, 11
concrete evaluation of, 10
F
Feedback vertex set, 62
Fixed point
greatest, 23
least, 22
post-, 22
Fixed-point iteration
accumulating, 24
local, 89
naive, 23
Index
recursive, 87
round-robin, 23
worklist, 83
Fortran, 3
Forward analysis, 34
F#, 1
Function
distributive, 28
folding, 8
inlining, 6
monotonic, 20
specialization, 7
strict, 28
totally distributive, 28
Functional abstraction, 2
H
Haskell, 141, 144, 147
I
Inequalities, 15
system of
Inlining
of functions, 6
procedure, 121
Interval analysis, 53
Interval arithmetic, 56
J
Java, 4
L
k calculus, 143
Lattice
atomic, 29
complete, 16, 17
flat, 18
height of, 25
powerset-, 18
Lisp, 7
List constructor, 8
Loop inversion, 98
M
Memoization, 7
Memory
dynamically allocated, 67
Memory cell
uninitialized, 74
Index
N
Narrowing, 63
O
Operator, 65
Ocaml, 141, 145
Order
dual, 23
partial, 16
P
Partial order, 16
Partition
refinement of, 77
Path
infeasible, 14
Pattern, 10
Pattern matching, 141
Pointer, 67
Pointer arithmetic, 67
Polymorphism, 141
Predominator, 98
Program
well-typed, 3
Program optimization semantics, 3
Program point, 8
Program state, 8
R
Recursion
tail, 123
Redundancy
partial, 89
Redundancy elimination, 13
Reflexivity, 17
Register
virtual, 4
Register allocation, 4
Root-strictness, 23
Round-robin iteration
correctness, 25
S
Scala, 141
Semantics
denotational, 162
177
instrumented, 74
operational, 3, 154
small-step operational, 8
Side effects, 5
Solution, 149
merge-over-all-paths, 27
Stack frame, 117
Strictness
root, 23
total, 24
Substitution, 3
Supergraph
interprocedural, 135
System of inequalities
size of, 85
T
Termination, 145
Top, 18
Total strictness, 164
Transitivity, 17
Tree grammar
regular, 151
type inference, 141
type system, 67
V
Value analysis, 149
expression, 150
Variable
binding, 9, 10
abstract, 44
dead, 32
definition of, 32
global, 32
live, 33
order, 25
partially dead, 102
renaming of a, 144
true use, 37
truly live, 37
use of, 32
Verification, 4
W
Widening, 60
-operator, 61