Efficiently Computing Static Single Assignment Form and the Control Dependence Graph

About this paperTiếng Việt日本語beside Tiếng Việtbeside 日本語

This paper is under a licence that does not allow it to be reproduced, so what is here is its front matter and a short abstract. Read it at the publisher's page.

In optimizing compilers, data structure choices directly influence the power and efficiency of practical program optimization. A poor choice of data structure can inhibit optimization or slow compilation to the point that advanced optimization features become undesirable. Recently, static single assignment form and the control dependence graph have been proposed to represent data flow and control flow properties of programs. Each of these previously unrelated techniques lends efficiency and power to a useful class of program optimizations. Although both of these structures are attractive, the difficulty of their construction and their potential size have discouraged their use. We present new algorithms that efficiently compute these data structures for arbitrary control flow graphs. The algorithms use dominance frontiers, a new concept that may have other applications. We also give analytical and experimental evidence that all of these data structures are usually linear in the size of the original program. This paper thus presents strong evidence that these structures can be of practical use in optimization.

Categories and Subject Descriptors: D.3.3 [Programming Languages]: Language Constructs—control structures; data types and structures; procedures, functions and subroutines; D.3.4 [Programming Languages]: Processors—compilers; optimization; I.1.2 [Algebraic Manipulation]: Algorithms—analysis of algorithms; I.2.2 [Artificial Intelligence]: Automatic Programming—program transformation

A preliminary version of this paper, “An Efficient Method of Computing Static Single Assignment Form,” appeared in the Conference Record of the 16th ACM Symposium on Principles of Programming Languages (Jan. 1989). F. K. Zadeck’s work on this paper was partially supported by the IBM Corporation, the Office of Naval Research, and the Defense Advanced Research Projects Agency under contract N00014-83-K-0146 and ARPA order 6320, Amendment 1. Authors’ addresses: R. Cytron, J. Ferrante, and M. N. Wegman, Computer Sciences Department, IBM Research Division, P. O. Box 704, Yorktown Heights, NY 10598; B. K. Rosen, Mathematical Sciences Department, IBM Research Division, P. O. Box 218, Yorktown Heights, NY 10598; F. K. Zadeck, Computer Science Department, P.O. Box 1910, Brown University, Providence, RI 02912. Permission to copy without fee all or part of this material is granted provided that the copies are not made or distributed for direct commercial advantage, the ACM copyright notice and the title of the publication and its date appear, and notice is given that copying is by permission of the Association for Computing Machinery. To copy otherwise, or to republish, requires a fee and/or specific permission. © 1991 ACM 0164-0925/91/1000-0451 \$01.50

General Terms: Algorithms, Languages Additional Key Words and Phrases: Control dependence, control flow graph, def-use chain, dominator, optimizing compilers

1 INTRODUCTION

In optimizing compilers, data structure choices directly influence the power and efficiency of practical program optimization. A poor choice of data structure can inhibit optimization or slow compilation to the point where advanced optimization features become undesirable. Recently, static single assignment (SSA) form [5, 43] and the control dependence graph [24] have been proposed to represent data flow and control flow properties of programs. Each of these previously unrelated techniques lends efficiency and power to a useful class of program optimizations. Although both of these structures are attractive, the difficulty of their construction and their potential size have discouraged their use [4]. We present new algorithms that efficiently compute these data structures for arbitrary control flow graphs. The algorithms use dominance frontiers, a new concept that may have other applications. We also give analytical and experimental evidence that the sum of the sizes of all the dominance frontiers is usually linear in the size of the original program. This paper thus presents strong evidence that SSA form and control dependences can be of practical use in optimization.

Figure 1 illustrates the role of SSA form in a compiler. The intermediate code is put into SSA form, optimized in various ways, and then translated back out of SSA form. Optimizations that can benefit from using SSA form include code motion [22] and elimination of partial redundancies [43], as well as the constant propagation discussed later in this section.

Variants of SSA form have been used for detecting program equivalence [5, 52] and for increasing parallelism in imperative programs [21]. The representation of simple data flow information (def-use chains) may be made more compact through SSA form. If a variable has DD definitions and UU uses, then there can be D×UD \times U def-use chains. When similar information is encoded in SSA form, there can be at most EE def-use chains, where EE is the number of edges in the control flow graph [40]. Moreover, the def-use information encoded in SSA form can be updated easily when optimizations are applied. This is important for a constant propagation algorithm that deletes branches to code proven at compile time to be unexecutable [50]. Specifically, the def-use information is just a list, for each variable, of the places in the program text that use that variable. Every use of V is indeed a use of the value provided by the (unique) assignment to V.

To see the intuition behind SSA form, it is helpful to begin with straight-line code. Each assignment to a variable is given a unique name (shown as a subscript in Figure 2), and all of the uses reached by that assignment are renamed to match the assignment’s new name.

Most programs, however, have branch and join nodes. At the join nodes, we add a special form of assignment called a ϕ\phi-function. In Figure 3, the

Original Intermediate Code ↓ P0→P1→P2→…PnP_0 \rightarrow P_1 \rightarrow P_2 \rightarrow \ldots P_n Optimized Intermediate Code ↑

Fig. 1. Vertical arrows represent translation to/from static single assignment form. Horizontal arrows represent optimizations.

V←4←V+5V←6←V+7V \leftarrow 4 \\ \leftarrow V + 5 \\ V \leftarrow 6 \\ \leftarrow V + 7
V1←4←V1+5V2←6←V2+7V_1 \leftarrow 4 \\ \leftarrow V_1 + 5 \\ V_2 \leftarrow 6 \\ \leftarrow V_2 + 7
Figure 2
Straight-line code and its single assignment version.
            
              if P
then V ← 4
else V ← 6
            
          

/ Use V several times. /

            
              if P
then V₁ ← 4
else V₂ ← 6
            
          

V3←ϕ(V1,V2)V_3 \leftarrow \phi(V_1, V_2) / Use V_3 several times. /

Figure 3
if-then-else and its single assignment version.

operands to the ϕ\phi-function indicate which assignments to VV reach the join point. Subsequent uses of VV become uses of V3V_3. The old variable VV is thus replaced by new variables V1,V2,V3,…V_1, V_2, V_3, \ldots, and each use of ViV_i is reached by just one assignment to ViV_i. Indeed, there is only one assignment to ViV_i in the entire program. This simplifies the record keeping for several optimizations. An especially clear example is constant propagation based on SSA form [50]; the next subsection sketches this application.

1.1 Constant Propagation

Figure 4 is a more elaborate version of Figure 3. The else branch includes a test of QQ, and propagating the constant true to this use of QQ tells a compiler that the else branch never falls through to the join point. If QQ is the constant true, then all uses of VV after the join point are really uses of the constant 4. Such possibilities can be taken into account without SSA form, but the processing is either costly [48] or difficult to understand [49]. With SSA form, the algorithm is fast and simple.

Initially, the algorithm assumes that each edge is unexecutable (i.e., never followed at run time) and that each variable is constant with an as-yet unknown value (denoted TT). Worklists are initialized appropriately, and the assumptions are corrected until they stabilize. Suppose the algorithm finds that variable PP is not constant (denoted ⊥\perp) and, hence, that either branch of

            
              if P
then do V ← 4
    end
else do V ← 6
    if Q then return
end
            
          

... ← V + 5

            
              if P
then do V₁ ← 4
    end
else do V₂ ← 6
    if Q then return
end
V₃ ← φ(V₁, V₂)
... ← V₃ + 5
            
          

Fig 4. Example of constant propagation.

the outer conditional may be taken. The outedges of the test of P are marked executable, and the statements they reach are processed. When V₁ ← 4 is processed, the assumption about V₁ is changed from T to 4, and all uses of V₁ are notified that they are uses of 4. (Each use of V₁ is indeed a use of this value, thanks to the single assignment property.) In particular, the φ-function combines 4 with V₂. The second operand of the φ-function, however, is associated with the inedge of the join point that corresponds to falling through the else branch. So long as this edge is considered unexecutable, the algorithm uses T for the second operand, no matter what is currently assumed about V₂. Combining 4 with T yields 4, so uses of V₃ are still tentatively assumed to be uses of 4. Eventually, the assumption about Q may change from T to a known constant or to ⊥. A change to either false or ⊥ would lead to the discovery that the second inedge of the join point can be followed, and then the 6 at V₂ combines with the 4 at V₁ to yield ⊥ at V₃. A change to true would have no effect on the assumption at V₃. Traditional constant propagation algorithms, on the other hand, would see that assignments of two different constants to V seem to “reach” all uses after the join point. They would thus decide that V is not constant at these uses.

The algorithm just sketched is linear in the size of the SSA program it sees [50], but this size might be nonlinear in the size of the original program. In particular, it would be safe but inefficient to place a φ-function for every variable at every join point. This paper shows how to obtain SSA form efficiently. When φ-functions are placed carefully, nonlinear behavior is still possible but is unlikely in practice. The size of the SSA program is typically linear in the size of the original program; the time to do the work is essentially linear in the SSA size.

1.2 Where to Place φ-Functions

At first glance, careful placement might seem to require the enumeration of pairs of assignment statements for each variable. Checking whether there are two assignments to V that reach a common point might seem to be intrinsically nonlinear. In fact, however, it is enough to look at the dominance frontier of each node in the control flow graph. Leaving the technicalities to later sections, we sketch the method here.

Suppose that a variable V has just one assignment in the original program, so that any use of V will be either a use of the value V₀ at entry to the program or a use of the value V₁ from the most recent execution of the assignment to V. Let X be the basic block of code that assigns to V, so X will determine the value of V when control flows along any edge X → Y to a basic block Y. When entered along X → Y, the code in Y will see V₁ and be unaffected by V₀. If Y ≠ X, but all paths to Y must still go through X (in which case X is said to strictly dominate Y), then the code in Y will always see V₁. Indeed, any node strictly dominated by X will always see V₁, no matter how far from X it may be. Eventually, however, control may be able to reach a node Z not strictly dominated by X. Suppose Z is the first such node on a path, so that Z sees V₁ along one inedge but may see V₀ along another inedge. Then Z is said to be in the dominance frontier of X and is clearly in need of a φ-function for V. In general, no matter how many assignments to V may appear in the original program and no matter how complex the control flow may be, we can place φ-functions for V by finding the dominance frontier of every node that assigns to V, then the dominance frontier of every node where a φ-function has already been placed, and so on.

The same concept of dominance frontiers used for computing SSA form can also be used to compute control dependences [20, 24], which identify those conditions affecting statement execution. Informally, a statement is control dependent on a branch if one edge from the branch definitely causes that statement to execute while another edge can cause the statement to be skipped. Such information is vital for detection of parallelism [2], program optimization, and program analysis [28].

1.3 Outline of the Rest of the Paper

Section 2 reviews the representation of control flow by a directed graph. Section 3 explains SSA form and sketches how to construct it. This section also considers variants of SSA form as defined here. Our algorithm can be adjusted to deal with these variants. Section 4 reviews the dominator tree concept and formalizes dominance frontiers. Then we show how to compute SSA form (Section 5) and the control dependence graph (Section 6) efficiently. Section 7 explains how to translate out of SSA form. Section 8 shows that our algorithms are linear in the size of programs restricted to certain control structures. We also give evidence of general linear behavior by reporting on experiments with FORTRAN programs. Section 9 summarizes the algorithms and time bounds, compares our technique with other techniques, and presents some conclusions.

2 CONTROL FLOW GRAPHS

The statements of a program are organized into (not necessarily maximal) basic blocks, where program flow enters a basic block at its first statement and leaves the basic block at its last statement [1, 36]. Basic blocks are indicated by the column of numbers in parentheses in Figure 5. A control flow graph is a directed graph whose nodes are the basic blocks of a program and two additional nodes, Entry and Exit. There is an edge from Entry to

            
              I ← 1	(1)
J ← 1	(1)
K ← 1	(1)
L ← 1	(1)
repeat	(2)
    if (P)	(2)
        then do	(3)
            J ← I	(3)
            if (Q)	(3)
                then L ← 2	(4)
                else L ← 3	(5)
            K ← K + 1	(6)
        end	(6)
    else K ← K + 2	(7)
print(I,J,K,L)	(8)
repeat	(9)
    if (R)	(9)
        then L ← L + 4	(10)
until (S)	(11)
I ← I + 6	(12)
until (T)	(12)
            
          

Fig 5 Simple program and its control flow graph.

any basic block at which the program can be entered, and there is an edge to Exit from any basic block that can exit the program. For reasons related to the representation of control dependences and explained in Section 6, there is also an edge from Entry to Exit. The other edges of the graph represent transfers of control (jumps) between the basic blocks. We assume that each node is on a path from Entry and on a path to Exit. For each node X, a successor of X is any node Y with an edge X → Y in the graph, and Succ(X) is the set of all successors of X; similarly for predecessors. A node with more than one successor is a branch node; a node with more than one predecessor is a join node. Finally, each variable is considered to have an assignment in Entry to represent whatever value the variable may have when the program is entered. This assignment is treated just like the ones that appear explicitly in the code. Throughout this paper, CFG denotes the control flow graph of the program under discussion.

For any nonnegative integer J, a path of length J in CFG consists of a sequence of J + 1 nodes (denoted X₀, ..., X_J) and a sequence of J edges (denoted e₁, ..., e_J) such that e_j runs from X_{j-1} to X_j for all j with 1 ≤ j ≤ J. (We write e_j: X_{j-1} → X_j.) As is usual with sequences, one item (node or edge) may occur several times. The null path, with J = 0, is allowed. We write p:X0→∗XJp: X_0 \to^* X_J for an unrestricted path pp, but p:X0→+XJp: X_0 \to^+ X_J if pp is known to be nonnull.

Nonnull paths p:X0→+XJp: X_0 \to^+ X_J and q:Y0→+YKq: Y_0 \to^+ Y_K are said to converge at a node ZZ if

X0≠Y0;X_0 \neq Y_0;
XJ=Z=YK;X_J = Z = Y_K;
(XJ=Yk)⇒(j=J or k=K).(X_J = Y_k) \Rightarrow (j = J \text{ or } k = K).

Intuitively, the paths pp and qq start at different nodes and are almost node-disjoint, but they come together at the end. The use of or rather than and in (3) is deliberate. One of the paths may happen to be a cycle Z→+ZZ \to^+ Z, but we still need to consider the possibility of convergence.

3 STATIC SINGLE ASSIGNMENT FORM

This section initially assumes that programs have been expanded to an intermediate form in which each statement evaluates some expressions and uses the results either to determine a branch or to assign to some variables. (Other program constructs are considered in Section 3.1.) An assignment statement AA has the form LHS(A)←RHS(A)LHS(A) \leftarrow RHS(A), where the left-hand side LHS(A)LHS(A) is a tuple of distinct target variables ⟨U,V,…⟩\langle U, V, \ldots \rangle and the right-hand side RHS(A)RHS(A) is a tuple of expressions, with the same length as the LHSLHS tuple. Each target variable in LHS(A)LHS(A) is assigned the corresponding value from RHS(A)RHS(A). In the examples already discussed, all tuples are 1-tuples, and there is no need to distinguish between VV and ⟨V⟩\langle V \rangle. Section 3.1 sketches the use of longer tuples in expanding other program constructs (such as procedure calls) so as to make explicit the variables used and/or changed by the statements in the source program. Such explicitness is a prerequisite for most optimizations anyway. The only novelty in our use of tuples is the fact that they provide a simple and uniform way to fold the results of analysis into the intermediate text. Practical concerns about the size of the intermediate text are addressed in Section 3.1.

Translating a program into SSA form is a two-step process. In the first step, some trivial ϕ\phi-functions V←ϕ(V,V,…)V \leftarrow \phi(V, V, \ldots) are inserted at some of the join nodes in the program's control flow graph. In the second step, new variables ViV_i (for i=0,1,2,…i = 0, 1, 2, \ldots) are generated. Each mention of a variable VV in the program is replaced by a mention of one of the new variables ViV_i, where a mention may be in a branch expression or on either side of an assignment statement. Throughout this paper, an assignment statement may be either an ordinary assignment or a ϕ\phi-function. Figure 6 displays the result of translating a simple program to SSA form.

A ϕ\phi-function at entrance to a node XX has the form V←ϕ(R,S,…)V \leftarrow \phi(R, S, \ldots), where V,R,S,…V, R, S, \ldots are variables. The number of operands R,S,…R, S, \ldots is the number of control flow predecessors of XX. The predecessors of XX are listed in some arbitrary fixed order, and the jjth operand of ϕ\phi is associated with the jjth

            
              I ← 1
J ← 1
K ← 1
L ← 1
repeat
    I₁ ← 1
    J₁ ← 1
    K₁ ← 1
    L₁ ← 1
    repeat
        I₂ ← φ(I₃, I₁)
        J₂ ← φ(J₄, J₁)
        K₂ ← φ(K₅, K₁)
        L₂ ← φ(L₉, L₁)
        if (P)
            then do
                J ← I
                if (Q)
                    then L ← 2
                    else L ← 3
                K ← K + 1
            end
        else K ← K + 2
    print(I, J, K, L)
    repeat
        if (R)
            then L ← L + 4
        until (S)
        I ← I + 6
until (T)

I₁ ← 1
J₁ ← 1
K₁ ← 1
L₁ ← 1
repeat
    I₂ ← φ(I₃, I₁)
    J₂ ← φ(J₄, J₁)
    K₂ ← φ(K₅, K₁)
    L₂ ← φ(L₉, L₁)
    if (P)
        then do
            J₃ ← I₂
            if (Q)
                then L₃ ← 2
                else L₄ ← 3
            L₅ ← φ(L₃, L₄)
            K₃ ← K₂ + 1
        end
    else K₄ ← K₂ + 2
        J₄ ← φ(J₃, J₂)
        K₅ ← φ(K₃, K₄)
        L₆ ← φ(L₂, L₅)
    print(I₂, J₄, K₅, L₆)
    repeat
        L₇ ← φ(L₉, L₆)
        if (R)
            then L₈ ← L₇ + 4
            L₉ ← φ(L₈, L₇)
        until (S)
        I₃ ← I₂ + 6
    until (T)
            
          

Fig. 6 Simple program and its SSA form.

predecessor. If control reaches X from its jth predecessor, then the run-time support¹ remembers j while executing the φ-functions in X. The value of φ(R, S, ...) is just the value of the jth operand. Each execution of a φ-function uses only one of the operands, but which one depends on the flow of control just before entering X. Any φ-functions in X are executed before the ordinary statements in X. Some variants of φ-functions as defined here are useful for special purposes. For example, each φ-function can be tagged

¹When SSA form is used only as an intermediate form (Figure 1), there is no need actually to provide this support. For us, the semantics of φ are only important when assessing the correctness of intermediate steps in a sequence of program transformations beginning and ending with code that has no φ-functions. Others [6, 11, 52] have found it useful to give φ another parameter that incidentally encodes j, under various restrictions on the control flow.

with the node XX where it appears [5]. When the control flow of a language is suitably restricted, each ϕ\phi-function can be tagged with information about conditionals or loops [5, 6, 11, 52]. The algorithms in this paper apply to such variants as well.

SSA form may be considered as a property of a single program or as a relation between two programs. A single program is defined to be in SSA form if each variable is a target of exactly one assignment statement in the program text. Translation to SSA form replaces the original program by a new program with the same control flow graph. For every original variable VV, the following conditions are required of the new program:

(1) If two nonnull paths X→+ZX \xrightarrow{+} Z and Y→+ZY \xrightarrow{+} Z converge at a node ZZ, and nodes XX and YY contain assignments to VV (in the original program), then a trivial ϕ\phi-function V←ϕ(V,…,V)V \leftarrow \phi(V, \ldots, V) has been inserted at ZZ (in the new program). (2) Each mention of VV in the original program or in an inserted ϕ\phi-function has been replaced by a mention of a new variable ViV_i, leaving the new program in SSA form. (3) Along any control flow path, consider any use of a variable VV (in the original program) and the corresponding use of ViV_i (in the new program). Then VV and ViV_i have the same value.

Translation to minimal SSA form is translation to SSA form with the proviso that the number of ϕ\phi-functions inserted is as small as possible, subject to Condition (1) above. The optimizations that depend on SSA form are still valid if there are some extraneous ϕ\phi-functions beyond those that would appear in minimal SSA form. However, extraneous ϕ\phi-functions can cause information to be lost, and they always add unnecessary overhead to the optimization process itself. Thus, it is important to place ϕ\phi-functions only where they are required. One variant of SSA form [52] would sometimes forego placing a ϕ\phi-function at a convergence point ZZ, so long as there are no more uses for VV in or after ZZ. The ϕ\phi-function could then be omitted without any risk of losing Condition (3). This has been called pruned SSA form [16], and it is sometimes preferable to our placement at all convergence points. However, as Figure 16 in Section 7 illustrates, our form is sometimes preferable to pruned form. When desired, pruned SSA form can be obtained by a simple adjustment of our algorithm [16, Section 5.1].

For any variable VV, the nodes at which we should insert ϕ\phi-functions in the original program can be defined recursively by Condition (1) in the definition of SSA form. A node ZZ needs a ϕ\phi-function for VV if ZZ is a convergence point for two paths that originate at two different nodes, both nodes containing assignments to VV or needing ϕ\phi-functions for VV. Nonrecursively, we may observe that a node ZZ needs a ϕ\phi-function for VV because ZZ is a convergence point for two nonnull paths X→+ZX \xrightarrow{+} Z and Y→+ZY \xrightarrow{+} Z that start at nodes XX and YY already containing assignments to VV. If ZZ did not already contain an assignment to VV, then the ϕ\phi-function inserted at ZZ adds ZZ to the set of nodes that contain assignments to VV. With more nodes to consider as origins of paths, we may observe more nodes appearing as convergence points of nonnull paths originating at nodes with assignments to V. The set of nodes needing ϕ\phi-functions could thus be found by iterating an observation/insertion cycle.2^2 The algorithm presented here obtains the same end results in much less time.

3.1 Other Program Constructs

If the source program computes expressions using constants and scalar variables to assign values to scalar variables, then it is straightforward to derive an intermediate text that is ready to be put into SSA form (or otherwise analyzed and transformed by an optimizing compiler). However, source programs commonly use other constructs as well: Some variables are not scalars, and some computations do not explicitly indicate which variables they use or change. This section sketches how to map some important constructs to an explicit intermediate text suitable for use in a compiler.

3.1.1 Arrays. It will suffice to consider one-dimensional arrays here. Arrays of higher dimension involve more notation but no more concerns.

If A and B are array variables, then array assignment statements like A \leftarrow B or A \leftarrow 0, if allowed by the source language, can be treated just like assignments to scalar variables. Many of the mentions of A(i) for some index i, which may be taken to be an integer variable. Treating A(i) as a variable would be awkward, both because an assignment to A(i) may or may not change the value of A(j) and because the value of A(i) could be changed by assigning to i rather than to A(i). An easier approach is illustrated in Figure 7. The entire array is treated like a single scalar variable, which may be one of the operands of Access or Update.3^3 The expression Access(A, i) evaluates to the ith component of A; the expression Update(A, j, V) evaluates to an array value that is of the same size as A and has the same component values, except for V as the value of the jth component. Assigning a scalar value V to A(j) is equivalent to assigning an array value to the entire array A, where the new array value depends on the old one, as well as on the index j and on the new scalar value V. The translation to SSA form is unconcerned with whether the values of variables are large objects or what the operators mean.

As with scalars, translation of array references to SSA form removes some anti- and output-dependences [32]. In the program in Figure 7a, dependence analysis may prohibit reordering the use of A(i) by the first statement and the definition of A(j) by the second statement. After translation to SSA form, the two references to A have been renamed, and reordering is then possible. For example, the two statements can execute concurrently. Where optimization does not reorder the two references, Section 7.2 describes how translation out of SSA form reclaims storage that would otherwise be necessary to maintain distinct variables.

\footnotetext{ 2^2In typical cases, paths originating at ϕ\phi-functions add nothing beyond the contributions from paths originating at ordinary assignments. The straightforward iteration is still too slow, even in typical cases 3^3This notation is similar to Select and Update [24], which, in turn, is similar to notation for referencing aggregate structures in a data flow language [23]. }

←A(i)←Access⁡(A,i)←Access⁡(A8,i7)A(j)←VA←Update⁡(A,j,V)A9←Update⁡(A8,j6,V5)←A(k)+2T←Access⁡(A,k)T1←Access⁡(A9,k4)←T+2←T1+2\begin{array}{lll} \leftarrow \mathbf{A}(i) & \leftarrow \operatorname{Access}(\mathbf{A}, i) & \leftarrow \operatorname{Access}(\mathbf{A}_8, i_7) \\ \mathbf{A}(j) \leftarrow V & \mathbf{A} \leftarrow \operatorname{Update}(\mathbf{A}, j, V) & \mathbf{A}_9 \leftarrow \operatorname{Update}(\mathbf{A}_8, j_6, V_5) \\ \leftarrow \mathbf{A}(k) + 2 & T \leftarrow \operatorname{Access}(\mathbf{A}, k) & T_1 \leftarrow \operatorname{Access}(\mathbf{A}_9, k_4) \\ \leftarrow T + 2 & \leftarrow T_1 + 2 & \end{array}

(a) (b) (c)

Figure 7
Source code with array component references (a) is equivalent to code with explicit Access and Update operators that treat the array A just like a scalar (b). Transformation to SSA form proceeds as usual (c).

Consider the loop shown in Figure 8a, which assigns to every component of array A. The value assigned to each component A(i)\mathbf{A}(i) does not depend (even indirectly) on any of the values previously assigned to components of A. In terms of its effect on A, the whole loop is like an assignment of the form A←(…)\mathbf{A} \leftarrow (\ldots), where A is not used in (\ldots). Any assignment to a component of A that is not used before entering such an initialization loop is dead code that should be eliminated. With or without SSA form, the Update operator in Figure 8b makes A appear to be live at entry to the loop.

One reasonable response to the crudeness of the Update operator is to accept it. Address calculations and other genuine scalar calculations can still be optimized extensively. Another response is to perform dependence analysis [3, 10, 32, 51], which can sometimes determine that no subsequent accesses of A require values produced by any other assignment to A. Such is the case for each execution of the assignment to A(i)\mathbf{A}(i) in Figure 8a. The assignment statement can then be viewed as an initialization of A. The problem for us, or for anyone who uses Update to make arrays look like scalars, is to communicate some of the results of dependence analysis to optimizations (like dead code elimination) that are usually formulated in terms of "scalar" variables. A simple solution to this formal problem is shown in Figure 8c, where the HiddenUpdate operator does not mention the assigned array operand. The actual code generated for an assignment from a HiddenUpdate expression is exactly the same as for an assignment from the corresponding Update expression, where the hidden operand is supplied by the target of the assignment.

3.1.2 Structures. A structure can be generally regarded as an array, where references to structure fields are treated as references to elements of the array. Thus, an assignment to a structure field is translated into an Update of the structure, and a use of a structure field is translated into an Access of the structure. In the prevalent case of simple structure field references, this treatment results in arrays whose elements are indexed by constants. Dependence analysis can often determine independence among such accesses, so that optimizations may move an assignment to one field far from an assignment to another field. If analysis or language semantics reveals a structure whose n fields are always accessed disjointly, then the structure can be decomposed into nn distinct variables. The elements of such structures are united in the source program only for organizational reasons, and the expression of the structure's decomposition in SSA form makes the program's actual use of the structure more apparent to subsequent optimization.

            
              integer A(1:100)
integer A₀(1:100)
integer A₁(1:100)
integer A₂(1:100)
i ← 1
repeat
    i₂ ← φ(i₁, i₃)
    A₁ ← φ(A₀, A₂)
    A₂ ← Update(A₁, i₂, i₂)
    i₃ ← i₂ + 1
    A(i) ← i
    i ← i + 1
until i ≥ 100
            
          
            
              (a)
integer A_0(1:100)
integer A_1(1:100)
integer A_2(1:100)
i_1 ← 1
repeat
    i_2 ← φ(i_1, i_3)
    A_1 ← φ(A_0, A_2)
    A_2 ← HiddenUpdate(i_2, i_2)
    i_3 ← i_2 + 1
until i_3 ≥ 100

(b)
integer A_0(1:100)
integer A_1(1:100)
integer A_2(1:100)
i_1 ← 1
repeat
    i_2 ← φ(i_1, i_3)
    A_1 ← φ(A_0, A_2)
    A_2 ← HiddenUpdate(i_2, i_2)
    i_3 ← i_2 + 1
until i_3 ≥ 100
(c)
            
          

Fig. 8. Source loop with array assignment (a) is equivalent to code with an Update operator that treats the array A just like a scalar (b). As Section 7.2 explains, the eventual translation out of SSA form will leave just one array here. Using HiddenUpdate (c) is a purely formal way to summarize some results of dependency analysis, if available.

3.1.3 Implicit References to Variables. The construction of SSA form requires knowing those variables modified and used by a statement. In addition to those variables explicitly referenced, a statement may use or modify variables not mentioned by the statement itself. Examples of such implicit references are global variables modified or used by a procedure call, aliased variables, and dereferenced pointer variables. To obtain SSA form, we must account for implicit as well as explicit references, either by conservative assumptions or by analysis. Heap storage can be conservatively modeled by representing the entire heap as a single variable that is both modified and used by any statement that may change the heap. More refined modeling is also possible [15], but the conservative approach is already strong enough to support optimization of code that does not involve the heap but is interspersed with heap-dependent code.

For any statement SS, three types of references affect translation into SSA form:

(1) MustMod(S) is the set of variables that must be modified by execution of SS. (2) MayMod(S) is the set of variables that may be modified by execution of SS. (3) MayUse(S) is the set of variables whose values prior to execution of SS may be used by SS.

We represent implicit references for any statement SS by transformation to an assignment statement AA, where all the variables in MayMod(S) appear in LHS(A)LHS(A) and all the variables in MayUse(S)∪(MayMod(S)−MustMod(S))MayUse(S) \cup (MayMod(S) - MustMod(S)) appear in RHS(A)RHS(A).

An optimizing compiler may or may not have access to the bodies of all procedures called (directly or indirectly) or to summaries of their effects. If no specific information is available, it is customary to make conservative assumptions. A call might change any global variable or any parameter passed by reference, but it is reasonable to assume that variables local to the caller will not change. Such assumptions limit the extent that transformations can be performed. Techniques are available to extract more detailed information: To determine parameter aliasing and effects of procedures on global variables, see [7–9], [19], [37], and [42]; to determine pointer aliasing, see [14, 15, 27, 29, 33, 34], and [44].

When a sophisticated analysis technique is applied, the usual result is that there are few side effects and the tuples (both LHS and RHS) are small. Small tuples can be represented directly. Sophisticated analysis, however, is often unavailable. Many compilers do no interprocedural analysis at all. Consider a call to an external procedure that has not been analyzed. Both tuples for the call must contain all global variables. The compiler’s own representation of the tuples can still be compact. The representation can be a structure that includes a flag (set to indicate that all globals are in the tuple) plus a direct representation of the few local variables that are in the tuple because of parameter transmission. The intuitive explanations and theoretical analyses of optimization techniques (with or without SSA form) are conveniently formulated in terms of explicit tuples; compact representations can still be used in the implementations.

3.2 Overview of the SSA Algorithm

Translation to minimal SSA form is done in three steps:

(1) The dominance frontier mapping is constructed from the control flow graph (Section 4.2). (2) Using the dominance frontiers, the locations of the ϕ\phi-functions for each variable in the original program are determined (Section 5.1). (3) The variables are renamed (Section 5.2) by replacing each mention of an original variable VV by an appropriate mention of a new variable VtV_t.

4 DOMINANCE

Section 4.1 reviews the dominance relation [47] between nodes in the control flow graph and how to summarize this relation in a dominator tree. Section 4.2 introduces the dominance frontier mapping and gives an algorithm for its computation.

4.1 Dominator Trees

Let XX and YY be nodes in the control flow graph CFGCFG of a program. If -XX appears on every path from Entry to YY, then XX dominates YY. Domination is both reflexive and transitive. If XX dominates YY and X≠YX \neq Y, then XX strictly

Fig. 9 Control flow graph and dominator tree of the simple program The sets of nodes listed in () and [ ] brackets summarize the dominance frontier calculation from Section 4.2. Each node XX is annotated with two sets [DFlocal(X)∣DF(X)][DF_{local}(X)|DF(X)] and a third set (DFup(X))(DF_{up}(X)).

dominates YY. In formulas, we write X≫YX \gg Y for strict domination and X≥YX \geq Y for domination. If XX does not strictly dominate YY, we write X≫̸YX \not\gg Y. The immediate dominator of YY (denoted idom(Y)idom(Y)) is the closest strict dominator of YY on any path from Entry to YY. In a dominator tree, the children of a node XX are all immediately dominated by XX. The root of a dominator tree is Entry, and any node YY other than Entry has idom(Y)idom(Y) as its parent in the tree. The dominator tree for CFGCFG from Figure 5 is shown in Figure 9. Let NN and EE be the numbers of nodes and edges in CFGCFG. The dominator tree can be constructed in O(Eα(E,N))O(E \alpha(E, N)) time [35] or (by a more difficult algorithm) in O(E)O(E) time [26]. For all practical purposes, α(E,N)\alpha(E, N) is a small constant,4^4 so this paper will consider the dominator tree to have been found in linear time.

The dominator tree of CFGCFG has exactly the same set of nodes as CFGCFG but a very different set of edges. Here, the words predecessor, successor, and path always refer to CFGCFG. The words parent, child, ancestor, and descendant always refer to the dominator tree.

4^4Under the definition of α\alpha used in analyzing the dominator tree algorithm [35, p. 123], N≤EN \leq E implies that α(E,N)=1\alpha(E, N) = 1 when log⁡2N<16\log_2 N < 16 and α(E,N)=2\alpha(E, N) = 2 when 16≤log⁡2N<21616 \leq \log_2 N < 2^{16}.

4.2 Dominance Frontiers

The dominance frontier DF(X)DF(X) of a CFGCFG node XX is the set of all CFGCFG nodes YY such that XX dominates a predecessor of YY but does not strictly dominate YY:

DF(X)={Y∣(∃P∈Pred(Y))(X≫P and X≫̸Y)}.DF(X) = \{ Y \mid (\exists P \in Pred(Y)) (X \gg P \text{ and } X \not\gg Y) \}.

Computing DF(X)DF(X) directly from the definition would require searching much of the dominator-tree. The total time to compute DF(X)DF(X) for all nodes XX would be quadratic, even when the sets themselves are small. To compute the dominance frontier mapping in time linear in the size ∑X∣DF(X)∣\sum_X |DF(X)| of the mapping, we define two intermediate sets DFlocalDF_{local} and DFupDF_{up} for each node such that the following equation holds:

DF(X)=DFlocal(X)∪⋃Z∈Children(X)DFup(Z).DF(X) = DF_{local}(X) \cup \bigcup_{Z \in Children(X)} DF_{up}(Z).

Given any node XX, some of the successors of XX may contribute to DF(X)DF(X). This local contribution DFlocal(X)DF_{local}(X) is defined by

DFlocal(X)=def{Y∈Succ(X)∣X≫̸Y}.DF_{local}(X) \overset{\text{def}}{=} \{ Y \in Succ(X) \mid X \not\gg Y \}.

Given any node ZZ that is not the root Entry of the dominator tree, some of the nodes in DF(Z)DF(Z) may contribute to DF(idom(Z))DF(idom(Z)). The contribution DFup(Z)DF_{up}(Z) that ZZ passes up to idom(Z)idom(Z) is defined by

DFup(Z)=def{Y∈DF(Z)∣idom(Z)≫̸Y}.DF_{up}(Z) \overset{\text{def}}{=} \{ Y \in DF(Z) \mid idom(Z) \not\gg Y \}.

Lemma 1. The dominance frontier equation (4) is correct.

Proof. Because dominance is reflexive, DFlocal(X)⊆DF(X)DF_{local}(X) \subseteq DF(X). Because dominance is transitive, each child ZZ of XX has DFup(Z)⊆DF(X)DF_{up}(Z) \subseteq DF(X). We must still show that everything in DF(X)DF(X) has been accounted for. Suppose Y∈DF(X)Y \in DF(X), and let U→YU \to Y be an edge such that XX dominates UU but does not strictly dominate YY. If U=XU = X, then Y∈DFlocal(X)Y \in DF_{local}(X), and we are done. If U≠XU \neq X, on the other hand, then there is a child ZZ of XX that dominates UU but cannot strictly dominate YY because XX does not strictly dominate YY. This implies that Y∈DFup(Z)Y \in DF_{up}(Z).

The intermediate sets can be computed with simple equality tests as follows:

Lemma 2. For any node XX,

DFlocal(X)={Y∈Succ(X)∣idom(Y)≠X}.DF_{local}(X) = \{ Y \in Succ(X) \mid idom(Y) \neq X \}.

Proof. We assume that Y∈Succ(X)Y \in Succ(X) and show that

(X≫Y)⇔(idom(Y)=X).(X \gg Y) \Leftrightarrow (idom(Y) = X).

The ⇐\Leftarrow part is true because the immediate dominator is defined to be a strict dominator. For the ⇒\Rightarrow part, suppose that XX strictly dominates YY and, hence,

            
              for each X in a bottom-up traversal of the dominator tree do
DF(X) ← ∅
for each Y ∈ Succ(X) do
/*local*/ if idom(Y) ≠ X then DF(X) ← DF(X) ∪ {Y}
end
for each Z ∈ Children(X) do
for each Y ∈ DF(Z) do
/*up*/ if idom(Y) ≠ X then DF(X) ← DF(X) ∪ {Y}
end
end
end
            
          
Figure 10
Calculation of DF(X)DF(X) for each CFGCFG node XX.

that some child VV of XX dominates YY. Then VV appears on any path from Entry to YY that goes to XX and then follows the edge X→YX \rightarrow Y, so either VV dominates XX or V=YV = Y. But VV cannot dominate XX, so V=YV = Y and idom(Y)=idom(V)=Xidom(Y) = idom(V) = X. □\Box

Lemma 3. For any node XX and any child ZZ of XX in the dominator tree,

DFup(Z)={Y∈DF(Z)∣idom(Y)≠X}.DF_{up}(Z) = \{ Y \in DF(Z) \mid idom(Y) \neq X \}.

Proof. We assume that Y∈DF(Z)Y \in DF(Z) and show that

(X≫Y)⇔(idom(Y)=X).(X \gg Y) \Leftrightarrow (idom(Y) = X).

The ⇐\Leftarrow part is true because strict dominance is the transitive closure of immediate dominance. For the ⇒\Rightarrow part, suppose that XX strictly dominates YY and, hence, that some child VV of XX dominates YY. Choose a predecessor UU of YY such that ZZ dominates UU. Then VV appears on any path from Entry to YY that goes to UU and then follows the edge U→YU \rightarrow Y, so either VV dominates UU or V=YV = Y. If V=YV = Y, then idom(Y)=idom(V)=Xidom(Y) = idom(V) = X, and we are done. Suppose that V≠YV \neq Y (and, hence, that VV dominates UU) and derive a contradiction. Only one child of XX can dominate UU, so V=ZV = Z and ZZ dominates YY. This contradicts the hypothesis that Y∈DF(Z)Y \in DF(Z). □\Box

These results imply the correctness of the algorithm for computing the dominance frontiers given in Figure 10. The /local/ line effectively computes DFlocal(X)DF_{local}(X) on the fly and uses it in (4) without needing to devote storage to it. The /up/ line is similar for DFup(Z)DF_{up}(Z). We traverse the dominator tree bottom-up, visiting each node XX only after visiting each of its children. To illustrate the working of this algorithm, we have annotated the dominator tree in Figure 9 with the information [ ] and () brackets.

Theorem 1. The algorithm in Figure 10 is correct.

Proof. Direct from the preceding lemmas. □\Box

Let CFGCFG have NN nodes and EE edges. The loop over Succ(X)Succ(X) in Figure 10 examines each edge just once, so all executions of the /local/ line are complete in time O(E)O(E). Similarly, all executions of the /up/ line are complete in time O(size(DF))O(\mathrm{size}(DF)). The overall time is thus O(E+size(DF))O(E + \mathrm{size}(DF)), which amounts to a worst-case complexity of O(E+N2)O(E + N^2). However, Section 8 shows that, in practice, the size of the mapping DFDF is usually linear. We have implemented this algorithm and have observed that it is faster than the standard data-flow computations in the PTRAN compiler [2].

4.3 Relating Dominance Frontiers to Joins

We start by stating more formally the nonrecursive characterization of where the ϕ\phi-functions should be located. Given a set S\mathcal{S} of CFGCFG nodes, the set J(S)J(\mathcal{S}) of join nodes is defined to be the set of all nodes ZZ such that there are two nonnull CFGCFG paths that start at two distinct nodes in S\mathcal{S} and converge at ZZ. The iterated join J+(S)J^+(\mathcal{S}) is the limit of the increasing sequence of sets of nodes

J1=J(S);J_1 = J(\mathcal{S});
Ji+1=J(S∪Ji).J_{i+1} = J(\mathcal{S} \cup J_i).

In particular, if S\mathcal{S} happens to be the set of assignment nodes for a variable VV, then J+(S)J^+(\mathcal{S}) is the set of ϕ\phi-function nodes for VV.

The join and iterated join operations map sets of nodes to sets of nodes. We extend the dominance frontier mapping from nodes to sets of nodes in the natural way:

DF(S)=⋃X∈SDF(X).DF(\mathcal{S}) = \bigcup_{X \in \mathcal{S}} DF(X).

As with join, the iterated dominance frontier DF+(S)DF^+(\mathcal{S}) is the limit of the increasing sequence of sets of nodes

DF1=DF(S);DF_1 = DF(\mathcal{S});
DFi+1=DF(S∪DFi).DF_{i+1} = DF(\mathcal{S} \cup DF_i).

The actual computation of DF+(S)DF^+(\mathcal{S}) is performed by the efficient worklist algorithm in Section 5.1; the formulation here is convenient for relating iterated dominance frontiers to iterated joins. If the set S\mathcal{S} is the set of assignment nodes for a variable VV, then we will show that

J+(S)=DF+(S)J^+(\mathcal{S}) = DF^+(\mathcal{S})

(this equation depends on the fact that Entry is in S\mathcal{S}) and, hence, that the location of the ϕ\phi-functions for VV can be computed by the worklist algorithm for computing DF+(S)DF^+(\mathcal{S}) that is given in Section 5.1.

The following lemmas do most of the work by relating dominance frontiers to joins:

Lemma 4. For any nonnull path p: X→+ZX \rightarrow^+ Z in CFGCFG, there is a node X′∈{X}∪DF+({X})X' \in \{ X \} \cup DF^+(\{ X \}) on p that dominates ZZ. Moreover, unless XX dominates every node on p, the node X′X' can be chosen in DF+({X})DF^+(\{ X \}).

Proof. If XX dominates every node on pp, then we just choose X′=XX' = X to get all the claimed properties of X′X'. We may assume that some of the nodes on pp are not dominated by XX. Let the sequence of nodes on pp be X=X0,…,XJ=ZX = X_0, \ldots, X_J = Z. For the smallest ii such that XX does not dominate XiX_i, the predecessor Xi−1X_{i-1} is dominated by XX and so puts XiX_i into DF(X)DF(X). Thus, there are choices of jj with Xj∈DF+({X})X_j \in DF^+(\{X\}). Consider X′=XjX' = X_j for the largest jj with Xj∈DF+({X})X_j \in DF^+(\{X\}). We will show that X′≫ZX' \gg Z. Suppose not. Then j<Jj < J, and there is a first kk with j<k≤Jj < k \leq J such that X′X' does not dominate XkX_k. The predecessor Xk−1X_{k-1} is dominated by X′X' and so puts XkX_k into DF(X′)DF(X'). Thus, Xk∈DF(DF+({X}))=DF+({X})X_k \in DF(DF^+(\{X\})) = DF^+(\{X\}), contradicting the choice of jj.

Lemma 5. Let X≠YX \neq Y be two nodes in CFG, and suppose that nonnull paths p:X→+Zp: X \to^+ Z and q:Y→+Zq: Y \to^+ Z in CFG converge at ZZ. Then Z∈DF+({X})∪DF+({Y})Z \in DF^+(\{X\}) \cup DF^+(\{Y\}).

Proof. We consider three cases that are obviously exhaustive. In the first two cases, we prove that Z∈DF+({X})∪DF+({Y})Z \in DF^+(\{X\}) \cup DF^+(\{Y\}). Then we show that the first two cases are (unobviously) exhaustive because the third case leads to a contradiction. Let X′X' be from Lemma 4 for the path pp, with the sequence of nodes X=X0,…,XJ=ZX = X_0, \ldots, X_J = Z. Let Y′Y' be from Lemma 4 for the path qq, with the sequence of nodes Y=Y0,…,YK=ZY = Y_0, \ldots, Y_K = Z.

Case 1. We suppose that X′X' is on qq and show that Z∈DF+({X})Z \in DF^+(\{X\}). By the definition of convergence (specifically, (3) in Section 2), X′=ZX' = Z. We may now assume that Z=XZ = X and XX dominates every node on pp. (Otherwise, Lemma 4 already asserts Z∈DF+({X})Z \in DF^+(\{X\}).) Because XX dominates the predecessor XJ−1X_{J-1} of ZZ but does not strictly dominate ZZ, we have Z∈DF(X)⊆DF+({X})Z \in DF(X) \subseteq DF^+(\{X\}).

Case 2. We suppose that Y′Y' is on pp, and show that Z∈DF+({Y})Z \in DF^+(\{Y\}), reasoning just as in Case 1.

Case 3. We derive a contradiction from the suppositions that X′X' is not on qq and Y′Y' is not on pp. Because X′≫ZX' \gg Z but X′X' is not on qq, X′≫YK=ZX' \gg Y_K = Z and, therefore, dominates all predecessors of YKY_K. In particular, X′≫YK−1X' \gg Y_{K-1}. But X′≠YK−1X' \neq Y_{K-1}, so X′≫YK−1X' \gg Y_{K-1}, and we continue inductively to show that X′≫YkX' \gg Y_k for all kk. In particular, X′≫Y′X' \gg Y'. On the other hand, by similar reasoning from the supposition that Y′≫ZY' \gg Z but Y′Y' is not on pp, we can show that Y′≫X′Y' \gg X'. Two nodes cannot strictly dominate each other, so Case 3 is impossible.

Lemma 6. For any set S\mathcal{S} of CFG nodes, J(S)⊆DF+(S)J(\mathcal{S}) \subseteq DF^+(\mathcal{S}).

Proof. We apply Lemma 5.

Lemma 7. For any set S\mathcal{S} of CFG nodes such that Entry ∈S\in \mathcal{S}, DF(S)⊆J(S)DF(\mathcal{S}) \subseteq J(\mathcal{S}).

Proof. Consider any X∈SX \in \mathcal{S} and any Y∈DF(X)Y \in DF(X). There is a path from XX to YY where all nodes before YY are dominated by XX. There is also a path from Entry to YY where none of the nodes are dominated by XX. The paths therefore converge at YY.

Theorem 2. The set of nodes that need ϕ\phi-functions for any variable V is the iterated dominance frontier DF+(S)DF^+(\mathcal{S}), where S\mathcal{S} is the set of nodes with assignments to V.

Proof. By Lemma 6 and induction on ii in the definition of J+J^+, we can show that

J+(S)⊆DF+(S).J^+(\mathcal{S}) \subseteq DF^+(\mathcal{S}).

The induction step is as follows:

Ji+1=J(S∪Ji)⊆J(S∪DF+(S))J_{i+1} = J(\mathcal{S} \cup J_i) \subseteq J(\mathcal{S} \cup DF^+(\mathcal{S}))
⊆DF+(S∪DF+(S))=DF+(S).\subseteq DF^+(\mathcal{S} \cup DF^+(\mathcal{S})) = DF^+(\mathcal{S}).

The node Entry is in S\mathcal{S}, so Lemma 7 and another induction yield

DF+(S)⊆J+(S).DF^+(\mathcal{S}) \subseteq J^+(\mathcal{S}).

The induction step is as follows:

DFi+1=DF(S∪DFi)⊆DF(S∪J+(S))DF_{i+1} = DF(\mathcal{S} \cup DF_i) \subseteq DF(\mathcal{S} \cup J^+(\mathcal{S}))
⊆J(S∪J+(S))=J+(S).\subseteq J(\mathcal{S} \cup J^+(\mathcal{S})) = J^+(\mathcal{S}).

The set of nodes that need ϕ\phi-functions for VV is precisely J+(S)J^+(\mathcal{S}), so (5) and (6) prove the theorem. □

5 CONSTRUCTION OF MINIMAL SSA FORM

5.1 Using Dominance Frontiers to Find Where ϕ\phi-Functions Are Needed

The algorithm in Figure 11 inserts trivial ϕ\phi-functions. The outer loop of this algorithm is performed once for each variable VV in the program. Several data structures are used:

— WW is the worklist of CFGCFG nodes being processed. In each iteration of this algorithm, WW is initialized to the set A(V)\mathcal{A}(V) of nodes that contain assignments to VV. Each node XX in the worklist ensures that each node YY in DF(X)DF(X) receives a ϕ\phi-function. Each iteration terminates when the worklist becomes empty. — Work(∗)Work(*) is an array of flags, one flag for each node, where Work(X)Work(X) indicates whether XX has ever been added to WW during the current iteration of the outer loop. — HasAlready(∗)HasAlready(*) is an array of flags, one for each node, where HasAlready(X)HasAlready(X) indicates whether a ϕ\phi-function for VV has already been inserted at XX.

The flags Work(X)Work(X) and HasAlready(X)HasAlready(X) are independent. We need two flags because the property of assigning to VV is independent of the property of needing a ϕ\phi-function for VV. The flags could have been implemented with just the values true and false, but this would require additional record keeping to reset any true flags between iterations, without the expense of looping over all the nodes. It is simpler to devote an integer to each flag and to test flags by comparing them with the current iteration count.

Let each node XX have Aorig(X)A_{orig}(X) original assignments to variables, where each ordinary assignment statement LHS←RHSLHS \leftarrow RHS contributes the length of the tuple LHSLHS to Aorig(X)A_{orig}(X). Counting assignments to variables is one of

            
              IterCount ← 0
for each node X do
    HasAlready(X) ← 0
    Work(X) ← 0
end
W ← ∅
for each variable V do
    IterCount ← IterCount + 1
    for each X ∈ A(V) do
        Work(X) ← IterCount
        W ← W ∪ {X}
    end
while W ≠ ∅ do
    take X from W
    for each Y ∈ DF(X) do
        if HasAlready(Y) < IterCount
            then do
                place ⟨ V ← φ(V, ..., V) ⟩ at Y
                HasAlready(Y) ← IterCount
                if Work(Y) < IterCount
                    then do
                        Work(Y) ← IterCount
                        W ← W ∪ {Y}
                    end
                end
            end
        end
    end
end
            
          

Fig. 11. Placement of ϕ\phi-functions.

several measures of program size.5^5 By this measure, the program expands from size Aorig=∑XAorig(X)A_{orig} = \sum_X A_{orig}(X) to size Atot=∑XAtot(X)A_{tot} = \sum_X A_{tot}(X), where each ϕ\phi-function placed at XX contributes 1 to Atot(X)=Aorig(X)+Aϕ(X)A_{tot}(X) = A_{orig}(X) + A_\phi(X). There is a similar expansion in the number of mentions of variables, from Morig=∑XMorig(X)M_{orig} = \sum_X M_{orig}(X) to Mtot=∑XMtot(X)M_{tot} = \sum_X M_{tot}(X), where each ϕ\phi-function placed at XX contributes 1 plus the indegree of XX to Mtot(X)=Morig(X)+Mϕ(X)M_{tot}(X) = M_{orig}(X) + M_\phi(X).

Placing a ϕ\phi-function at YY in Figure 11 has cost linear in the indegree of YY, so there is an O(∑XMϕ(X))O(\sum_X M_\phi(X)) contribution to the running time from the work done when HasAlready(Y)<IterCountHasAlready(Y) < IterCount. Replacing mentions of variables will contribute at least O(Mtot)O(M_{tot}) to the running time of any SSA translation algorithm, so the cost of placement can be ignored in analyzing the contribution of Figure 11 to the O(...)O(...) bound for the whole process. The O(N)O(N) cost of

5^5The various measures relevant here are reviewed when the whole SSA translation process is summarized in Section 9.1.

initialization can be similarly ignored because it is subsumed by the cost of the dominance frontier calculation. What cannot be ignored is the cost of managing the worklist W. The statement take X from W in Figure 11 is performed Atot(X)A_{tot}(X) times, and each time incurs a cost linear in ∣DF(X)∣|DF(X)| because all Y∈DF(X)Y \in DF(X) are polled. The contribution of Figure 11 to the running time of the whole process is therefore O(∑X(Atot(X)×∣DF(X)∣))O(\sum_X (A_{tot}(X) \times |DF(X)|)). Sizing the output in the natural way as AtotA_{tot}, we can also describe the contribution as O(Atot×avrgDF)O(A_{tot} \times avrgDF), where the weighted average

avrgDF=def(∑X(Atot(X)×∣DF(X)∣))/(∑XAtot(X))avrgDF \overset{\text{def}}{=} \left( \sum_X (A_{tot}(X) \times |DF(X)|) \right) / \left( \sum_X A_{tot}(X) \right)

emphasizes the dominance frontiers of nodes with many assignments. As Section 8 shows, the dominance frontiers are small in practice, and Figure 11 is effectively O(Atot)O(A_{tot}). This, in turn, is effectively O(Aorig)O(A_{orig}).

5.2 Renaming

The algorithm in Figure 12 renames all mentions of variables. New variables denoted ViV_i, where ii is an integer, are generated for each variable VV. Figure 12 begins a top-down traversal of the dominator tree by calling SEARCH at the root node Entry. The visit to a node processes the statements associated with the node in sequential order, starting with any ϕ\phi-functions that may have been inserted. The processing of a statement requires work for only those variables actually mentioned in the statement. In contrast with Figure 11, we need a loop over all variables only when we initialize two arrays among the following data structures:

—S(∗)S(*) is an array of stacks, one stack for each variable VV. The stacks can hold integers. The integer ii at the top of S(V)S(V) is used to construct the variable ViV_i that should replace a use of VV. —C(∗)C(*) is an array of integers, one for each variable VV. The counter value C(V)C(V) tells how many assignments to VV have been processed. —WhichPred(X,YX, Y) is an integer telling which predecessor of YY in CFGCFG is XX. The jjth operand of a ϕ\phi-function in YY corresponds to the jjth predecessor of YY from the listing of the inedges of YY. —Each assignment statement AA has the form

LHS(A)←RHS(A)LHS(A) \leftarrow RHS(A)

where the right-hand side RHS(A)RHS(A) is a tuple of expressions and the left-hand side LHS(A)LHS(A) is a tuple of distinct target variables. These tuples change as mentions of variables are renamed, but the original target tuple is still remembered as oldLHS(A)oldLHS(A). To minimize notation, a conditional branch is treated as an ordinary assignment to a special variable whose value indicates which edge the branch should follow. A real implementation would recognize that a conditional branch involves a little less work than a genuine assignment: The LHSLHS part of the processing can be omitted.

            
              for each variable V do
    C(V) ← 0
    S(V) ← EmptyStack
end
call SEARCH(Entry)
            
          

SEARCH(XX) : for each statement AA in XX do if AA is an ordinary assignment then for each variable VV used in RHS(A)RHS(A) do replace use of VV by use of ViV_i, where i=Top(S(V))i = Top(S(V)) end for each VV in LHS(A)LHS(A) do i←C(V)i \leftarrow C(V) replace VV by new ViV_i in LHS(A)LHS(A) push ii onto S(V)S(V) C(V)←i+1C(V) \leftarrow i + 1 end end / of first loop / for each Y∈Succ(X)Y \in Succ(X) do j←WhichPred(Y,X)j \leftarrow WhichPred(Y, X) for each ϕ\phi-function FF in YY do replace the jj-th operand VV in RHS(F)RHS(F) by ViV_i where i=Top(S(V))i = Top(S(V)) end end for each Y∈Children(X)Y \in Children(X) do call SEARCH(Y) end end for each assignment AA in XX do for each VV in oldLHS(A)oldLHS(A) do pop S(V)S(V) end end end SEARCH

Fig. 12 Renaming mentions of variables. The list of uses of ViV_i grows with each replacement of VV by ViV_i in a RHSRHS

The processing of an assignment statement AA considers the mentions of variables in AA. The simplest case is that of a target variable VV in the tuple LHS(A)LHS(A). We need a new variable ViV_i. By keeping a count C(V)C(V) of the number of assignments to VV that have already been processed, we can find an appropriate new variable by using i=C(V)i = C(V) and then incrementing C(V)C(V). To facilitate renaming uses of VV in the future, we also push ii (which identifies ViV_i) onto a stack S(V)S(V) of (integers that identify) new variables replacing VV.

A subtler computation is needed for the right-hand side RHS(A)RHS(A). Consider any variable VV used in RHS(A)RHS(A); that is, VV appears in at least one of the expressions in the tuple. We want to replace VV by ViV_i, where ViV_i is the target of the assignment that produces the value for VV actually used in RHS(A)RHS(A). There are two subcases because AA may be either an ordinary assignment or a ϕ\phi-function. Both subcases get ViV_i from the top of the stack S(V)S(V), but they inspect S(V)S(V) at different times in Figure 12. Lemma 10, introduced later in this section, shows that ViV_i is correctly chosen in both subcases.

The correctness proof for renaming depends on results from Section 4 and on three more lemmas. We start by showing that it makes sense to speak of "the" assignment to a variable in the transformed program.

Lemma 8. Each new variable ViV_i in the transformed program is a target of exactly one assignment.

Proof. Because the counter C(V)C(V) is incremented after processing each assignment to VV, there can be at most one assignment to ViV_i. To show that there is at least one assignment to ViV_i, we consider the two ways ViV_i can be mentioned. If ViV_i is mentioned on the LHS on an assignment, then there is nothing more to show. If the value of ViV_i is used, on the other hand, then i=Top(S(V))i = Top(S(V)) was true at the time when the algorithm renamed the old use of VV to a use of ViV_i. At the earlier time when ii was pushed onto S(V)S(V), it was pushed because an assignment to VV had just been changed to an assignment to ViV_i.

A node XX may contain assignments to the variable VV that appeared in the original program or were introduced to receive the value of a ϕ\phi-function for VV. Let TopAfter(V,X)TopAfter(V, X) denote the new variable in effect for VV after all statements in node XX have been considered by the first loop in Figure 12. Specifically, we consider the top of each stack S(V)S(V) at the end of this loop and define

TopAfter(V,X)=defViwherei=Top(S(V)).TopAfter(V, X) \overset{\text{def}}{=} V_i \quad \text{where} \quad i = Top(S(V)).

If there are no assignments to VV in XX, then the top-down traversal ensures that TopAfter(V,X)TopAfter(V, X) is inherited from the closest dominator of XX that assigns to VV.

Lemma 9. For any variable VV and any CFG edge X→YX \to Y such that YY does not have a ϕ\phi-function for VV,

TopAfter(V,X)=TopAfter(V,idom(Y)).(8)TopAfter(V, X) = TopAfter(V, idom(Y)). \tag{8}
(8)

Proof. We may assume that X≠idom(Y)X \neq idom(Y). Because YY does not have a ϕ\phi-function for VV, if a node ZZ has Y∈DF(Z)Y \in DF(Z), then ZZ does not assign to a new variable derived from VV. We use this fact twice below.

By Lemma 2, Y∈DFlocal(X)⊆DF(X)Y \in DF_{local}(X) \subseteq DF(X), and, thus, XX does not assign to a new variable derived from VV. Let UU be the first node in the sequence idom(X),idom(idom(X)),…idom(X), idom(idom(X)), \ldots that assigns to such a variable. Then

TopAfter(V,X)=TopAfter(V,U).(9)TopAfter(V, X) = TopAfter(V, U). \tag{9}
(9)

Because UU assigns to a new variable derived from VV, it follows that Y∉DF(U)Y \notin DF(U). But UU dominates a predecessor of YY, so UU strictly dominates YY. For any ZZ with U≫Z≫idom(Y)U \gg Z \gg idom(Y), we get Z≫XZ \gg X because Z≫YZ \gg Y and there is an edge X→YX \to Y. By the choice of U,U≫Z≫XU, U \gg Z \gg X implies that ZZ does not assign to a new variable derived from VV. Therefore,

TopAfter(V,U)=TopAfter(V,idom(Y)),TopAfter(V, U) = TopAfter(V, idom(Y)),

and (8) follows from (9).

The preceding lemma helps establish Condition (3) in the definition of SSA form, but first we must extend TopAfterTopAfter so as to specify which new variable corresponds to VV just before and just after each statement AA. There is one iteration of the first loop in Figure 12 for AA. We consider the top of each stack just before and just after this iteration to define

TopBefore(V,A)=defViwherei=Top(S(V)) before processing A;TopBefore(V, A) \overset{\mathrm{def}}{=} V_i \quad \text{where} \quad i = Top(S(V)) \text{ before processing } A;
TopAfter(V,A)=defViwherei=Top(S(V)) after processing A.TopAfter(V, A) \overset{\mathrm{def}}{=} V_i \quad \text{where} \quad i = Top(S(V)) \text{ after processing } A.

In particular, if AA happens to be the last statement in block XX, then TopAfter(V,A)=TopAfter(V,X)TopAfter(V, A) = TopAfter(V, X). If, on the other hand, AA is followed by another statement BB, then TopAfter(V,A)=TopBefore(V,B)TopAfter(V, A) = TopBefore(V, B).

Lemma 10. Consider any control flow path in the transformed program and the same path in the original program. Consider any variable VV and any occurrence of a statement AA along the path. If AA is from the original program, then the value of VV just before executing AA in the original program is the same as the value of TopBefore(V,A)TopBefore(V, A) just before executing AA in the transformed program.

Proof. We use induction along the path. We consider the kkth statement executed along the path and assume that, for all j<kj < k, the jjth statement TT is either not from the original program6^6 or has each variable VV agreeing with TopBefore(V,T)TopBefore(V, T) just before TT. We assume that the kkth statement AA is from the original program and show that VV agrees with TopBefore(V,A)TopBefore(V, A) just before AA.

Case 1. Suppose that AA is not the first original statement in its basic block YY. Let TT be the statement just before AA in YY. By the induction hypothesis and the semantics of assignment, VV agrees with TopAfter(V,T)TopAfter(V, T) just after TT. Thus, VV agrees with TopBefore(V,A)TopBefore(V, A) just before AA.

Case 2. Suppose that AA is the first original statement in its basic block YY. Let X→YX \to Y be the edge followed by the control path. We claim that VV agrees with TopAfter(V,X)TopAfter(V, X) when control flows along this edge.

If X=EntryX = \mathbf{Entry}, then TopAfter(V,X)TopAfter(V, X) is V0V_0 and does hold the entry value of VV. If X≠EntryX \neq \mathbf{Entry}, let TT be the last statement in XX. By the induction hypothesis and the semantics of assignment, VV agrees with TopAfter(V,T)TopAfter(V, T)

6^6It could be a nominal Entry assignment or a ϕ\phi-function.

Figure 13
In the proof of Lemma 10, the node Y may or may not have a ϕ\phi-function for V.

just after T and, hence, with TopAfter(V, X) when control flows along X → Y.

We must still bridge the gap between knowing that V agrees with TopAfter(V, X) along X → Y and knowing that V agrees with TopBefore(V, A) just before doing A in Y. As Figure 13 illustrates, there may or may not be a ϕ\phi-function for V ahead of A in the transformed program.

Case 2.1. Suppose that V has a ϕ\phi-function Vi←ϕ(…)V_i \leftarrow \phi(\ldots) in Y. The ϕ\phi operand corresponding to X → Y is TopAfter(V, X), thanks to the loop over successors of X in Figure 12. This is the operand whose value is assigned to ViV_i, which is TopBefore(V, A). Thus, V agrees with TopBefore(V, A) just before doing A in Y.

Case 2.2. Suppose that V does not have a ϕ\phi-function in Y. By Lemma 9, V agrees with TopAfter(V, X) = TopAfter(V, idom(Y)) = TopBefore(V, A) just before doing A in Y. □

Theorem 3. Any program can be put into minimal SSA form by computing the dominance frontiers and then applying the algorithms in Figure 11 and Figure 12. Let AtotA_{tot} be the total number of assignments to variables in the resulting program.\footnote{Each ordinary assignment LHS←RHSLHS \leftarrow RHS contributes the length of the tuple LHSLHS to AtotA_{tot}, and each ϕ\phi-function contributes 1 to AtotA_{tot}.} Let MtotM_{tot} be the total number of mentions of variables in the resulting program. Let E be the number of edges in CFG. Let avrgDF be the weighted average (7) of dominance frontier sizes. Then the running time of the whole process is

O(E+∑X∣DF(X)∣+(Atot×avrgDF)+Mtot).O\left( E + \sum_X |DF(X)| + (A_{tot} \times \text{avrgDF}) + M_{tot} \right).

Proof. Figure 11 places the ϕ\phi-functions for V at the nodes in the iterated dominance frontier DF+(S)DF^+(\mathcal{S}), where S\mathcal{S} is the set of assignments to V in the original program. By Theorem 2, DF+(S)DF^+(\mathcal{S}) is the set of nodes that need ϕ-functions for V, so we have obtained Condition (1) in the definition of translation to SSA form with the fewest possible ϕ-functions. We must still show that renaming is done correctly by Figure 12. Condition (2) in the definition follows from Lemma 8. Condition (3) follows from Lemma 10.

Let N be the number of nodes in CFG. The dominator tree has O(N) edges, and Figure 12 runs in O(N+E+Mtot)O(N + E + M_{tot}) time. The N+EN + E term is subsumed by the O(E+∑X∣DF(X)∣)O(E + \sum_X |DF(X)|) cost of computing the dominance frontiers, so Figure 12 contributes O(Mtot)O(M_{tot}). As explained at the end of Section 5.1, Figure 11 contributes O(Atot×avgDF)O(A_{tot} \times avgDF).

6 CONSTRUCTION OF CONTROL DEPENDENCIES

In this section we show that control dependences [24] are essentially the dominance frontiers in the reverse graph of the control flow graph. Let X and Y be nodes in CFG. If X appears on every path from Y to Exit, then X postdominates Y.8 Like the dominator relation, the postdominator relation is reflexive and transitive. If X postdominates Y but X≠YX \neq Y, then X strictly postdominates Y. The immediate postdominator of Y is the closest strict postdominator of Y on any path from Y to Exit. In a postdominator tree, the children of a node X are all immediately postdominated by X.

A CFG node Y is control dependent on a CFG node X if both of the following hold:

(1) There is a nonnull path p:X+→Yp: X^+ \to Y such that Y postdominates every node after X on p. (2) The node Y does not strictly postdominate the node X.

In other words, there is some edge from X that definitely causes Y to execute, and there is also some path from X that avoids executing Y. We associate with this control dependence from X to Y the label on the control flow edge from X that causes Y to execute. Our definition of control dependence here can easily be shown to be equivalent to the original definition [24].

Lemma 11. Let X and Y be CFG nodes. Then Y postdominates a successor of X if and only if (iff) there is a nonnull path p:X+→Yp: X^+ \to Y such that Y postdominates every node after X on p.

Proof. Suppose that Y postdominates a successor U of X. Choose any path q from U to Exit. Then Y appears on q. Let r be the initial segment of q that reaches the first appearance of Y on q. For any node V on r, we can get from U to Exit by following r to V and then by taking any path from V to Exit. Because Y postdominates U but does not appear before the end of r, Y must postdominate V as well. Let p be the path that starts with the edge X→UX \to U and then proceeds along r. Then p:X+→Yp: X^+ \to Y and Y postdominates every node after X on p.

8The postdominance relation in [24] is irreflexive, whereas the definition we use here is reflexive. The two relations are identical on pairs of distinct elements. We choose the reflexive definition here to make postdominance the dual of the dominance relation.

build RCFGRCFG build dominator tree for RCFGRCFG apply the algorithm in Figure 10 to find the dominance frontier mapping RDFRDF for RCFGRCFG

            
              for each node X do CD(X) ← ∅ end
for each node Y do
    for each X ∈ RDF(Y) do
        CD(X) ← CD(X) ∪ { Y }
    end
end
            
          
Figure 14
Algorithm for computing the set CD(X)CD(X) of nodes that are control dependent on XX.
NodeCD(Node)CD(Node)
Entry1,2,8,9,11,12
1
23,6,7
34,5
4
5
6
7
8
910
10
119,11
122,8,9,11,12
Figure 15
Control dependences of the program in Figure 5.

Conversely, given a path pp with these properties, let UU be the first node after XX on pp. Then UU is a successor of XX and YY postdominates UU. □\Box

The reverse control flow graph RCFGRCFG has the same nodes as the control flow graph CFGCFG, but has an edge Y→XY \to X for each edge X→YX \to Y in CFGCFG. The roles of Entry and Exit are also reversed. The postdominator relation on CFGCFG is the dominator relation on RCFGRCFG.

Corollary 1. Let XX and YY be nodes in CFGCFG. Then YY is control dependent on XX in CFGCFG iff X∈DF(Y)X \in DF(Y) in RCFGRCFG.

Proof. Using Lemma 11 to simplify the first condition in the definition of control dependence, we find that YY is control dependent on XX iff YY postdominates a successor of XX but does not strictly postdominate XX. In RCFGRCFG this means that YY dominates a predecessor of XX but does not strictly dominate XX; that is, X∈DF(Y)X \in DF(Y). □\Box

Figure 14 applies this result to the computation of control dependences. After building the dominator tree for RCFGRCFG by the standard method [35] in time O(Eα(E,N))O(E\alpha(E,N)), we spend O(size(RDF))O(\text{size}(RDF)) finding dominance frontiers and then inverting them. The total time is thus O(E+size(RDF))O(E + \text{size}(RDF)) for all practical purposes. By applying the algorithm in Figure 14 to the control flow graph in Figure 5, we obtain the control dependences in Figure 15. Note that the edge from Entry to Exit was added to CFGCFG so that the control dependence relation, viewed as a graph, would be rooted at Entry.

7 TRANSLATING FROM SSA FORM

Many powerful analysis and transformation techniques can be applied to programs in SSA form. Eventually, however, a program must be executed. The ϕ\phi-functions have precise semantics, but they are generally not represented in existing target machines. This section describes how to translate out of SSA form by replacing each ϕ\phi-function with some ordinary assignments. Naive translation could yield inefficient object code, but efficient code can be generated if two already useful optimizations are applied: dead code elimination and storage allocation by coloring.

Naively, a kk-input ϕ\phi-function at entrance to a node XX can be replaced by kk ordinary assignments, one at the end of each control flow predecessor of XX. This is always correct, but these ordinary assignments sometimes perform a good deal of useless work. If the naive replacement is preceded by dead code elimination and then followed by coloring, however, the resulting code is efficient.

7.1 Dead Code Elimination

The original source program may have dead code (i.e., code that has no effect on any program output). Some of the intermediate steps in compilation (such as procedure integration) may also introduce dead code. Code that once was live may become dead in the course of optimization. With so many possible sources for dead code, it is natural to perform (or repeat) dead code elimination late in the optimization process, rather than burden many intermediate steps with concerns about dead code.

Translation to SSA form is one of the compilation steps that may introduce dead code. Suppose that VV is assigned and then used along each branch of an if...then...else..., but that VV is never used after the join point. The original assignments to VV are live, but the added assignment by the ϕ\phi-function is dead. Often, such dead ϕ\phi-functions are useful, as in the equivalencing and redundancy elimination algorithms that are based on SSA form [5, 43]. One such use is shown in Figure 16. Although others have avoided placement of dead ϕ\phi-functions in translating to SSA form [16, 52], we prefer to include the dead ϕ\phi-functions to increase optimization opportunities.

There are many different definitions of dead code in the literature. Dead code is sometimes defined to be unreachable code and sometimes defined (as it is here) to be ineffectual code. In both cases, it is desirable to use the broadest possible definition, subject to the correctness condition that "dead" code really can be safely removed.\footnote{The definition used here is broader than the usual one [1, p 595] and similar to that of "faint" variables [25, p. 489].} A procedural version of the definition is more intuitive than a recursive version, so we prefer the procedural style.

Initially, all statements are tentatively marked dead. Some statements, however, need to be marked live because of the conditions listed below. Marking these statements live may cause others to be marked live. When the natural worklist eventually empties, any statements that are still marked dead are truly dead and can be safely removed from the code.

            
              if P₁
then do
    Y₁ ← 1
    use of Y₁
end
else do
    Y₂ ← X₁
    use of Y₂
end
Y₃ ← φ(Y₁, Y₂)
...
if P₁
    then Z₁ ← 1
    else Z₂ ← X₁
Z₃ ← φ(Z₁, Z₂)
use of Z₃ if P₁
then do
    Y₁ ← 1
    use of Y₁
end
else do
    Y₂ ← X₁
    use of Y₂
end
Y₃ ← φ(Y₁, Y₂)
...
use of Y₃
            
          
Figure 16
On the left is an unoptimized program containing a dead φ-function that assigns to Y₃. The value numbering technique in [5] can determine that Y₃ and Z₃ have the same value. Thus, Z₃ and many of the computations that produce it can be eliminated. The dead φ-function is brought to life by using Y₃ in place of Z₃.

A statement is marked live iff at least one of the following holds:

(1) The statement is one that should be assumed to affect program output, such as an I/O statement, an assignment to a reference parameter, or a call to a routine that may have side effects. (2) The statement is an assignment statement, and there are statements already marked live that use some of its outputs. (3) The statement is a conditional branch, and there are statements already marked live that are control dependent on this conditional branch.

Several published algorithms eliminate dead code in the narrower sense that requires every conditional branch to be marked live [30, 31, 38].¹⁰ Our algorithm, given in Figure 17, goes one step further in eliminating dead conditional branches. (An unpublished algorithm by R. Paige does this also.)

¹⁰ The more readily accessible [31] contains a typographical error; the earlier technical report [30] should be consulted for the correct version.

            
              for each statement S do
    if S ∈ PreLive
        then Live(S) ← true
        else Live(S) ← false
    end
WorkList ← PreLive
            
          

while (WorkList≠∅WorkList \neq \emptyset) do take SS from WorkList

            
              for each D ∈ Definers(S) do
        if Live(D) = false
            then do
                Live(D) ← true
                WorkList ← WorkList ∪ { D }
            end
        end
    end

for each block B in CD⁻¹(Block(S)) do
        if Live(Last(B)) = false
            then do
                Live(Last(B)) ← true
                WorkList ← WorkList ∪ { Last(B) }
            end
        end
    end
end
            
          

for each statement SS do if Live(S)=Live(S) = false then delete SS from Block(S)Block(S) end

Fig. 17 Dead code elimination

The following data structures are used:

—Live(S)Live(S) indicates that statement SS is live. —PreLivePreLive is the set of statements whose execution is initially assumed to affect program output. Statements that result in I/O or side effects outside the current procedure scope are typically included. —WorkListWorkList is a list of statements whose liveness has been recently discovered. —Definers(S)Definers(S) is the set of statements that provide values used by statement SS. —Last(B)Last(B) is the statement that terminates basic block BB. —Block(S)Block(S) is the basic block containing statement SS. —ipdom(B)ipdom(B) is the basic block that immediately postdominates block BB.

—CD−1(B)CD^{-1}(B) is the set of nodes that are control dependence predecessors of the node corresponding to block BB. This is the same as RDF(B)RDF(B) in Figure 14.

After the algorithm discovers the live statements, all those still not marked live are deleted.11^{11} Other optimizations (such as code motion) may leave empty blocks, so there is already ample reason for a late optimization to remove them. The empty blocks left by dead code elimination can be removed along with any other empty blocks.

The fact that statements are considered dead until marked live is crucial for condition (2). Statements that depend (transitively) on themselves are never marked live unless required by some other live statement. Condition (3) is handled by the loop over CD−1(Block(S))CD^{-1}(Block(S)). A basic block whose termination controls a block with live statements is itself live.

7.2 Allocation by Coloring

At first, it might seem possible simply to map all occurrences of ViV_i back to VV and to delete all of the ϕ\phi-functions. However, the new variables introduced by translation to SSA form cannot always be eliminated, because optimizations may have capitalized on the storage independence of the new variables. The useful persistence of the new variables introduced by translation to SSA form can be illustrated by the code motion example in Figure 18. The source code (Figure 18a) assigns to VV twice and uses it twice. The SSA form (Figure 18b) can be optimized by moving the invariant assignment out of the loop, yielding a program with separate variables for separate purposes (Figure 18c). The dead assignment to V3V_3 will be eliminated. These optimizations leave a region in the program where V1V_1 and V2V_2 are simultaneously live. Thus, both variables are required: The original variable VV cannot substitute for both renamed variables.

Any graph coloring algorithm [12, 13, 17, 18, 21] can be used to reduce the number of variables needed and thereby can remove most of the associated assignment statements. The choice of coloring technique should be guided by the eventual use of the output. If the goal is to produce readable source code, then it is desirable to consider each original variable VV separately, coloring just the SSA variables derived from VV. If the goal is machine code, then all of the SSA variables should be considered at once. In both cases, the process of coloring changes most of the assignments that were inserted to model the ϕ\phi-functions into identity assignments, that is, assignments of the form V←VV \leftarrow V. These identity assignments can all be deleted.

Storage savings are especially noticeable for arrays. If optimization does not perturb the order of the first two statements in Figure 7, then arrays A8A_8 and A9A_9 can be assigned the same color and, hence, can share the same storage. The array A9A_9 is then assigned an Update from an identically colored array. Such operations can be implemented inexpensively by assigning to just one component if the arrays share storage. In particular, the actual operation performed by HiddenUpdate is always of this form.

11^{11}A conditional branch can be deleted by transforming it to an unconditional branch to any one of its prior targets.

            
              while (...) do    while (...) do    while (...) do
    W₃ ← φ(W₀, W₂)
    V₃ ← φ(V₀, V₂)
read V
read V_1
W \leftarrow V + W   W_1 \leftarrow V_1 + W_3
V \leftarrow 6      V_2 \leftarrow 6
W \leftarrow V + W   W_2 \leftarrow V_2 + W_1
end               end               end
(a)                (b)                (c)
            
          

Fig 18. Program that really uses two instances for a variable after code motion. (a) Source program; (b) unoptimized SSA form; (c) result of code motion.

8 ANALYSIS AND MEASUREMENTS

The number of nodes that contain ϕ\phi-functions for a variable V\mathbf{V} is a function of the program control flow structure and the assignments to V\mathbf{V}. Program structure alone determines dominance frontiers and the number of control dependences. It is possible that dominance frontiers may be larger than necessary for computing ϕ\phi-function locations for some programs, since the actual assignments are not taken into account. In this section we prove that the size of the dominance frontiers is linear in the size of the program when control flow branching is restricted to if-then-else constructs and while-do loops. (We assume that expressions and predicates perform no internal branching.) Such programs can be described by the grammar given in Figure 19. We also give experimental results that suggest that the behavior is linear for actual programs.

THEOREM 4. For programs comprised of straight-line code, if-then-else, and while-do constructs, the dominance frontier of any CFG node contains at most two nodes.

PROOF. Consider a top-down parse of a program using the grammar shown in Figure 19. Initially, we have a single ⟨program⟩\langle \text{program} \rangle node in the parse tree and a control flow graph CFGCFG with two nodes and one edge: Entry \rightarrow Exit. The initial dominance frontiers are DF(Entry)=∅=DF(Exit)DF(\text{Entry}) = \varnothing = DF(\text{Exit}). For each production, we consider the associated changes to CFGCFG and to the dominance frontiers of nodes. When a production expands a nonterminal parse tree node SS, a new subgraph is inserted into CFGCFG in place of SS. In this new subgraph, each node corresponds to a symbol on the right-hand side of the production.

We will show that applying any of the productions preserves the following invariants:

—Each CFGCFG node SS corresponding to an unexpanded ⟨statement⟩\langle \text{statement} \rangle symbol has at most one node in its dominance frontier.

            
              <program> ::= <statement>
<statement> ::= <statement><statement>
<statement> ::= if <predicate>
    then <statement>
    else <statement>
<statement> ::= while <predicate>
    do <statement>
<statement> ::= <variable> ← <expression>
            
          
Figure 19
Grammar for control structures.

—Each CFGCFG node TT corresponding to a terminal symbol has at most two nodes in its dominance frontier.

We consider the productions in turn.

(1) This production adds a CFGCFG node SS and edges Entry →\rightarrow SS →\rightarrow Exit, yielding DF(S)={Exit}DF(S) = \{\textbf{Exit}\}.

(2) When this production is applied, a CFGCFG node SS is replaced by two nodes S1S_1 and S2S_2. Edges previously entering and leaving SS now enter S1S_1 and leave S2S_2. A single edge is inserted from S1S_1 to S2S_2. Although the control flow graph has changed, consider how this production affects the dominator tree: Nodes S1S_1 and S2S_2 dominate all nodes that were dominated by SS; additionally, S1S_1 dominates S2S_2. Thus, we have DF(S1)=DF(S)=DF(S2)DF(S_1) = DF(S) = DF(S_2).

(3) When this production is applied, a CFGCFG node SS is replaced by nodes TifT_{if}, SthenS_{then}, SelseS_{else}, and TendifT_{endif}. Edges previously entering and leaving SS now enter TifT_{if} and leave TendifT_{endif}. Edges are inserted from TifT_{if} to both SthenS_{then} and SelseS_{else}; edges are also inserted from SthenS_{then} and SelseS_{else} to TendifT_{endif}. In the dominator tree, TifT_{if} and TendifT_{endif} both dominate all nodes that were dominated by SS. Additionally, TifT_{if} dominates SthenS_{then} and SelseS_{else}. By the argument made for production (2), we have DF(Tif)=DF(S)=DF(Tendif)DF(T_{if}) = DF(S) = DF(T_{endif}). Now consider nodes SthenS_{then} and SelseS_{else}. From the definition of a dominance frontier, we obtain DF(Sthen)=DF(Selse)={Tendif}DF(S_{then}) = DF(S_{else}) = \{T_{endif}\}.

(4) When this production is applied, a CFGCFG node SS is replaced by nodes TwhileT_{while} and SdoS_{do}. All edges previously associated with node SS are now associated with node TwhileT_{while}. Edges are inserted from TwhileT_{while} to SdoS_{do} and from SdoS_{do} to TwhileT_{while}. Node TwhileT_{while} dominates all nodes that were dominated by node SS. Additionally, TwhileT_{while} dominates SdoS_{do}. Thus, we have DF(Twhile)=DF(S)∪{Twhile}DF(T_{while}) = DF(S) \cup \{T_{while}\} and DF(Sdo)={Twhile}DF(S_{do}) = \{T_{while}\}.

(5) After application of this production, the new control flow graph is isomorphic to the old graph. □\Box

Corollary 2. For programs comprised of straight-line code, if-then-else, and while-do constructs, every node is control dependent on at most two nodes.

Proof. Consider a program PP composed of the allowed constructs and its associated control flow graph CFGCFG. The reverse control flow graph RCFGRCFG is

Table I. Summary Statistics of Our Experiment

            
              Package name  Statements in all procedures  Statements per procedure  Description
Min  Median  Max
EISPACK  7,034  22  89  327  Dense matrix eigenvectors and values
FLO52  2,054  9  54  351  Flow past an airfoil
SPICE  14,093  8  43  753  Circuit simulation
Totals  23,181  8  55  753  221 FORTRAN procedures
            
          

itself a structured control flow graph for some program P′P'. For all YY in RCFG,DF(Y)RCFG, DF(Y) contains at most two nodes by Theorem 4. By Corollary 1, YY is then control dependent on at most two nodes. □

Unfortunately, these linearity results do not hold for all program structures. In particular, consider the nest of repeat-until loops illustrated in Figure 5. For each loop, the dominance frontier of the entrance to that loop includes each of the entrances to surrounding loops. For nn nested loops, this leads to a dominance frontier mapping whose total size is Θ(n2)\Theta(n^2), yet each variable needs at most O(n)O(n) ϕ\phi-functions. Most of the dominance frontier mapping is not actually used in placing ϕ\phi-functions, so it seems that the computation of dominance frontiers might take excessive time with respect to the resulting number of actual ϕ\phi-functions. We therefore wish to measure the number of dominance frontier nodes as a function of program size over a diverse set of programs.

We implemented our algorithms for constructing dominance frontiers and placing ϕ\phi-functions in the PTRAN system, which already offered the required local data flow and control flow analysis [2]. We ran these algorithms on 61 library procedures from EISPACK [46] and 160 procedures from two "Perfect" [39] benchmarks. Some summary statistics of these procedures are shown in Table I. These FORTRAN programs were chosen because they contain irreducible intervals and other unstructured constructs. As the plot in Figure 20 shows, the size of the dominance frontier mapping appears to vary linearly with program size. The ratio of these sizes ranged from 0.6 (the Entry node has an empty dominance frontier) to 2.1.

For the programs we tested, the plot in Figure 21 shows that the number of ϕ\phi-functions is also linear in the size of the original program. The ratio of these sizes ranged from 0.5 to 5.2. The largest ratio occurred for a procedure of only 12 statements, and 95 percent of the procedures had a ratio under 2.3. All but one of the remaining procedures contained fewer than 60 statements. Finally, the plot in Figure 22 shows that the size of the control dependence graph is linear in the size of the original program. The ratio of these sizes ranged from 0.6 to 2.4, which is very close to the range of ratios for dominance frontiers.

The ratio avrgDFavrgDF (defined by (7) in Section 5.1) measures the cost of placing ϕ\phi-functions relative to the number of assignments in the resulting

Fig. 20. Size of dominance frontier mapping versus number of program statements.

Fig. 21. Number of ϕ\phi-functions versus number of program statements.

SSA form program. This ratio varied from 1 to 2, with median 1.3. There was no correlation with program size.

We also measured the expansion Atot/AorigA_{tot}/A_{orig} in the number of assignments when translating to SSA form. This ratio varied from 1.3 to 3.8. Finally, we measured the expansion Mtot/MorigM_{tot}/M_{orig} in the number of mentions (assignments or uses) of variables when translating to SSA form. This ratio varied from 1.6 to 6.2. For both of these ratios, there was no correlation with program size.

Figure 22
Size of control dependence graph versus number of program statements.

9 DISCUSSION

9.1 Summary of Algorithms and Time Bounds

The conversion to SSA form is done in three steps:

(1) The dominance frontier mapping is constructed from the control flow graph CFGCFG (Section 4.2). Let CFGCFG have NN nodes and EE edges. Let DFDF be the mapping from nodes to their dominance frontiers. The time to compute the dominator tree and then the dominance frontiers in CFGCFG is O(E+∑X∣DF(X)∣)O(E + \sum_X |DF(X)|).

(2) Using the dominance frontiers, the locations of the ϕ\phi-functions for each variable in the original program are determined (Section 5.1). Let AtotA_{tot} be the total number of assignments to variables in the resulting program, where each ordinary assignment statement LHS←RHSLHS \leftarrow RHS contributes the length of the tuple LHSLHS to AtotA_{tot}, and each ϕ\phi-function contributes 1 to AtotA_{tot}. Placing ϕ\phi-functions contributes O(Atot×avrgDF)O(A_{tot} \times avrgDF) to the overall time, where avrgDFavrgDF is the weighted average (7) of the sizes ∣DF(X)∣|DF(X)|.

(3) The variables are renamed (Section 5.2). Let MtotM_{tot} be the total number of mentions of variables in the resulting program. Renaming contributes O(Mtot)O(M_{tot}) to the overall time.

To state the time bounds in terms of fewer parameters, let the overall size RR of the original program be the maximum of the relevant numbers: NN nodes, EE edges, AorigA_{orig} original assignments to variables, and MorigM_{orig} original mentions of variables. In the worst case, avrgDF=Ω(N)=Ω(R)avrgDF = \Omega(N) = \Omega(R), and kk ordinary assignments can require Ω(kR)\Omega(kR) insertions of ϕ\phi-functions. Thus, Atot=Ω(R2)A_{tot} = \Omega(R^2) at worst. In the worst case, a ϕ\phi-function has Ω(R)\Omega(R) operands. Thus, Mtot=Ω(R3)M_{tot} = \Omega(R^3) at worst. The one-parameter worst-case time bounds are thus O(R2)O(R^2) for finding dominance frontiers and O(R3)O(R^3) for translation to SSA form.

However, the data in Section 8 suggest that the entire translation to SSA form will be linear in practice. The dominance frontier of each node in CFGCFG is small, as is the number of ϕ\phi-functions added for each variable. In effect, avrgDFavrgDF is constant, Atot=O(Aorig)A_{tot} = O(A_{orig}), and Mtot=O(Morig)M_{tot} = O(M_{orig}). The entire translation process is effectively O(R)O(R).

Control dependences are read off from the dominance frontiers in the reverse graph RCFGRCFG (Section 6) in time O(E+size⁡(RDF))O(E + \operatorname{size}(RDF)). Since the size of RDFRDF is the size of the output of the control dependence calculation, this algorithm is linear in the size of the output. The only quadratic behavior is caused by the output being Ω(R2)\Omega(R^2) in the worst case. The data in Section 8 suggest that the control dependence calculation is effectively O(R)O(R).

9.2 Related Work

Minimal SSA form is a refinement of Shapiro and Saint's [45] notion of a pseudoassignment. The pseudoassignment nodes for VV are exactly the nodes that need ϕ\phi-functions for VV. A closer precursor [22] of SSA form associated new names for VV with pseudoassignment nodes and inserted assignments from one new name to another. Without explicit ϕ\phi-functions, however, it was difficult to manage the new names or reason about the flow of values.

Suppose the control flow graph CFGCFG has NN nodes and EE edges for a program with QQ variables. One algorithm [41] requires O(Eα(E,N))O(E\alpha(E, N)) bit vector operations (where each vector is of length QQ) to find all of the pseudoassignments. A simpler algorithm [43] for reducible programs computes SSA form in time O(E×Q)O(E \times Q). With lengths of bit vectors taken into account, both of these algorithms are essentially O(R2)O(R^2) on programs of size RR, and the simpler algorithm sometimes inserts extraneous ϕ\phi-functions. The method presented here is O(R3)O(R^3) at worst, but Section 8 gives evidence that it is O(R)O(R) in practice. The earlier O(R2)O(R^2) algorithms have no provision for running faster in typical cases; they appear to be intrinsically quadratic.

For CFGCFG with NN nodes and EE edges, previous general control dependence algorithms [24] can take quadratic time in (N+E)(N + E). This analysis is based on the worst-case Ω(N)\Omega(N) depth of the (post)dominator tree [24, p. 326]. Section 6 shows that control dependences can be determined by computing dominance frontiers in the reverse graph RCFGRCFG. In general, our approach can also take quadratic time, but the only quadratic behavior is caused by the output being Ω(N2)\Omega(N^2) in the worst case. In particular, suppose a program is comprised only of straight-line code, if-then-else, and while-do constructs. By Corollary 2, our algorithm computes control dependences in linear time. We obtain a better time bound for such programs because our algorithm is based on dominance frontiers, whose sizes are not necessarily related to the depth of the dominator tree. For languages that offer only these constructs, control dependences can also be computed from the parse tree [28] in linear time, but our algorithm is more robust. It handles all cases in quadratic time and typical cases in linear time.

9.3 Conclusions

Previous work has shown that SSA form and control dependences can support powerful code optimization algorithms that are highly efficient in terms of time and space bounds based on the size of the program after translation to the forms. We have shown that this translation can be performed efficiently, that it leads to only a moderate increase in program size, and that applying the early steps in the SSA translation to the reverse graph is an efficient way to compute control dependences. This is strong evidence that SSA form and control dependences form a practical basis for optimization.

Acknowledgments

We would like to thank Fran Allen for early encouragement and Fran Allen, Trina Avery, Julian Padget, Bob Paige, Tom Reps, Randy Scarborough, and Jin-Fan Shaw for various helpful comments. We are especially grateful to the referees, whose thorough reports led to many improvements in this paper.

References

  1. Aho, A. V., Sethi, R., and Ullman, J. D. Compilers: Principles, Techniques, and Tools. Addison-Wesley, Reading, Mass , 1986.
  2. Allen, F. E., Burke, M., Charles, P., Cytron, R., and Ferrante, J An overview of the PTRAN analysis system for multiprocessing. J. Parallel Distrib. Comput. 5, (Oct. 1988), 617–640.
  3. Allen, J. R Dependence analysis for subscripted variables and its application to program transformations. Ph.D. thesis, Department of Computer Science, Rice Univ., Houston, Tex., Apr. 1983.
  4. Allen, J R , and Johnson, S Compiling C for vectorization, parallelization and inline expansion. In Proceedings of the SIGPLAN '88 Symposium on Compiler Construction. SIGPLAN Not. (ACM) 23, 7 (June 1988), 241–249
  5. Alpern, B , Wegman, M. N., and Zadeck, F. K. Detecting equality of values in programs In Conference Record of the 15th ACM Symposium on Principles of Programming Languages (Jan. 1988), ACM, New York, pp. 1–11.
  6. Ballance, R. A., MacCabe, A. B., and Ottensstein, K. J. The program dependence web: A representation supporting control-, data-, and demand driven interpretation of languages. In Proceedings of the SIGPLAN 90 Symposium on Compiler Construction. SIGPLAN Not. (ACM) 25, 6 (June 1990), 257–271.
  7. Banning, J. B. An efficient way to find the side effects of procedure calls and the aliases of variables. In Conference Record of the 6th ACM Symposium on Principles of Programming Languages (Jan. 1979) ACM, New York, pp. 29–41
  8. Barth, J. M. An interprocedural data flow analysis algorithm. In Conference Record of the 4th ACM Symposium on Principles of Programming Languages (Jan. 1977) ACM, New York: pp. 119–131.
  9. Burke, M. An interval-based approach to exhaustive and incremental interprocedural data flow analysis. ACM Trans. Program Lang. Syst. 12, 3 (July 1990), 341–395.
  10. Burke, M., and Cytron, R. Interprocedural dependence analysis and parallelization. In Proceedings of the SIGPLAN 86 Symposium on Compiler Construction SIGPLAN Not (ACM) 21, 7 (June 1986), 162–175. ACM Transactions on Programming Languages and Systems, Vol. 13, No. 4, October 1991 Static Single Assignment Form and the Control Dependence Graph • 489
  11. Cartwright, R., and Felleisen, M. The semantics of program dependence. In *Proceedings of the SIGPLAN'89 Symposium on Compiler Construction. SIGPLAN Not. (ACM)* 24, 7 (July 1989), 13–27.
  12. Chaitin, G. J. Register allocation and spilling via graph coloring. In *Proceedings of the SIGPLAN '82 Symposium on Compiler Construction. SIGPLAN Not. (ACM)* 17, 6 (June 1982), 98–105.
  13. Chaitin, G. J., Auslander, M. A., Chandra, A. K., Cocke, J., Hopkins, M. E., and Markstein, P. W. Register allocation via coloring. *Computer Lang.* 6 (1981), 47–57.
  14. Chase, D. R. Safety considerations for storage allocation optimizations. In *Proceedings of the SIGPLAN'88 Symposium on Compiler Construction. SIGPLAN Not. (ACM)* 23, 7 (June 1988), 1–10.
  15. Chase, D. R., Wegman, M. and Zadeck, F. K. Analysis of pointers and structures. In *Proceedings of the SIGPLAN 90 Symposium on Compiler Construction. SIGPLAN Not. (ACM)* 25, 6 (June 1990), 296–310.
  16. Choi, J., Cytron, R., and Ferrante, J. Automatic construction of sparse data flow evaluation graphs. In *Conference Record of the 18th ACM Symposium on Principles of Programming Languages* (Jan. 1991). ACM, New York, pp. 55–66.
  17. Chow, F. C. A portable machine-independent global optimizer—Design and measurements. Ph.D. thesis and Tech. Rep. 83-254, Computer Systems Laboratory, Stanford Univ., Stanford, Calif., Dec. 1983.
  18. Chow, F. C., and Hennessy, J. L. The priority-based coloring approach to register allocation. *ACM Trans. Program. Lang. Syst.* 12, 4 (Oct. 1990), 501–536.
  19. Cooper, K. D. Interprocedural data flow analysis in a programming environment. Ph.D. thesis, Dept. of Mathematical Sciences, Rice Univ., Houston, Tex., 1983.
  20. Cytron, R., and Ferrante, J. An improved control dependence algorithm. Tech. Rep. RC 13291, IBM Corp., Armonk, N.Y., 1987.
  21. Cytron, R., and Ferrante, J. What's in a name? In *Proceedings of the 1987 International Conference on Parallel Processing* (Aug. 1987), pp. 19–27.
  22. Cytron, R., Lowry, A., and Zadeck, F. K. Code motion of control structures in high-level languages. In *Conference Record of the 13th ACM Symposium on Principles of Programming Languages* (Jan. 1986). ACM, New York, pp. 70–85.
  23. Dennis, J. B. First version of a data flow procedure language. Tech. Rep. Comput. Struc. Group Memo 93 (MAC Tech. Memo 61), MIT, Cambridge, Mass., May 1975.
  24. Ferrante, J., Ottenstein, K. J., and Warren, J. D. The program dependence graph and its use in optimization. *ACM Trans. Program. Lang. Syst.* 9, 3 (July 1987), 319–349.
  25. Giegerich, R. A formal framework for the derivation of machine-specific optimizers. *ACM Trans. Program. Lang. Syst.* 5, 3 (July 1983), 478–498.
  26. Harel, D. A linear time algorithm for finding dominators in flow graphs and related problems. In *Proceedings of the 17th ACM Symposium on Theory of Computing* (May 1985). ACM, New York, pp. 185–194.
  27. Horwitz, S., Pfeiffer, P., and Reps, T. Dependence analysis for pointer variables. In *Proceedings of the SIGPLAN 89 Symposium on Compiler Construction. SIGPLAN Not. (ACM)* 24, 7 (June 1989).
  28. Horwitz, S., Prins, J., and Reps, T. Integrating non-interfering versions of programs. *ACM Trans. Program. Lang. Syst.* 11, 3 (July 1989), 345–387.
  29. Jones, N. D., and Muchnick, S. S. Flow analysis and optimization of LISP-like structures. In *Program Flow Analysis*, S. S. Muchnick and N. D. Jones, Eds. Prentice-Hall, Englewood Cliffs, N.J., 1981, chap. 4, pp. 102–131.
  30. Kennedy, K. W. Global dead computation elimination. Tech. Rep. SETL Newsl. 111, Courant Institute of Mathematical Sciences, New York Univ., New York, N.Y., Aug. 1973.
  31. Kennedy, K. W. A survey of data flow analysis techniques. In *Program Flow Analysis*, S. S. Muchnick and N. D. Jones, Eds. Prentice-Hall, Englewood Cliffs, N.J., 1981.
  32. Kuck, D. J. *The Structure of Computers and Computations.* Wiley, New York, 1978.
  33. Larus, J. R. Restructuring symbolic programs for concurrent execution on multiprocessors. Tech. Rep. UCB/CSD 89/502, Computer Science Dept., Univ. of California at Berkeley, Berkeley, Calif., May 1989. ACM Transactions on Programming Languages and Systems, Vol. 13, No. 4, October 1991. 490 • Ron Cytron et al.
  34. Larus, J. R., and Hilfinger, P. N. Detecting conflicts between structure accesses. In *Proceedings of the ACM SIGPLAN 88 Symposium on Compiler Construction*. SIGPLAN Not. 23, 7 (July 1988), 21–34.
  35. Lengauer, T., and Tarjan, R. E. A fast algorithm for finding dominators in a flowgraph. *ACM Trans. Program. Lang. Syst.* 1, 1 (July 1979), 121–141.
  36. Muchnick, S. S., and Jones, N. D., Eds. *Program Flow Analysis*. Prentice-Hall, Englewood Cliffs, N.J., 1981.
  37. Myers, E. W. A precise interprocedural data flow algorithm. In *Conference Record of the 8th ACM Symposium on Principles of Programming Languages* (Jan. 1981). ACM, New York, pp. 219–230.
  38. Ottenstein, K. J. Data-flow graphs as an intermediate form. Ph.D. thesis, Dept. of Computer Science, Purdue Univ., W. Lafayette, Ind., Aug. 1978.
  39. Pointer, L. Perfect report: 1. Tech. Rep CSR D 896, Center for Supercomputing Research and Development, Univ. of Illinois at Urbana-Champaign, Urbana, Ill., July 1989.
  40. Reif, J. H., and Lewis, H. R. Efficient symbolic analysis of programs. *J. Comput. Syst. Sci.* 32, 3 (June 1986), 280–313.
  41. Reif, J. H., and Tarjan, R. E. Symbolic program analysis in almost linear time. *SIAM J. Comput.* 11, 1 (Feb. 1982), 81–93.
  42. Rosen, B. K. Data flow analysis for procedural languages. *J. ACM* 26, 2 (Apr. 1979), 322–344.
  43. Rosen, B. K., Wegman, M. N., and Zadeck, F. K. Global value numbers and redundant computations. In *Conference Record of the 15th ACM Symposium on Principles of Programming Languages* (Jan. 1988). ACM, New York, pp. 12–27.
  44. Ruggieri, C., and Murtagh, T. P. Lifetime analysis of dynamically allocated objects. In *Conference Record of the 15th ACM Symposium on Principles of Programming Languages* (Jan. 1988). ACM, New York, pp. 285–293.
  45. Shapiro, R. M., and Saint, H. The representation of algorithms. Tech. Rep. CA-7002-1432, Massachusetts Computer Associates, Feb. 1970.
  46. Smith, B. T., Boyle, J. M., Dongarra, J. J., Garbow, B. S., Ikebe, Y., Klema, V. C., and Moler, C. B. *Matrix Eigensystem Routines – Eispack Guide*. Springer-Verlag, New York, 1976.
  47. Tarjan, R. E. Finding dominators in directed graphs. *SIAM J. Comput.* 3, 1 (1974), 62–89.
  48. Wegbreit, B. Property extraction in well-founded property sets. *IEEE Trans. Softw. Eng.* SE-1, 3 (Sept. 1975), 270–285.
  49. Wegman, M. N., and Zadeck, F. K. Constant propagation with conditional branches. In *Conference Record of the 12th ACM Symposium on Principles of Programming Languages* (Jan.). ACM, New York, pp. 291–299.
  50. Wegman, M. N., and Zadeck, F. K. Constant propagation with conditional branches. *ACM Trans. Program. Lang. Syst.* To be published.
  51. Wolfe, M. J. Optimizing supercompilers for supercomputers. Ph.D. thesis, Dept of Computer Science, Univ. of Illinois at Urbana-Champaign, Urbana Ill., 1982.
  52. Yang, W., Horwitz, S., and Reps, T. Detecting program components with equivalent behaviors. Tech. Rep. 840. Dept. of Computer Science, Univ. of Wisconsin at Madison, Madison, Apr. 1989. Received July 1989; revised March 1991; accepted March 1991 ACM Transactions on Programming Languages and Systems, Vol 13, No 4, October 1991.

Read off the page by a vision model reading the page images using gpt-5.