Lecture notes 20210607 SSA 2

Review: SSA Construction

We introduced control flow graphs (CFG) and statis single assignemnt form (SSA form) last time, which is already widely used in modern compiler optimization. We also introduced some basic ideas of constructing SSA from a normal CFG. That is, inserting PHI instructions according to dominance frontiers (DF).
Consider the following CFG, point out all pairs of nodes u, v such that u dominates v.
       r --> A
       A --> BC
       B --> A
Consider the following CFG, point out all pairs of nodes u, v such that u dominates v (and strictly dominates v, respectively).
       r --> A
       A --> BC
       B --> D
       C --> DE
       D --> AE
Consider the following CFG, point out all pairs of nodes u, v such that u strictly dominates v.
       r --> AE
       A --> BG
       B --> CD
       C --> EF
       D --> FG
       E --> H
       F --> HI
       G --> I
       H --> J
       I --> J
Consider the following CFG, compute DF(A), DF(B), DF(C) and DF(D).
       r --> A
       A --> BC
       B --> D
       C --> DE
       D --> AE
Consider the following CFG, compute DF(A) and DF(B)
       r --> AE
       A --> BG
       B --> CD
       C --> EF
       D --> FG
       E --> H
       F --> HI
       G --> I
       H --> J
       I --> J
Intuitively, DF(n) is the boundary of those nodes dominated by n.

Inserting PHI commands

Last time, we mentioned that we can compute the location of inserting PHI commands by computing iterated dominance frontier, theoretically. In practice, DF(n) are pre-computed for every node n and insertion locations are computed using a working list W and flags F (to avoid multiple insertions). The following is the algorithm description.
  • For every variable v in the original program
  • (1) Let F be the empty set
  • (2) Let W be the empty set
  • (3) For every basic block B in the CFG
  • (3.1) Check whether B contains a definition of v
  • (3.2) If yes, let W be the union of W and {B}.
  • (4) While W is nonempty, execute the following operations
  • (4.1) Let X be an element of W
  • (4.2) Remove X from W
  • (4.3) For every Y in DF(X) but not in F
  • (4.3.1) Add a PHI-command for v at the entry of Y
  • (4.3.2) Add Y to F
  • (4.3.3) If Y itself does not contain a definition of v, add Y to W
In short, the worklist of nodes W is used to record definition points that the algorithm has not yet processed, i.e., it has not yet inserted PHI-commands at their dominance frontiers. Because a PHI-command is itself a definition of v, it may require further PHI-functions to be inserted. This is the cause of node insertions into the worklist W during iterations of the inner loop in the algorithm above. Effectively, we compute the iterated dominance frontier on the fly. The set F is used to avoid repeated insertion of PHI-comamnds on a single block. Dominance frontiers of distinct nodes may intersect, but once a PHI-command for a particular variable has been inserted at a node, there is no need to insert another.
For inserting PHI commands, the only leftover problem is how to compute dominance frontiers. Naively, we can traverse CFG from the root and compute DF(n) for every node n according to its definition. We will introduce a more efficient algorithm for it later today.
Remark 1. For each variable use in a PHI-function, it is conventional to treat them as if the use actually occurs on the corresponding incoming edge or at the end of the corresponding predecessor node. If we follow this convention, then we can claim that SSA forms always have the following nice property: the single definition that reaches each use dominates that use.
Remark 2. Although we write consecutive PHI-commands sequentially, but their behavior is actually parallel.

Renaming variables

Intuitively, every use of a variable v should be renamed together with the closest definition of v before it. Moreover, this definition should dominate the use, or else a PHI-command will be inserted. This critical observation inspires us to complete variable renaming efficiently with the help of dominance relation.

Dominance trees

The main idea is, the dominance relation of a CFG has a tree-like structure. Consider three node u, v and w and suppose that both u and v strictly dominates w. Then either u dominates v or v dominates u according to the definition of dominance relation.
The immediate dominator or ``idom'' of a node n is the unique node that strictly dominates n but does not strictly dominate any other node that strictly dominates n. All nodes in a CFG but the entry node have immediate dominators. A dominance tree is a tree where the children of each node are those nodes it immediately dominates. For example,
       CFG:

       A --> BC
       B --> D
       C --> D
       D --> E

       Dom tree:

       A --> BCD
       D --> E
Compute the dominance tree of the following CFG.
       A --> B
       B --> CD
       C --> B
       D --> E

Renaming variables using dominance tree

We can complete variable renaming via a depth first search on a dominance tree.
  • When entering a basic block n in DFS preorder traversal of the dominance tree
  • (1) For each instruction i in n (in their order)
  • (1.1) For each v used in i, when i is not a PHI-command
  • (1.1.1) UpdateReachingDef(v, i)
  • (1.1.2) Replace this use of v in i by v.RD
  • (1.2) For the variable v defined by i
  • (1.2.1) UpdateReachingDef(v, i)
  • (1.2.2) Create fresh variable v'
  • (1.2.3) Replace this definition of v by v' in i
  • (1.2.4) Let v'.PD be v.RD
  • (1.2.5) Let v.RD be v'
  • (2) For each PHI-command i in n's successors
  • (2.1) For each v used in i
  • (2.1.1) UpdateReachingDef(v, i)
  • (2.1.2) Replace this use of v in i by v.RD
In this algorithm, for new created variables v', v'.PD represents a linked-list-like structure indicating the previous available definition (PD) of v''s original variable. For an original program variable v, v.RD is the reaching definition (RD) of v that can be used on the current location. It will be updated by the following algorithm (the algorithm of UpdateReachingDef(v,i)):
  • (1) Let r be v.RD
  • (2) While the location of r's definition does not dominates i
  • (2.1) Let r be r.PD
  • (3) Let v.RD be r

Computing dominance frontiers by dominance tree

Using dominance tree, we can compute dominance frontiers of different nodes in an more efficient approach.
  • (1) For every node n in the CFG
  • (1.1) Let DF(n) be the empty set
  • (2) For every edge from X to Y in the CFG
  • (2.1) Let n be X
  • (2.2) While n does not strictly dominates Y
  • (2.2.1) Add Y to DF(n)
  • (2.2.2) Let n be n's immediate dominator

Desctructing SSA form

When freshly constructed, an SSA code is conventional (we will explain that later) and its destruction is straightforward: one simply has to rename all PHI-related variables (source and destination operands of the same Phi-function) into a unique representative variable. Then, each PHI-function should have syntactically identical names for all its operands, and thus can be removed to coalesce the related live-ranges.
Here, we say that x and y are PHI-related to one another if they are referenced by the same PHI-command, i.e., if x and y are either parameters or defined by the PHI-command. We refer to a set of PHI-related variables as a PHI-web, i.e. a PHI-web is an equivalence class of the reflexive transitive closure of the PHI-related relation. The PHI-webs discovery algorithm is straightforward and efficient based on the union-find pattern:
  • (1) For each variable v
  • (1.1) Let phiweb(v) be the singleton set containing v
  • (2) For each PHI-command a0 = PHI(a1, a2, ..., an)
  • (2.1) For i in 1, 2, 3, ..., n
  • (2.1.1) Union phiweb(a0) and phiweb(ai)
We call an SSA code conventional, if all variables of a PHI-web have non-overlapping live-ranges.
(* 2021-06-07 09:42 *)