Lesson 21.4 — Beyond trees: DAG covering, NP-completeness and near-optimal selection¶
Techniques: tree decomposition and greedy DAG matching; NOLTIS (Koes–Goldstein); PBQP and ILP formulations (Eckstein–König–Scholz, Ebner et al.) · Prerequisites: Lesson 21.2 (dynamic programming), Lesson 8.3 (expression DAGs) · Time: 4–6 hours
Real IR is not a forest of trees. In x = a*b + c; y = a*b + d the product has two users, and in a load/store pair on p + 8 the address has two users. The value-numbered DAG of a block (Lesson 8.3), LLVM's SelectionDAG and SSA use–def graphs are all DAGs. A shared node can be computed once into a register, which saves recomputation but stops any tile from covering it. Or it can be recomputed inside each user's tile, which duplicates work but lets both users use big instructions. Which is better depends on the costs. On the toy ISA of this chapter, sharing wins for a multiplication (cost 7 instead of 8) and duplication wins for an address (cost 5 instead of 6). Finding the best mix is NP-complete (Theorem 21.4.4). Real selectors therefore use heuristics: cut the DAG into trees (the classic approach and lcc's), fold greedily with a single-use rule (LLVM), or run the DP twice with a cost model for sharing (NOLTIS). Optimum solvers exist for smaller problems: PBQP and integer linear programming.
1. Problem and motivation¶
The input is a DAG \(D\) (a basic block's expression DAG, or LLVM's SelectionDAG) with root nodes that must be computed (stores, the block's live-out values), and a tile grammar \(G\). The output is a set of tile instances that computes all roots, where every value a tile needs is produced by another chosen tile, and whose total cost is minimum. Unlike on trees, one node can be covered by several tiles. Lesson 21.5 shows LLVM's version: the SelectionDAG selector matches patterns on a DAG and decides greedily, node by node, whether an operand may be folded.
Tree decomposition and greedy DAG matching¶
The oldest answer is to avoid the problem. Split the DAG at every node with more than one user, compute that node into a register (a temporary), and tile each resulting tree optimally with the DP of Lesson 21.2. The Dragon book presents code generation for basic blocks this way [Dragon2, §8.5, §8.9], and lcc does it with undag [LCC-Src]. The greedy DAG matchers of production compilers are a refinement of the same idea: match patterns directly on the DAG, but allow a pattern to swallow a shared node only in special cases, such as cheap address arithmetic.
NOLTIS¶
Koes and Goldstein asked how close a linear-time algorithm can get to the optimum on DAGs. Their NOLTIS ("near-optimal linear-time instruction selection") runs the tree DP on the DAG as if shared nodes could be duplicated freely. Where that leads to recomputing a shared node, it compares the cost of the recomputation with the cost of computing the node once, fixes the nodes where sharing is cheaper, and runs the DP again [KG08]. They report that on their benchmarks NOLTIS finds an optimal tiling (checked against an integer-programming optimum) in 99.7 % of the cases.
PBQP and ILP formulations¶
Eckstein, König and Scholz formulated instruction selection on the SSA graph of a whole function as a partitioned Boolean quadratic problem (PBQP): every node chooses one rule, and edge cost matrices charge for incompatible choices [EKS03]. Ebner et al. generalized it to patterns with several results (DAG patterns) and implemented it in LLVM, reporting code quality close to optimal with acceptable compile times [EBSKWK08]. Integer linear programming (ILP) and constraint programming formulations give true optima at a higher price. Blindell's universal instruction selection models selection, global code motion and block placement together as one constraint problem [Bli16, Ch. 4–5].
2. Definitions and algorithms¶
Definition 21.4.1 (Expression DAG)
An expression DAG is a finite directed acyclic graph \(D = (V, E)\) whose nodes are labelled with operators of \(\Sigma\) (Definition 21.1.1), with ordered children \(\mathrm{child}_1(v), \dots, \mathrm{child}_{\mathrm{ar}(v)}(v)\), together with a set \(\mathrm{Roots} \subseteq V\) of nodes that must be computed. A node is shared if it has more than one parent. A tree is a DAG without shared nodes and with one root.
Definition 21.4.2 (DAG cover)
A tile instance is a pair \((v, r)\) of a node and a rule \(r = A \to \pi\) whose pattern matches at \(v\) (Definition 21.1.3, reading the children of the DAG); it produces \(A\) at \(v\). A set \(X\) of tile instances is a cover of \(D\) if (i) for every root \(v\) there is an instance \((v, r) \in X\) producing the start nonterminal (or the nonterminal the root requires), and (ii) for every instance \((v, r) \in X\) and every operand \((u, B)\) of \(r\) at \(v\), there is an instance \((u, r') \in X\) producing \(B\) (for a chain rule \(A \to B\), an instance producing \(B\) at \(v\) itself). The cost is \(\sum_{(v, r) \in X} c(r)\): every instance is paid once, however many tiles use its value. Nodes may lie inside several instances' patterns (overlap, which means recomputation). \(\mathrm{OPT}(D)\) is the minimum cost of a cover.
Definition 21.4.3 (The DAG covering problem)
DAG-COVER: given a grammar \(G\), a DAG \(D\) and an integer \(K\), is there a cover of \(D\) of cost at most \(K\)?
Theorem 21.4.4 (DAG covering is NP-complete)
DAG-COVER is NP-complete, even for one fixed grammar whose rule costs are 0 and 1.
Proof
Membership in NP. A cover contains at most \(\lvert V \rvert \cdot \lvert R \rvert\) instances, so it is a certificate of polynomial size. Conditions (i) and (ii) and the cost can be checked in polynomial time.
Hardness, by reduction from 3-SAT. Fix the grammar \(G_{\mathrm{SAT}}\) with operators \(\mathsf{X}/0\) and \(\mathsf{C}_s/3\) for each sign pattern \(s \in \{+, -\}^3\) (eight operators), nonterminals \(T, F, A, \mathsf{stmt}\), and rules \(T \to \mathsf{X}\) (cost 1), \(F \to \mathsf{X}\) (cost 1), \(A \to T\) (0), \(A \to F\) (0), and, for every \(s\) and every position \(i \in \{1, 2, 3\}\), a rule \(\mathsf{stmt} \to \mathsf{C}_s(Y_1, Y_2, Y_3)\) (cost 0) with \(Y_i = T\) if \(s_i = +\), \(Y_i = F\) if \(s_i = -\), and \(Y_j = A\) for \(j \ne i\). (That is \(4 + 24 = 28\) rules, fixed once and for all.) Given a formula with variables \(x_1, \dots, x_m\) and clauses \(c_1, \dots, c_\ell\), where each clause has three distinct variables and every variable occurs in some clause, build \(D\) with one node \(\mathsf{X}_j\) per variable and one root \(\mathsf{C}_{s(c)}(\mathsf{X}_{j_1}, \mathsf{X}_{j_2}, \mathsf{X}_{j_3})\) per clause, where \(s(c)\) records the signs of \(c\)'s literals. Set \(K = m\).
(⇒) Given a satisfying assignment \(\alpha\), take the instance \((\mathsf{X}_j, T \to \mathsf{X})\) if \(\alpha(x_j)\) is true and \((\mathsf{X}_j, F \to \mathsf{X})\) otherwise, the matching chain instance \(A \to T\) or \(A \to F\) at every \(\mathsf{X}_j\), and at each clause root the rule for a position \(i\) whose literal \(\alpha\) makes true. Its operand \(Y_i\) is exactly the nonterminal produced at that variable, and the others are \(A\), which is produced everywhere. This is a cover of cost \(m\).
(⇐) Every variable node is an operand of some clause root, so any cover contains at least one of \((\mathsf{X}_j, T \to \mathsf{X})\) and \((\mathsf{X}_j, F \to \mathsf{X})\) for every \(j\) (\(A\) can only be produced through \(T\) or \(F\)). So its cost is at least \(m\), and a cover of cost \(\le m\) contains exactly one of them per variable. Define $\alpha(x_j) = $ true iff the \(T\) instance is chosen. Each clause root has some instance \(\mathsf{stmt} \to \mathsf{C}_s(\dots)\) in the cover, whose position \(i\) requires \(T\) (or \(F\)) at that variable, and that is the variable's only choice, so the literal at position \(i\) is true under \(\alpha\). Every clause is satisfied.
The construction is polynomial, so 3-SAT \(\le_p\) DAG-COVER. Proebsting gives a similar reduction [Pro98]. For costs that include register pressure, the problem was already known to be NP-complete for one-register machines [BS76] and for DAGs with common subexpressions [AJU77].
The reduction on a two-clause formula
\((x_1 \lor \lnot x_2 \lor x_3) \land (\lnot x_1 \lor x_2 \lor x_3)\) gives variable nodes \(\mathsf{X}_1, \mathsf{X}_2, \mathsf{X}_3\) and roots \(\mathsf{C}_{+-+}(\mathsf{X}_1, \mathsf{X}_2, \mathsf{X}_3)\) and \(\mathsf{C}_{-++}(\mathsf{X}_1, \mathsf{X}_2, \mathsf{X}_3)\), with \(K = 3\). The assignment $x_1 = x_2 = $ true, $x_3 = $ false gives the cover \(\{(\mathsf{X}_1, T), (\mathsf{X}_2, T), (\mathsf{X}_3, F)\}\) plus chains to \(A\), the first root using position 1 (\(T\) at \(\mathsf{X}_1\)) and the second position 2 (\(T\) at \(\mathsf{X}_2\)). Its cost is 3.
Tree decomposition and greedy DAG matching¶
Algorithm 21.4.5 (Tree decomposition)
- Input: an expression DAG \(D\); a grammar \(G\) in which a value in a register can be read
as the leaf nonterminal (Tessera:
reg: TEMP). - Output: a cover of \(D\) (and its code).
- Precondition: \(G\) derives every node of every resulting tree.
- Postcondition: every cut node is the root of a tile instance and lies strictly inside no instance (its users read its register), and the cover is optimum among covers with that property (Proposition 21.4.7).
- Invariant: the trees are processed in a topological order of the DAG, so a shared node's register is written before any tree reads it.
function TreeDecomposition(D):
cut ← Roots ∪ { v | v has more than one parent }
for each v in cut, in topological order (children before parents):
T_v ← the tree rooted at v obtained by stopping at every other node of cut
(a stopped node u becomes the leaf TEMP t_u)
goal ← (v ∈ Roots and v is a statement) ? S : reg
Label(T_v); Reduce(T_v, goal) # Algorithms 21.2.3 and 21.2.4
if v is not a statement: record that t_u names the register of the result
Algorithm 21.4.6 (Greedy DAG matching with a fold rule)
- Input: an expression DAG \(D\); patterns ordered by priority (Algorithm 21.1.8's size, or LLVM's complexity).
- Output: a cover.
- Precondition: every node has a matching single-operator pattern.
- Postcondition: every node is either a tile root or covered by a tile of one of its users.
A shared node lies inside a tile only if the target's
Foldableallows it. - Invariant: nodes are selected from the roots downwards (reverse topological order). When a node is selected, all its users are already selected.
function GreedyDAG(D):
for each node v in reverse topological order:
if v is inside a tile already chosen for all of its users: continue
for each pattern π in priority order:
if MatchesDAG(π, v): choose (v, π); break
function MatchesDAG(π, v): # like Matches, but an inner pattern node may
... # swallow a DAG node u only if Foldable(u, v):
function Foldable(u, v):
return u has a single use # LLVM: IsProfitableToFold
and folding u into v creates no cycle # LLVM: IsLegalToFold
or u is cheap address arithmetic the target duplicates on purpose
Proposition 21.4.7 (What tree decomposition guarantees)
Algorithm 21.4.5 returns a valid cover. Among all covers in which every shared node is the root
of an instance producing reg and lies strictly inside no instance (every user reads the
shared value from its register), it is optimum. In general it is not optimum: there are DAGs
where recomputing a shared node is strictly cheaper.
Proof
Validity. Each tree \(T_v\) is covered by a derivation (Theorem 21.2.7), whose leaves are
either real leaves or the registers of cut nodes. Every cut node is itself a root of some
\(T_u\) covered for reg. So condition (ii) of Definition 21.4.2 holds, and condition (i) holds
for the roots. Restricted optimality. In a cover of the restricted kind, no instance crosses a
cut node, so the instances split into independent covers of the trees \(T_v\) (a shared node
appears in its users' trees only as the leaf TEMP t_u). The minimum of the sum is the sum of the minima, which the DP computes for
each tree. Not optimum in general: Example D1 in §3 has a cover of cost 5, while
decomposition costs 6.
NOLTIS¶
Definition 21.4.8 (Overlap and CSE costs, as used in this course)
Run the tree DP (Algorithm 21.2.3) on \(D\) as if every node had its own copy for each parent, and let \(\Phi\) be the tiles chosen top-down from the roots. For a shared node \(n\) that lies strictly inside tiles \(T_1, \dots, T_q\) of \(\Phi\) and is the root of none of them:
- the overlap cost \(\omega(n) = \sum_{i} \bigl(c(T_i) - c(T_i \setminus n)\bigr)\), where \(c(T_i)\) is the tile's cost at its root and \(c(T_i \setminus n)\) is the cheapest cost at the same root and goal if \(n\) were a register leaf of cost 0 (the extra work each tile does by recomputing \(n\));
- the CSE cost \(\kappa(n) = C(n, \mathsf{reg})\), the cost of computing \(n\) once.
Koes and Goldstein's definitions differ in detail (they also account for tiles that overlap \(n\) only partially), but they make the same comparison [KG08].
Algorithm 21.4.9 (NOLTIS, after [KG08])
- Input: an expression DAG \(D\) and a grammar \(G\).
- Output: a cover of \(D\).
- Precondition: as for the tree DP. The DAG is given with parent lists.
- Postcondition: a valid cover (Proposition 21.4.10) in which every shared node \(n\) with \(\kappa(n) < \omega(n)\) in the first pass is a tile root.
- Invariant: in the second DP, no pattern may place a fixed node strictly inside a tile.
function NOLTIS(D):
fixed ← {}
DAGLabel(D, fixed) # tree DP on the DAG, sharing ignored
Φ ← TopDownSelect(D) # tiles reachable from the roots
for each shared node n covered only strictly inside tiles of Φ:
if κ(n) < ω(n): fixed ← fixed ∪ {n} # Definition 21.4.8
DAGLabel(D, fixed)
return TopDownSelect(D)
function DAGLabel(D, fixed):
for each node v in topological order (children first):
Label(v) as in Algorithm 21.2.3, except that a pattern whose inner node would map to
a node of fixed does not match, and a fixed node's label is read as 0 by its parents
(its cost is paid once, at its own tile)
function TopDownSelect(D):
work ← [(v, goal of v) for v in Roots] + [(n, reg) for n in fixed]; chosen ← {}
while work is not empty:
(v, A) ← pop(work)
if (v, A) already handled: continue
r ← rule[v][A]; chosen ← chosen ∪ {(v, r)}
push every operand (u, B) of r at v onto work
return chosen
Proposition 21.4.10 (NOLTIS is valid and linear)
Algorithm 21.4.9 returns a cover of \(D\) in time \(O(\lvert V \rvert + \lvert E \rvert)\) for a fixed grammar.
Proof
Validity. TopDownSelect starts from every root with its required goal and, for every chosen
instance, pushes every operand with the nonterminal it needs. A pair is handled once and
receives the rule that DAGLabel found for it, which exists because labels are computed for
every node (the grammar derives every node, as for trees). So conditions (i) and (ii) of
Definition 21.4.2 hold when the work list empties. Time. Each of the two labeling passes
visits every node once and tries \(O(R)\) rules of size \(O(p)\), a constant for a fixed grammar.
TopDownSelect handles each (node, nonterminal) pair at most once, and there are
\(O(\lvert V \rvert \cdot \lvert N \rvert)\) of them. Computing \(\omega\) for all shared nodes
re-labels each covering tile's root once with \(n\) cut, \(O(1)\) per (tile, node) incidence,
and the incidences are bounded by \(O(\lvert V \rvert p)\).
PBQP and ILP formulations¶
Definition 21.4.11 (PBQP for instruction selection)
Put \(G\) in normal form, sharing identical helper nonterminals (Definition 21.3.1). A PBQP instance has one variable \(x_v\) per DAG node, ranging over the normal-form rules whose operator is \(v\)'s operator, a cost vector \(\vec{c}_v[r] = c(r)\), and for every edge \((v, u)\), where \(u\) is the \(i\)-th child of \(v\), a cost matrix $M_{vu}[r][r'] = $ the cheapest chain-rule cost from the nonterminal \(r'\) produces to the nonterminal \(r\) expects at position \(i\) (0 if equal, \(\infty\) if impossible). A solution assigns a rule to every variable and costs \(\sum_v \vec{c}_v[x_v] + \sum_{(v, u)} M_{vu}[x_v][x_u]\).
Algorithm 21.4.12 (PBQP by reduction, after Scholz–Eckstein)
- Input: a PBQP instance (a graph with vectors on nodes and matrices on edges).
- Output: an assignment \(x\).
- Precondition: vector and matrix entries are in \(\mathbb{N} \cup \{\infty\}\).
- Postcondition: if only R0, R1 and R2 were needed, \(x\) is optimum (Theorem 21.4.13). Otherwise it is a heuristic solution.
- Invariant: the instance on the remaining nodes, with the folded vectors and matrices, has the same optimum as the original restricted to those nodes.
function SolvePBQP(G):
stack ← []
while G has nodes:
if some node y has degree 0: push (R0, y); remove y
elif some node y has degree 1 (neighbor z): # R1
for each choice j of z: c_z[j] += min_i (c_y[i] + M_yz[i][j])
push (R1, y); remove y
elif some node y has degree 2 (neighbors z, w): # R2
for each j, k: M_zw[j][k] += min_i (c_y[i] + M_yz[i][j] + M_yw[i][k])
push (R2, y); remove y
else: # RN: heuristic
y ← a node of highest degree; fix x_y ← argmin_i (c_y[i] + Σ_z min_j M_yz[i][j])
fold x_y into its neighbors' vectors; push (RN, y); remove y
while stack is not empty: # back-propagation
(rule, y) ← pop(stack)
if rule ≠ RN: x_y ← the i minimizing y's term given the already chosen neighbors
return x
Theorem 21.4.13 (R0, R1 and R2 preserve the optimum)
Removing a node of degree at most 2 with the updates of Algorithm 21.4.12 gives an instance whose optimum equals the original optimum. Back-propagation reconstructs an optimum assignment of the removed node. Hence if the graph can be reduced with R0–R2 alone (every series-parallel graph can), the solution is optimum.
Proof
The objective is a sum of terms, and the removed node \(y\) appears only in its own vector and in the matrices of its at most two edges. For fixed choices of the neighbors, the best choice of \(y\) is \(\min_i\) of exactly those terms. So \(\min_{x} \mathrm{cost}(x) = \min_{x_{-y}} \bigl( \mathrm{cost}_{-y}(x_{-y}) + \min_{i} (c_y[i] + M_{yz}[i][x_z] + M_{yw}[i][x_w]) \bigr)\). R1 adds this inner minimum (with one neighbor) to \(z\)'s vector, and R2 adds it (with two neighbors) to the \(z\)–\(w\) matrix, creating the edge if needed. R0 adds the constant \(\min_i c_y[i]\). In each case the reduced instance's objective equals the inner minimization of the original, so the optima agree. Back-propagation picks, given the neighbors' final values, an \(i\) attaining that inner minimum, which yields an optimum of the original. By induction over the stack, all removed nodes are assigned optimally.
Definition 21.4.14 (ILP formulation)
Binary variables \(y_{v,r}\) for every node \(v\) and matching rule \(r\). Minimize \(\sum_{v,r} c(r)\, y_{v,r}\) subject to: \(\sum_{r \text{ produces the required goal}} y_{v,r} \ge 1\) for every root \(v\); and for every \((v, r)\) and every operand \((u, B)\) of \(r\) at \(v\), \(y_{v,r} \le \sum_{r' \text{ produces } B \text{ at } u} y_{u,r'}\). The feasible 0/1 points are exactly the covers of Definition 21.4.2, so the ILP optimum is \(\mathrm{OPT}(D)\).
3. Worked example¶
Two small DAGs over Tessera (Table 21.1.1), each a basic block with two statements.
- D1 (a shared address):
STORE(q, TEMP z)thenMOVE(TEMP x, MEM(q))with the shared node $q = $ADD(TEMP p, CONST 8). - D2 (a shared product):
MOVE(TEMP x, ADD(m, TEMP c))andMOVE(TEMP y, ADD(m, TEMP d))with the shared node $m = $MUL(TEMP a, TEMP b).
flowchart TD
s1([s1: STORE]) --> q[q: ADD]
s1 --> z[TEMP z]
s2([s2: MOVE x]) --> mm[MEM]
mm --> q
q --> p[TEMP p]
q --> c8[CONST 8]
classDef hl fill:#fde68a,stroke:#b45309;
class q hl;
Tree decomposition and greedy DAG matching¶
D1. Cut at \(q\), so $T_q = $ ADD(p, 8) → addi r1, p, 8 (1). Then $T_{s1} = $ STORE(t_q, z) → r2 st z, 0(r1) (2), and $T_{s2} = $ MOVE(x, MEM(t_q)) → r17 ld r2, 0(r1) (2) + r1 mv x, r2 (1). Total 6.
D2. Cut at \(m\): mul r1, a, b (3). Then add r2, r1, c, mv x, r2, add r3, r1, d, mv y, r3 (1 each). Total 7.
Greedy matching with LLVM's single-use rule gives the same result on both. \(q\) and \(m\) have two uses, so no pattern may swallow them, and every tree is then tiled greedily (munch finds the optimum on these small trees). A target that duplicates address arithmetic on purpose (§7) folds \(q\) into both memory operands instead and gets 5 on D1.
NOLTIS¶
D1, first pass (tree DP, sharing ignored). The labels are \(C(q, \mathsf{reg}) = 1\) (r9), \(C(s1) = 2\) (r3 STORE(ADD(reg, CONST), reg), which covers \(q\) and 8), \(C(\mathsf{MEM}) = 2\) (r18, which also covers \(q\)), and \(C(s2) = 3\) (r1). TopDownSelect chooses r3, r1 and r18. \(q\) lies inside two tiles and is the root of none.
| shared node | tiles containing it | \(c(T_i)\) | \(c(T_i \setminus n)\) | overlap \(\omega\) | CSE \(\kappa = C(n, \mathsf{reg})\) | decision |
|---|---|---|---|---|---|---|
| \(q\) (D1) | r3 at s1; r18 at MEM |
2; 2 | r2: 2; r17: 2 | 0 + 0 = 0 | 1 (addi) |
keep the overlap |
| \(m\) (D2) | r13 at the first ADD; r13 at the second |
3; 3 | r8: 1; r8: 1 | 2 + 2 = 4 | 3 (mul) |
fix \(m\) |
- D1. Recomputing \(q\) inside the addressing modes is free, and sharing costs one
addi. The second pass is not needed. The code isst z, 8(p)(2),ld r1, 8(p)(2),mv x, r1(1). Total 5, which is optimum: the store and the load cost at least 2 each (every Tessera rule with aSTOREorMEMroot costs 2 or more), and theMOVEat least 1. - D2. Each
maddrecomputes the product, which costs 2 more than anaddthat reads it from a register, 4 in total. Computing \(m\) once costs 3. So \(m\) is fixed. In the second pass theADDs can no longer use r13, and the result ismul(3) + 2add+ 2mv. Total 7. The first pass alone would have produced twomadds and twomvs, total 8.
PBQP and ILP formulations¶
PBQP on D2. The variables are \(m\) ∈ {r12 (produces reg, cost 3), \(h\) (the helper MUL(reg, reg) inside r13, cost 0)}, \(a_1, a_2\) ∈ {r8 (cost 1, wants reg at position 1), r13 (cost 3, wants \(h\) at position 1)}. The MOVEs and leaves have one choice each, which contributes a constant 2 (the two mv) plus 0. The edge matrices \(M_{a_i m}\) are 0 on (r8, r12) and (r13, \(h\)) and \(\infty\) elsewhere. The graph is the path \(a_1 - m - a_2\).
| step | rule | node removed | update | vectors after the step |
|---|---|---|---|---|
| 1 | R1 | \(a_1\) | \(c_m[\text{r12}] \mathrel{+}= \min(1 + 0,\ 3 + \infty) = 1\); \(c_m[h] \mathrel{+}= \min(1 + \infty,\ 3 + 0) = 3\) | \(c_m = (4, 3)\) |
| 2 | R1 | \(a_2\) | same increments | \(c_m = (5, 6)\) |
| 3 | R0 | \(m\) | choose r12 | constant 5 |
| 4 | back-propagate | \(a_2\), \(a_1\) | given $m = $ r12, choose r8 (1 + 0) | — |
Optimum: \(5 + 2 = 7\), the same as NOLTIS. On D1 the same construction lets \(q\) choose the shared helper d: ADD(reg, c) that both r3 and r18 use (Definition 21.3.1). Both parents are then satisfied by one choice, and PBQP finds 5.
ILP on D1. The variables include \(y_{q,\text{r8}}\), \(y_{q,\text{r9}}\), \(y_{q,d}\) (the helper), \(y_{s1,\text{r2}}\), \(y_{s1,\text{r3}}\), \(y_{M,\text{r17}}\), \(y_{M,\text{r18}}\) and \(y_{s2,\text{r1}}\), plus the leaves. Two of the constraints are \(y_{s1,\text{r2}} + y_{s1,\text{r3}} \ge 1\) and \(y_{M,\text{r17}} \le y_{q,\text{r8}} + y_{q,\text{r9}}\) (r17 needs reg at \(q\), which r8 or r9 can produce). The minimum, 5, sets \(y_{s1,\text{r3}} = y_{M,\text{r18}} = y_{s2,\text{r1}} = 1\) and uses no instance producing reg at \(q\).
Try it
The drills stay on trees, where these methods coincide with the DP:
./course drill dp-tiling --seed 7 --difficulty hard --solution. To see LLVM's greedy DAG
matcher decide between folding and sharing, run the boxes of §7 on your own IR.
4. Invariants and correctness¶
Tree decomposition and greedy DAG matching¶
Proposition 21.4.7 covers decomposition. For greedy DAG matching the invariant that matters is acyclicity. Folding a node \(u\) into a tile rooted at \(v\) turns the tile into one machine instruction. If some other path leads from \(v\) back to \(u\) (through a third node \(x\) that uses \(u\) and is used by \(v\)), the folded instruction would need its own result before it runs, which is a cycle. LLVM's IsLegalToFold rejects exactly this by searching for such a path (findNonImmUse). A fold that passes it keeps the selected graph a DAG, so a schedule exists. Without the check the selector would produce unschedulable code, which is why it is a correctness condition, not a heuristic.
NOLTIS¶
Proposition 21.4.10 gives validity and linear time. NOLTIS is not optimum. Theorem 21.4.4 says no polynomial algorithm is, unless P = NP. Its decisions are local: whether to fix \(n\) is decided per node from costs computed in a pass that assumed all other shared nodes were duplicated. A pair of shared nodes whose best decisions depend on each other can mislead it. The 99.7 % figure is empirical [KG08].
PBQP and ILP formulations¶
Theorem 21.4.13 proves the reductions exact, and Definition 21.4.14 makes the ILP exact by construction. The PBQP formulation of Definition 21.4.11 forces one rule per node. A shared node that two parents want as different nonterminals (one reg, one inside a pattern that needs a different helper) cannot be both. Sharing identical helpers solves the common cases, and Ebner et al. add extra constraints for patterns with several roots [EBSKWK08]. When RN fires, optimality is lost. On SSA graphs of real programs most nodes reduce with R1/R2, which is why PBQP is close to optimal in practice.
5. Complexity¶
Variables: \(\lvert V \rvert\) nodes, \(\lvert E \rvert\) edges, \(R\) rules, \(p\) pattern size, \(d\) the largest number of alternatives per PBQP variable, \(\lvert N \rvert\) nonterminals.
| Technique | Time (worst) | Time (typical) | Space | Justification |
|---|---|---|---|---|
| Tree decomposition | \(O(\lvert V \rvert R p)\) | linear | \(O(\lvert V \rvert \lvert N \rvert)\) | one tree DP per piece; the pieces partition \(V\) |
| Greedy DAG matching | \(O(\lvert V \rvert R p \cdot \mathrm{fold})\), where the cycle check findNonImmUse may walk \(O(\lvert V \rvert)\) |
near linear | \(O(\lvert V \rvert)\) | a pattern match per node plus a legality search per fold |
| NOLTIS | \(O(\lvert V \rvert + \lvert E \rvert)\) | linear | \(O(\lvert V \rvert \lvert N \rvert)\) | Proposition 21.4.10 |
| PBQP (R0–R2 only) | \(O(\lvert V \rvert d^3 + \lvert E \rvert d^2)\) | near linear | \(O(\lvert E \rvert d^2)\) | R2 builds a \(d \times d\) matrix, minimizing over \(d\) for each entry |
| ILP / exact | exponential (Theorem 21.4.4) | seconds to minutes per function | \(O(\lvert V \rvert R)\) variables | NP-hard |
A pathological family for greedy matching's cycle check. A long chain \(u \to x_1 \to \dots \to x_k \to v\) plus a direct edge \(v \to u\) with a foldable \(u\) makes findNonImmUse walk the whole chain for every attempted fold. SDNode::hasPredecessorHelper accepts a MaxSteps bound, but the fold check passes 0 (unbounded) and relies on topological node ids to prune the search (TopologicalPrune), so the walk stops at nodes that are topologically too early to reach the folded node.
A pathological family for decomposition's quality. A DAG in which one shared address \(q\) is used by \(k\) memory operations costs decomposition \(1 + 2k\) (one addi plus \(k\) accesses through a register) against \(2k\) for folding \(q\) everywhere. The absolute gap is 1. Make \(q\) an expensive shared subtree that every user can fold for free, for example the address p + (i << 3) under a scaled addressing mode. Then decomposition pays the subtree once while the optimum pays nothing, and the ratio grows with the subtree's cost.
Real-world scale. Koes and Goldstein report that NOLTIS found the optimum in 99.7 % of their cases, where the optimum was computed with an ILP solver [KG08]. Ebner et al. report compile-time overheads for PBQP selection in LLVM that were acceptable for an optimizing compiler [EBSKWK08].
6. Variants and refinements¶
Tree decomposition and greedy DAG matching¶
- Split only non-duplicable nodes. Treat loads, stores and calls as shared (never duplicated) but let arithmetic be recomputed. This is LLVM's practice for addressing modes (§7). It gets much of NOLTIS's benefit with no second pass.
- DAG-optimal grammars. Ertl shows that for grammars with a certain structural property the bottom-up DP is already optimal on DAGs. He gives a check for the property, which lets a generator certify that a grammar can be used on DAGs [Ert99].
- Rematerialize later. Keep the shared value in a register at selection time and let the register allocator recompute it if it would otherwise spill (Ch 22).
NOLTIS¶
- Iterate the improvement. Recompute \(\omega\) and \(\kappa\) after the second pass and repeat. This can fix more nodes at the price of more passes. Koes and Goldstein found one improvement round sufficient [KG08].
- Integrate with register pressure. Charge \(\kappa(n)\) for the register a shared value occupies. This is closer to Aho–Johnson's register-bounded costs (Lesson 21.2).
PBQP and ILP formulations¶
- SSA graphs across blocks [EKS03, EBSKWK08]: select for a whole function at once. Patterns may span blocks, which matters for DSPs with complex instructions.
- Constraint programming, universal selection [Bli16]: add global code motion and block ordering to the model, at the price of solver time.
- Superoptimization-style exact selection: Denali searches e-graphs of equivalent programs with a SAT solver for the shortest sequence [JNR02] (Lesson 21.7).
7. In real compilers¶
Tree decomposition and greedy DAG matching¶
LLVM's SelectionDAG matcher is greedy DAG matching. A load may be folded into its user only if SelectionDAGISel::IsProfitableToFold (by default N.hasOneUse()) and SelectionDAGISel::IsLegalToFold (the cycle check with findNonImmUse) agree, both in llvm/lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp. Addressing modes are matched separately by target code (X86DAGToDAGISel::matchAddress and selectAddr in llvm/lib/Target/X86/X86ISelDAGToDAG.cpp), which deliberately duplicates shared address arithmetic (LLVM 23.1.2) [LLVM-SDISel, LLVM-X86ISelDAG]. lcc cuts DAGs into trees in undag (src/dag.c), assigning a temporary to every value with several uses unless the target sets wants_dag [LCC-Src].
LLVM folds a single-use load, but not a shared one
Reproduce (llc 23.1.2):
cat > share.ll <<'EOF'
define i64 @one_use(ptr %p, i64 %x) {
%v = load i64, ptr %p, align 8
%a = add i64 %v, %x
ret i64 %a
}
define i64 @two_uses(ptr %p, i64 %x, i64 %y) {
%v = load i64, ptr %p, align 8
%a = add i64 %v, %x
%b = mul i64 %v, %y
%r = xor i64 %a, %b
ret i64 %r
}
EOF
llc -O2 -mtriple=x86_64-linux-gnu share.ll -o - | grep -vE '^\s*\.|^#|^$|# -- '
Output (complete after the filter):
one_use: # @one_use
movq %rsi, %rax
addq (%rdi), %rax
retq
two_uses: # @two_uses
movq (%rdi), %rax
leaq (%rsi,%rax), %rcx
imulq %rdx, %rax
xorq %rcx, %rax
retq
What to notice: with one use, the load becomes the memory operand of addq (a tile
covering load + add). With two uses, IsProfitableToFold refuses. Folding into both
users would read memory twice, and folding into one would still need a separate load for the
other. The load becomes its own tile, as in tree decomposition.
NOLTIS¶
NOLTIS itself was prototyped in LLVM by its authors but is not part of upstream LLVM. Its central decision, duplicate a shared node when that is cheaper than sharing it, is what LLVM's address-mode matchers do for shared address arithmetic: matchAddress folds a base+index·scale+offset computation into every memory operand that uses it, whatever its number of uses [LLVM-X86ISelDAG].
A shared address computation is duplicated into both addressing modes
Reproduce (llc 23.1.2):
cat > dup.ll <<'EOF'
define i64 @swap_in(ptr %p, i64 %i, i64 %z) {
%q = getelementptr inbounds i64, ptr %p, i64 %i
%v = load i64, ptr %q, align 8
store i64 %z, ptr %q, align 8
ret i64 %v
}
EOF
llc -O2 -mtriple=x86_64-linux-gnu -stop-after=finalize-isel dup.ll -o - | sed -n '/^body/,$p' | sed 's/[[:space:]]*$//'
llc -O2 -mtriple=aarch64-linux-gnu dup.ll -o - | grep -vE '^\s*\.|^#|^$|// -- '
Output (complete after the filters):
body: |
bb.0 (%ir-block.0):
liveins: $rdi, $rsi, $rdx
%2:gr64 = COPY $rdx
%1:gr64_nosp = COPY $rsi
%0:gr64 = COPY $rdi
%3:gr64 = MOV64rm %0, 8, %1, 0, $noreg :: (load (s64) from %ir.q)
MOV64mr %0, 8, %1, 0, $noreg, %2 :: (store (s64) into %ir.q)
$rax = COPY %3
RET 0, $rax
...
swap_in: // @swap_in
// %bb.0:
mov x8, x0
ldr x0, [x0, x1, lsl #3]
str x2, [x8, x1, lsl #3]
ret
What to notice: %q has two users, yet no instruction computes it. Both MOV64rm and
MOV64mr carry the whole address %0 + 8·%1 in their operands, and on AArch64 both
ldr and str use [base, index, lsl #3]. This is example D1: the overlap cost of the shared
address is 0 (addressing modes are free), so recomputing it inside each memory tile beats
computing it once, which is the choice NOLTIS makes with \(\omega(q) = 0 < \kappa(q)\).
PBQP and ILP formulations¶
LLVM's PBQP solver is in llvm/include/llvm/CodeGen/PBQP/ (Graph.h, ReductionRules.h with applyR1, applyR2 and backpropagate). Upstream it serves the PBQP register allocator (llc -regalloc=pbqp, llvm/lib/CodeGen/RegAllocPBQP.cpp). Ebner et al.'s PBQP instruction selector used the same kind of solver in their LLVM prototype but was not merged (LLVM 23.1.2) [LLVM-PBQP, EBSKWK08].
The R1 reduction in LLVM's PBQP solver
Reproduce (LLVM source at tag llvmorg-23.1.2; needs network access to GitHub):
curl -sL https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-23.1.2/llvm/include/llvm/CodeGen/PBQP/ReductionRules.h \
| sed -n '/void applyR1(/,/^ }/p'
Output (complete):
void applyR1(GraphT &G, typename GraphT::NodeId NId) {
using NodeId = typename GraphT::NodeId;
using EdgeId = typename GraphT::EdgeId;
using Vector = typename GraphT::Vector;
using Matrix = typename GraphT::Matrix;
using RawVector = typename GraphT::RawVector;
assert(G.getNodeDegree(NId) == 1 &&
"R1 applied to node with degree != 1.");
EdgeId EId = *G.adjEdgeIds(NId).begin();
NodeId MId = G.getEdgeOtherNodeId(EId, NId);
const Matrix &ECosts = G.getEdgeCosts(EId);
const Vector &XCosts = G.getNodeCosts(NId);
RawVector YCosts = G.getNodeCosts(MId);
// Duplicate a little to avoid transposing matrices.
if (NId == G.getEdgeNode1Id(EId)) {
for (unsigned j = 0; j < YCosts.getLength(); ++j) {
PBQPNum Min = ECosts[0][j] + XCosts[0];
for (unsigned i = 1; i < XCosts.getLength(); ++i) {
PBQPNum C = ECosts[i][j] + XCosts[i];
if (C < Min)
Min = C;
}
YCosts[j] += Min;
}
} else {
for (unsigned i = 0; i < YCosts.getLength(); ++i) {
PBQPNum Min = ECosts[i][0] + XCosts[0];
for (unsigned j = 1; j < XCosts.getLength(); ++j) {
PBQPNum C = ECosts[i][j] + XCosts[j];
if (C < Min)
Min = C;
}
YCosts[i] += Min;
}
}
G.setNodeCosts(MId, YCosts);
G.disconnectEdge(EId, MId);
}
What to notice: this is step 1 of the D2 trace, line for line. For every choice j of the
neighbor, add \(\min_i\) (node cost + edge cost) to the neighbor's vector, then disconnect the
degree-1 node. The proof of Theorem 21.4.13 is the comment "the reduced instance's objective
equals the inner minimization", and this loop computes that inner minimization.
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| Tree decomposition and greedy DAG matching | optimum only among covers that never duplicate (Proposition 21.4.7); greedy folds follow fixed rules | linear · the default in production | D1: 6, D2: 7 (optimum 5 and 7) | low: reuse the tree selector; fold legality checks | LLVM SelectionDAG (single-use fold + address-mode duplication), lcc (undag), the Dragon book |
| NOLTIS | near-optimal: optimum on 99.7 % of cases [KG08]; D1: 5, D2: 7 | linear, two DP passes | usually optimal; local decisions can mislead it | moderate: DP twice plus overlap/CSE costs | research prototype in LLVM; the idea is in address-mode duplication |
| PBQP and ILP formulations | PBQP optimum when R0–R2 suffice (Theorem 21.4.13); ILP always optimum | PBQP near linear with RN; ILP exponential | best available; PBQP degrades gracefully | high: formulation, solver, DAG-pattern constraints | DSP compilers, research (Ebner et al. in LLVM), universal selection with CP |
- Choose tree decomposition or greedy matching when compile time matters and your costs make recomputation rarely worthwhile, except for addressing modes, which you special-case.
- Choose NOLTIS when you have a tree DP already and want most of the DAG benefit at linear cost.
- Choose PBQP or ILP when instructions are irregular (DSPs, SIMD with odd constraints), code quality justifies solver time, or you need a baseline to measure heuristics against.
9. Assessment¶
| Technique | Quiz ids (solutions/quizzes/ch21.yaml) |
Drill | Flashcard tag | Exercises |
|---|---|---|---|---|
| Tree decomposition and greedy DAG matching | dag-decomposition-cost, dag-np-complete |
none: DAG covering is NP-complete in general, and small instances are better done in the quiz with the lesson's D1/D2 | dag-decomposition |
— |
| NOLTIS | noltis-fix, noltis-overlap |
none (as above); the per-tree DP inside it is ./course drill dp-tiling |
noltis |
— |
| PBQP and ILP formulations | pbqp-r1, pbqp-optimal-when |
none: reductions are quizzed on a fixed instance | pbqp-ilp |
— |
Charging a shared value twice
Running the tree DP on a DAG and adding the labels of a shared child into each parent counts its cost once per parent. The DP then prefers duplication even when sharing is cheaper, because sharing looks twice as expensive as it is. In the DAG cost model (Definition 21.4.2) an instance is paid once, however many tiles read its register. NOLTIS's second pass reads a fixed node's label as 0 for exactly this reason.
References¶
See the chapter references.