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