Source file src/cmd/compile/internal/ssa/dfplus_iter.go
1 // Copyright 2026 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package ssa 6 7 import ( 8 "container/heap" 9 "iter" 10 ) 11 12 // DF(x), the dominance frontier of x, holds every block y such that x 13 // dominates a predecessor of y but does not strictly dominate y. Its 14 // transitive closure DF+ (also called the merge set) is where a phi 15 // may need to be placed for a variable defined in x. DF+ of a set of 16 // blocks S, denoted IDF(S) (iterated dominance frontier), is the union 17 // of the DF+ of the blocks in S. 18 19 // IterDomFrontierPlus iterates the DF+ of seeds: every block at which 20 // a phi may need to be placed if a variable were defined in the seed 21 // blocks. For each frontier block, it also yields the block whose 22 // outgoing edge discovered the frontier. Frontier blocks are yielded 23 // at most once, in a deterministic order; an early break stops the walk. 24 // Seed blocks themselves are not yielded as such, but a seed that is 25 // also a merge point (e.g. a loop header) is. 26 // seeds iterator is consumed in full before the walk starts (the current 27 // algorithm has to walk deeper roots first). 28 // CFG must not change while iteration is in progress; inserting 29 // values (like phis) is fine. 30 func (f *Func) IterDomFrontierPlus(seeds iter.Seq[*Block]) iter.Seq2[*Block, *Block] { 31 return func(yield func(*Block, *Block) bool) { 32 // Materialize the seeds into a pooled slice reused by walkDFPlus. 33 s := f.Cache.AllocBlockSlice(f.NumBlocks())[:0] 34 defer f.Cache.FreeBlockSlice(s[:cap(s)]) 35 for b := range seeds { 36 s = append(s, b) 37 } 38 f.walkDFPlus(s, yield) 39 } 40 } 41 42 // Per-block state of a DF+ walk, packed into one flag byte per block. 43 // None of the bits is cleared during the walk. Each of the following happens at 44 // most once per block: 45 // - enters the work queue, 46 // - is banked as a root, 47 // - is yielded. 48 const ( 49 // The block's subtree walk is done or pending on q. 50 flagQueued = 1 << iota 51 // The block has been added to the PiggyBank: a seed, or a block 52 // yielded earlier in this walk. 53 flagPiggyBanked 54 // The block has been yielded to the caller. 55 flagYielded 56 ) 57 58 // walkDFPlus is the engine under IterDomFrontierPlus. 59 // The walk is the Sreedhar & Gao DJ-graph algorithm, "A Linear Time 60 // Algorithm for Placing Φ-Nodes". Work is proportional to the dominator 61 // subtrees walked (skipping subtrees already covered, deeper roots) 62 // plus the frontier found, and memory is O(f.NumBlocks()). The walk reads 63 // the CFG's edges and uses the cached dominator tree. 64 // The seeds slice is reused in place by the PiggyBank. 65 func (f *Func) walkDFPlus(seeds []*Block, yield func(*Block, *Block) bool) { 66 sdom := f.Sdom() 67 68 // Roots to process, deepest first. 69 piggyBank := blockHeap{t: sdom, a: seeds[:0]} 70 71 // The worklist is a pooled slice, freed after the walk is done. 72 // Each block enters it at most once, so it never outgrows its capacity. 73 q := f.Cache.AllocBlockSlice(f.NumBlocks())[:0] 74 defer f.Cache.FreeBlockSlice(q[:cap(q)]) 75 76 // per-block walk state; see the flag constants above. 77 flags := f.Cache.AllocInt8Slice(f.NumBlocks()) 78 defer f.Cache.FreeInt8Slice(flags) 79 80 // Bank the seeds as roots, compacting in place to drop duplicates. 81 for _, b := range seeds { 82 if flags[b.ID]&flagPiggyBanked == 0 { 83 flags[b.ID] |= flagPiggyBanked 84 piggyBank.a = append(piggyBank.a, b) 85 } 86 } 87 heap.Init(&piggyBank) 88 89 // Visit the roots from deepest to shallowest. 90 for len(piggyBank.a) > 0 { 91 currentRoot := heap.Pop(&piggyBank).(*Block) 92 // Walk the subtree below the root, skipping subtrees already 93 // covered by previous (deeper) roots, and find the edges 94 // exiting it: their targets are the dominance frontier. 95 // Roots are popped deepest first, so any block a later root's walk could 96 // queue lies strictly below that root and was already queued 97 // by its own root-push. 98 if flags[currentRoot.ID]&flagQueued != 0 { 99 f.Fatalf("root already in queue") 100 } 101 flags[currentRoot.ID] |= flagQueued 102 q = append(q, currentRoot) 103 for len(q) > 0 { 104 b := q[len(q)-1] 105 q = q[:len(q)-1] 106 107 currentRootLevel := sdom.Level(currentRoot) 108 for _, e := range b.Succs { 109 c := e.Block() 110 if sdom.Level(c) > currentRootLevel { 111 // a D-edge, or an edge whose target is in currentRoot's subtree. 112 continue 113 } 114 if flags[c.ID]&flagYielded != 0 { 115 continue 116 } 117 flags[c.ID] |= flagYielded 118 if flags[c.ID]&flagPiggyBanked == 0 { 119 // Bank c as a root; its subtree may find further frontier edges. 120 // Invariant: piggyBanked = seeds ∪ yielded 121 flags[c.ID] |= flagPiggyBanked 122 heap.Push(&piggyBank, c) 123 } 124 if !yield(c, b) { 125 return 126 } 127 } 128 129 // Visit children if they have not been visited yet. 130 for ch := sdom.Child(b); ch != nil; ch = sdom.Sibling(ch) { 131 if flags[ch.ID]&flagQueued == 0 { 132 flags[ch.ID] |= flagQueued 133 q = append(q, ch) 134 } 135 } 136 } 137 } 138 } 139 140 // A block heap is used as a priority queue to implement the PiggyBank 141 // from Sreedhar and Gao. That paper uses an array which is better 142 // asymptotically but worse in the common case when the PiggyBank 143 // holds a sparse set of blocks. 144 type blockHeap struct { 145 a []*Block // blocks in heap 146 t SparseTree // dominator tree; provides block levels for priority 147 } 148 149 func (h *blockHeap) Len() int { return len(h.a) } 150 func (h *blockHeap) Swap(i, j int) { a := h.a; a[i], a[j] = a[j], a[i] } 151 152 func (h *blockHeap) Push(x any) { 153 v := x.(*Block) 154 h.a = append(h.a, v) 155 } 156 func (h *blockHeap) Pop() any { 157 old := h.a 158 n := len(old) 159 x := old[n-1] 160 h.a = old[:n-1] 161 return x 162 } 163 func (h *blockHeap) Less(i, j int) bool { 164 return h.t.Level(h.a[i]) > h.t.Level(h.a[j]) 165 } 166