Skip to content

Commit 7979c8f

Browse files
committed
cmd/compile/internal/liveness: introduce "live intervals" utility
Introduce a helper type "Intervals" that contains sets of sorted disjoint ranges corresponding to live ranges within a function. Example: the Intervals set "{ [0,1), [4,10) }" would indicate that something is live starting at instruction 0, then up to but not including instruction 1, then dead from 1-3, then live again at instruction 4 up to (but not including) instruction 10. This patch provides APIs for constructing interval sets, testing to see whether two sets overlap, and unioning/merging together two intervals sets. Updates #62737. Updates #65532. Updates #65495. Change-Id: I7140a5989eba93bf3b8762d9224261f5eba0646d Reviewed-on: https://go-review.googlesource.com/c/go/+/566177 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent 593daf7 commit 7979c8f

File tree

2 files changed

+914
-0
lines changed

2 files changed

+914
-0
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
// Copyright 2024 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 liveness
6+
7+
// This file defines an "Intervals" helper type that stores a
8+
// sorted sequence of disjoint ranges or intervals. An Intervals
9+
// example: { [0,5) [9-12) [100,101) }, which corresponds to the
10+
// numbers 0-4, 9-11, and 100. Once an Intervals object is created, it
11+
// can be tested to see if it has any overlap with another Intervals
12+
// object, or it can be merged with another Intervals object to form a
13+
// union of the two.
14+
//
15+
// The intended use case for this helper is in describing object or
16+
// variable lifetime ranges within a linearized program representation
17+
// where each IR instruction has a slot or index. Example:
18+
//
19+
// b1:
20+
// 0 VarDef abc
21+
// 1 memset(abc,0)
22+
// 2 VarDef xyz
23+
// 3 memset(xyz,0)
24+
// 4 abc.f1 = 2
25+
// 5 xyz.f3 = 9
26+
// 6 if q goto B4
27+
// 7 B3: z = xyz.x
28+
// 8 goto B5
29+
// 9 B4: z = abc.x
30+
// // fallthrough
31+
// 10 B5: z++
32+
//
33+
// To describe the lifetime of the variables above we might use these
34+
// intervals:
35+
//
36+
// "abc" [1,7), [9,10)
37+
// "xyz" [3,8)
38+
//
39+
// Clients can construct an Intervals object from a given IR sequence
40+
// using the "IntervalsBuilder" helper abstraction (one builder per
41+
// candidate variable), by making a
42+
// backwards sweep and invoking the Live/Kill methods to note the
43+
// starts and end of a given lifetime. For the example above, we would
44+
// expect to see this sequence of calls to Live/Kill:
45+
//
46+
// abc: Live(9), Kill(8), Live(6), Kill(0)
47+
// xyz: Live(8), Kill(2)
48+
49+
import (
50+
"fmt"
51+
"os"
52+
"strings"
53+
)
54+
55+
const debugtrace = false
56+
57+
// Interval hols the range [st,en).
58+
type Interval struct {
59+
st, en int
60+
}
61+
62+
// Intervals is a sequence of sorted, disjoint intervals.
63+
type Intervals []Interval
64+
65+
func (i Interval) String() string {
66+
return fmt.Sprintf("[%d,%d)", i.st, i.en)
67+
}
68+
69+
// TEMPORARY until bootstrap version catches up.
70+
func imin(i, j int) int {
71+
if i < j {
72+
return i
73+
}
74+
return j
75+
}
76+
77+
// TEMPORARY until bootstrap version catches up.
78+
func imax(i, j int) int {
79+
if i > j {
80+
return i
81+
}
82+
return j
83+
}
84+
85+
// Overlaps returns true if here is any overlap between i and i2.
86+
func (i Interval) Overlaps(i2 Interval) bool {
87+
return (imin(i.en, i2.en) - imax(i.st, i2.st)) > 0
88+
}
89+
90+
// adjacent returns true if the start of one interval is equal to the
91+
// end of another interval (e.g. they represent consecutive ranges).
92+
func (i1 Interval) adjacent(i2 Interval) bool {
93+
return i1.en == i2.st || i2.en == i1.st
94+
}
95+
96+
// MergeInto merges interval i2 into i1. This version happens to
97+
// require that the two intervals either overlap or are adjacent.
98+
func (i1 *Interval) MergeInto(i2 Interval) error {
99+
if !i1.Overlaps(i2) && !i1.adjacent(i2) {
100+
return fmt.Errorf("merge method invoked on non-overlapping/non-adjacent")
101+
}
102+
i1.st = imin(i1.st, i2.st)
103+
i1.en = imax(i1.en, i2.en)
104+
return nil
105+
}
106+
107+
// IntervalsBuilder is a helper for constructing intervals based on
108+
// live dataflow sets for a series of BBs where we're making a
109+
// backwards pass over each BB looking for uses and kills. The
110+
// expected use case is:
111+
//
112+
// - invoke MakeIntervalsBuilder to create a new object "b"
113+
// - series of calls to b.Live/b.Kill based on a backwards reverse layout
114+
// order scan over instructions
115+
// - invoke b.Finish() to produce final set
116+
//
117+
// See the Live method comment for an IR example.
118+
type IntervalsBuilder struct {
119+
s Intervals
120+
// index of last instruction visited plus 1
121+
lidx int
122+
}
123+
124+
func (c *IntervalsBuilder) last() int {
125+
return c.lidx - 1
126+
}
127+
128+
func (c *IntervalsBuilder) setLast(x int) {
129+
c.lidx = x + 1
130+
}
131+
132+
func (c *IntervalsBuilder) Finish() (Intervals, error) {
133+
// Reverse intervals list and check.
134+
// FIXME: replace with slices.Reverse once the
135+
// bootstrap version supports it.
136+
for i, j := 0, len(c.s)-1; i < j; i, j = i+1, j-1 {
137+
c.s[i], c.s[j] = c.s[j], c.s[i]
138+
}
139+
if err := check(c.s); err != nil {
140+
return Intervals{}, err
141+
}
142+
r := c.s
143+
return r, nil
144+
}
145+
146+
// Live method should be invoked on instruction at position p if instr
147+
// contains an upwards-exposed use of a resource. See the example in
148+
// the comment at the beginning of this file for an example.
149+
func (c *IntervalsBuilder) Live(pos int) error {
150+
if pos < 0 {
151+
return fmt.Errorf("bad pos, negative")
152+
}
153+
if c.last() == -1 {
154+
c.setLast(pos)
155+
if debugtrace {
156+
fmt.Fprintf(os.Stderr, "=-= begin lifetime at pos=%d\n", pos)
157+
}
158+
c.s = append(c.s, Interval{st: pos, en: pos + 1})
159+
return nil
160+
}
161+
if pos >= c.last() {
162+
return fmt.Errorf("pos not decreasing")
163+
}
164+
// extend lifetime across this pos
165+
c.s[len(c.s)-1].st = pos
166+
c.setLast(pos)
167+
return nil
168+
}
169+
170+
// Kill method should be invoked on instruction at position p if instr
171+
// should be treated as as having a kill (lifetime end) for the
172+
// resource. See the example in the comment at the beginning of this
173+
// file for an example. Note that if we see a kill at position K for a
174+
// resource currently live since J, this will result in a lifetime
175+
// segment of [K+1,J+1), the assumption being that the first live
176+
// instruction will be the one after the kill position, not the kill
177+
// position itself.
178+
func (c *IntervalsBuilder) Kill(pos int) error {
179+
if pos < 0 {
180+
return fmt.Errorf("bad pos, negative")
181+
}
182+
if c.last() == -1 {
183+
return nil
184+
}
185+
if pos >= c.last() {
186+
return fmt.Errorf("pos not decreasing")
187+
}
188+
c.s[len(c.s)-1].st = pos + 1
189+
// terminate lifetime
190+
c.setLast(-1)
191+
if debugtrace {
192+
fmt.Fprintf(os.Stderr, "=-= term lifetime at pos=%d\n", pos)
193+
}
194+
return nil
195+
}
196+
197+
// check examines the intervals in "is" to try to find internal
198+
// inconsistencies or problems.
199+
func check(is Intervals) error {
200+
for i := 0; i < len(is); i++ {
201+
st := is[i].st
202+
en := is[i].en
203+
if en <= st {
204+
return fmt.Errorf("bad range elem %d:%d, en<=st", st, en)
205+
}
206+
if i == 0 {
207+
continue
208+
}
209+
// check for badly ordered starts
210+
pst := is[i-1].st
211+
pen := is[i-1].en
212+
if pst >= st {
213+
return fmt.Errorf("range start not ordered %d:%d less than prev %d:%d", st, en,
214+
pst, pen)
215+
}
216+
// check end of last range against start of this range
217+
if pen > st {
218+
return fmt.Errorf("bad range elem %d:%d overlaps prev %d:%d", st, en,
219+
pst, pen)
220+
}
221+
}
222+
return nil
223+
}
224+
225+
func (is *Intervals) String() string {
226+
var sb strings.Builder
227+
for i := range *is {
228+
if i != 0 {
229+
sb.WriteString(" ")
230+
}
231+
sb.WriteString((*is)[i].String())
232+
}
233+
return sb.String()
234+
}
235+
236+
// intWithIdx holds an interval i and an index pairIndex storing i's
237+
// position (either 0 or 1) within some previously specified interval
238+
// pair <I1,I2>; a pairIndex of -1 is used to signal "end of
239+
// iteration". Used for Intervals operations, not expected to be
240+
// exported.
241+
type intWithIdx struct {
242+
i Interval
243+
pairIndex int
244+
}
245+
246+
func (iwi intWithIdx) done() bool {
247+
return iwi.pairIndex == -1
248+
}
249+
250+
// pairVisitor provides a way to visit (iterate through) each interval
251+
// within a pair of Intervals in order of increasing start time. Expected
252+
// usage model:
253+
//
254+
// func example(i1, i2 Intervals) {
255+
// var pairVisitor pv
256+
// cur := pv.init(i1, i2);
257+
// for !cur.done() {
258+
// fmt.Printf("interval %s from i%d", cur.i.String(), cur.pairIndex+1)
259+
// cur = pv.nxt()
260+
// }
261+
// }
262+
//
263+
// Used internally for Intervals operations, not expected to be exported.
264+
type pairVisitor struct {
265+
cur intWithIdx
266+
i1pos int
267+
i2pos int
268+
i1, i2 Intervals
269+
}
270+
271+
// init initializes a pairVisitor for the specified pair of intervals
272+
// i1 and i2 and returns an intWithIdx object that points to the first
273+
// interval by start position within i1/i2.
274+
func (pv *pairVisitor) init(i1, i2 Intervals) intWithIdx {
275+
pv.i1, pv.i2 = i1, i2
276+
pv.cur = pv.sel()
277+
return pv.cur
278+
}
279+
280+
// nxt advances the pairVisitor to the next interval by starting
281+
// position within the pair, returning an intWithIdx that describes
282+
// the interval.
283+
func (pv *pairVisitor) nxt() intWithIdx {
284+
if pv.cur.pairIndex == 0 {
285+
pv.i1pos++
286+
} else {
287+
pv.i2pos++
288+
}
289+
pv.cur = pv.sel()
290+
return pv.cur
291+
}
292+
293+
// sel is a helper function used by 'init' and 'nxt' above; it selects
294+
// the earlier of the two intervals at the current positions within i1
295+
// and i2, or a degenerate (pairIndex -1) intWithIdx if we have no
296+
// more intervals to visit.
297+
func (pv *pairVisitor) sel() intWithIdx {
298+
var c1, c2 intWithIdx
299+
if pv.i1pos >= len(pv.i1) {
300+
c1.pairIndex = -1
301+
} else {
302+
c1 = intWithIdx{i: pv.i1[pv.i1pos], pairIndex: 0}
303+
}
304+
if pv.i2pos >= len(pv.i2) {
305+
c2.pairIndex = -1
306+
} else {
307+
c2 = intWithIdx{i: pv.i2[pv.i2pos], pairIndex: 1}
308+
}
309+
if c1.pairIndex == -1 {
310+
return c2
311+
}
312+
if c2.pairIndex == -1 {
313+
return c1
314+
}
315+
if c1.i.st <= c2.i.st {
316+
return c1
317+
}
318+
return c2
319+
}
320+
321+
// Overlaps returns whether any of the component ranges in is overlaps
322+
// with some range in is2.
323+
func (is Intervals) Overlaps(is2 Intervals) bool {
324+
// check for empty intervals
325+
if len(is) == 0 || len(is2) == 0 {
326+
return false
327+
}
328+
li := len(is)
329+
li2 := len(is2)
330+
// check for completely disjoint ranges
331+
if is[li-1].en <= is2[0].st ||
332+
is[0].st >= is2[li2-1].en {
333+
return false
334+
}
335+
// walk the combined sets of intervals and check for piecewise
336+
// overlap.
337+
var pv pairVisitor
338+
first := pv.init(is, is2)
339+
for {
340+
second := pv.nxt()
341+
if second.done() {
342+
break
343+
}
344+
if first.pairIndex == second.pairIndex {
345+
first = second
346+
continue
347+
}
348+
if first.i.Overlaps(second.i) {
349+
return true
350+
}
351+
first = second
352+
}
353+
return false
354+
}
355+
356+
// Merge combines the intervals from "is" and "is2" and returns
357+
// a new Intervals object containing all combined ranges from the
358+
// two inputs.
359+
func (is Intervals) Merge(is2 Intervals) Intervals {
360+
if len(is) == 0 {
361+
return is2
362+
} else if len(is2) == 0 {
363+
return is
364+
}
365+
// walk the combined set of intervals and merge them together.
366+
var ret Intervals
367+
var pv pairVisitor
368+
cur := pv.init(is, is2)
369+
for {
370+
second := pv.nxt()
371+
if second.done() {
372+
break
373+
}
374+
375+
// Check for overlap between cur and second. If no overlap
376+
// then add cur to result and move on.
377+
if !cur.i.Overlaps(second.i) && !cur.i.adjacent(second.i) {
378+
ret = append(ret, cur.i)
379+
cur = second
380+
continue
381+
}
382+
// cur overlaps with second; merge second into cur
383+
cur.i.MergeInto(second.i)
384+
}
385+
ret = append(ret, cur.i)
386+
return ret
387+
}

0 commit comments

Comments
 (0)