Skip to content

Commit 00263a8

Browse files
committed
cmd/compile: reduce debugger-worsening line number churn
Reuse block head or preceding instruction's line number for register allocator's spill, fill, copy, rematerialization instructionsl; and also for phi, and for no-src-pos instructions. Assembler creates same line number tables for copy-predecessor-line and for no-src-pos, but copy-predecessor produces better-looking assembly language output with -S and with GOSSAFUNC, and does not require changes to tests of existing assembly language. Split "copyInto" into two cases, one for register allocation, one for otherwise. This caused the test score line change count to increase by one, which may reflect legitimately useful information preserved. Without any special treatment for copyInto, the change count increases by 21 more, from 51 to 72 (i.e., quite a lot). There is a test; using two naive "scores" for line number churn, the old numbering is 2x or 4x worse. Fixes #18902. Change-Id: I0a0a69659d30ee4e5d10116a0dd2b8c5df8457b1 Reviewed-on: https://go-review.googlesource.com/36207 Run-TryBot: David Chase <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 1df777f commit 00263a8

File tree

5 files changed

+340
-6
lines changed

5 files changed

+340
-6
lines changed

src/cmd/compile/internal/gc/ssa.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4346,6 +4346,29 @@ func (s *SSAGenState) SetPos(pos src.XPos) {
43464346
s.pp.pos = pos
43474347
}
43484348

4349+
// DebugFriendlySetPos sets the position subject to heuristics
4350+
// that reduce "jumpy" line number churn when debugging.
4351+
// Spill/fill/copy instructions from the register allocator,
4352+
// phi functions, and instructions with a no-pos position
4353+
// are examples of instructions that can cause churn.
4354+
func (s *SSAGenState) DebugFriendlySetPosFrom(v *ssa.Value) {
4355+
// The two choices here are either to leave lineno unchanged,
4356+
// or to explicitly set it to src.NoXPos. Leaving it unchanged
4357+
// (reusing the preceding line number) produces slightly better-
4358+
// looking assembly language output from the compiler, and is
4359+
// expected by some already-existing tests.
4360+
// The debug information appears to be the same in either case
4361+
switch v.Op {
4362+
case ssa.OpPhi, ssa.OpCopy, ssa.OpLoadReg, ssa.OpStoreReg:
4363+
// leave the position unchanged from beginning of block
4364+
// or previous line number.
4365+
default:
4366+
if v.Pos != src.NoXPos {
4367+
s.SetPos(v.Pos)
4368+
}
4369+
}
4370+
}
4371+
43494372
// genssa appends entries to pp for each instruction in f.
43504373
func genssa(f *ssa.Func, pp *Progs) {
43514374
var s SSAGenState
@@ -4381,8 +4404,7 @@ func genssa(f *ssa.Func, pp *Progs) {
43814404
thearch.SSAMarkMoves(&s, b)
43824405
for _, v := range b.Values {
43834406
x := s.pp.next
4384-
s.SetPos(v.Pos)
4385-
4407+
s.DebugFriendlySetPosFrom(v)
43864408
switch v.Op {
43874409
case ssa.OpInitMem:
43884410
// memory arg needs no code

src/cmd/compile/internal/ssa/regalloc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ func (s *regAllocState) allocValToReg(v *Value, mask regMask, nospill bool, pos
468468
c = s.curBlock.NewValue1(pos, OpCopy, v.Type, s.regs[r2].c)
469469
} else if v.rematerializeable() {
470470
// Rematerialize instead of loading from the spill location.
471-
c = v.copyInto(s.curBlock)
471+
c = v.copyIntoNoXPos(s.curBlock)
472472
} else {
473473
// Load v from its spill location.
474474
spill := s.makeSpill(v, s.curBlock)
@@ -1949,13 +1949,13 @@ func (e *edgeState) processDest(loc Location, vid ID, splice **Value, pos src.XP
19491949
e.s.f.Fatalf("can't find source for %s->%s: %s\n", e.p, e.b, v.LongString())
19501950
}
19511951
if dstReg {
1952-
x = v.copyInto(e.p)
1952+
x = v.copyIntoNoXPos(e.p)
19531953
} else {
19541954
// Rematerialize into stack slot. Need a free
19551955
// register to accomplish this.
19561956
e.erase(loc) // see pre-clobber comment below
19571957
r := e.findRegFor(v.Type)
1958-
x = v.copyInto(e.p)
1958+
x = v.copyIntoNoXPos(e.p)
19591959
e.set(r, vid, x, false, pos)
19601960
// Make sure we spill with the size of the slot, not the
19611961
// size of x (which might be wider due to our dropping

src/cmd/compile/internal/ssa/value.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,21 @@ func (v *Value) reset(op Op) {
212212

213213
// copyInto makes a new value identical to v and adds it to the end of b.
214214
func (v *Value) copyInto(b *Block) *Value {
215-
c := b.NewValue0(v.Pos, v.Op, v.Type)
215+
c := b.NewValue0(v.Pos, v.Op, v.Type) // Lose the position, this causes line number churn otherwise.
216+
c.Aux = v.Aux
217+
c.AuxInt = v.AuxInt
218+
c.AddArgs(v.Args...)
219+
for _, a := range v.Args {
220+
if a.Type.IsMemory() {
221+
v.Fatalf("can't move a value with a memory arg %s", v.LongString())
222+
}
223+
}
224+
return c
225+
}
226+
227+
// copyInto makes a new value identical to v and adds it to the end of b.
228+
func (v *Value) copyIntoNoXPos(b *Block) *Value {
229+
c := b.NewValue0(src.NoXPos, v.Op, v.Type) // Lose the position, this causes line number churn otherwise.
216230
c.Aux = v.Aux
217231
c.AuxInt = v.AuxInt
218232
c.AddArgs(v.Args...)

test/fixedbugs/issue18902.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// run
2+
// +build !nacl
3+
4+
// Copyright 2016 The Go Authors. All rights reserved.
5+
// Use of this source code is governed by a BSD-style
6+
// license that can be found in the LICENSE file.
7+
8+
// Runs a build -S to capture the assembly language
9+
// output, checks that the line numbers associated with
10+
// the stream of instructions do not change "too much".
11+
// The changes that fixes this (that reduces the amount
12+
// of change) does so by treating register spill, reload,
13+
// copy, and rematerializations as being "unimportant" and
14+
// just assigns them the line numbers of whatever "real"
15+
// instructions preceded them.
16+
17+
// nacl is excluded because this runs a compiler.
18+
19+
package main
20+
21+
import (
22+
"bufio"
23+
"bytes"
24+
"fmt"
25+
"os"
26+
"os/exec"
27+
"strconv"
28+
"strings"
29+
)
30+
31+
// updateEnv modifies env to ensure that key=val
32+
func updateEnv(env *[]string, key, val string) {
33+
if val != "" {
34+
var found bool
35+
key = key + "="
36+
for i, kv := range *env {
37+
if strings.HasPrefix(kv, key) {
38+
(*env)[i] = key + val
39+
found = true
40+
break
41+
}
42+
}
43+
if !found {
44+
*env = append(*env, key+val)
45+
}
46+
}
47+
}
48+
49+
func main() {
50+
testarch := os.Getenv("TESTARCH") // Targets other platform in test compilation.
51+
debug := os.Getenv("TESTDEBUG") != "" // Output the relevant assembly language.
52+
53+
cmd := exec.Command("go", "build", "-gcflags", "-S", "fixedbugs/issue18902b.go")
54+
var buf bytes.Buffer
55+
cmd.Stdout = &buf
56+
cmd.Stderr = &buf
57+
cmd.Env = os.Environ()
58+
59+
updateEnv(&cmd.Env, "GOARCH", testarch)
60+
61+
err := cmd.Run()
62+
if err != nil {
63+
fmt.Printf("%s\n%s", err, buf.Bytes())
64+
return
65+
}
66+
begin := "\"\".(*gcSortBuf).flush" // Text at beginning of relevant dissassembly.
67+
s := buf.String()
68+
i := strings.Index(s, begin)
69+
if i < 0 {
70+
fmt.Printf("Failed to find expected symbol %s in output\n%s\n", begin, s)
71+
return
72+
}
73+
s = s[i:]
74+
r := strings.NewReader(s)
75+
scanner := bufio.NewScanner(r)
76+
first := true // The first line after the begin text will be skipped
77+
beforeLineNumber := "issue18902b.go:" // Text preceding line number in each line.
78+
lbln := len(beforeLineNumber)
79+
80+
var scannedCount, changes, sumdiffs float64
81+
82+
prevVal := 0
83+
for scanner.Scan() {
84+
line := scanner.Text()
85+
if first {
86+
first = false
87+
continue
88+
}
89+
i = strings.Index(line, beforeLineNumber)
90+
if i < 0 {
91+
// Done reading lines
92+
if scannedCount < 200 { // When test was written, 251 lines observed on amd64
93+
fmt.Printf("Scanned only %d lines, was expecting more than 200", scannedCount)
94+
return
95+
}
96+
// Note: when test was written, before changes=92, after=50 (was 62 w/o rematerialization NoXPos in *Value.copyInto())
97+
// and before sumdiffs=784, after=180 (was 446 w/o rematerialization NoXPos in *Value.copyInto())
98+
// Set the dividing line between pass and fail at the midpoint.
99+
// Normalize against instruction count in case we unroll loops, etc.
100+
if changes/scannedCount >= (50+92)/(2*scannedCount) || sumdiffs/scannedCount >= (180+784)/(2*scannedCount) {
101+
fmt.Printf("Line numbers change too much, # of changes=%.f, sumdiffs=%.f, # of instructions=%.f\n", changes, sumdiffs, scannedCount)
102+
}
103+
return
104+
}
105+
scannedCount++
106+
i += lbln
107+
lineVal, err := strconv.Atoi(line[i : i+3])
108+
if err != nil {
109+
fmt.Printf("Expected 3-digit line number after %s in %s\n", beforeLineNumber, line)
110+
}
111+
if prevVal == 0 {
112+
prevVal = lineVal
113+
}
114+
diff := lineVal - prevVal
115+
if diff < 0 {
116+
diff = -diff
117+
}
118+
if diff != 0 {
119+
changes++
120+
sumdiffs += float64(diff)
121+
}
122+
// If things change too much, set environment variable TESTDEBUG to help figure out what's up.
123+
// The "before" behavior can be recreated in DebugFriendlySetPosFrom (currently in gc/ssa.go)
124+
// by inserting unconditional
125+
// s.SetPos(v.Pos)
126+
// at the top of the function.
127+
128+
if debug {
129+
fmt.Printf("%d %.f %.f %s\n", lineVal, changes, sumdiffs, line)
130+
}
131+
prevVal = lineVal
132+
}
133+
if err := scanner.Err(); err != nil {
134+
fmt.Println("Reading standard input:", err)
135+
return
136+
}
137+
}

test/fixedbugs/issue18902b.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// skip
2+
3+
// Copyright 2016 The Go Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE file.
6+
7+
package foo
8+
9+
import (
10+
"unsafe"
11+
)
12+
13+
type gcMaxTreeNodeVal uint64
14+
15+
var work struct {
16+
full uint64 // lock-free list of full blocks workbuf
17+
empty uint64 // lock-free list of empty blocks workbuf
18+
pad0 [64]uint8 // prevents false-sharing between full/empty and nproc/nwait
19+
bytesMarked uint64
20+
markrootNext uint32 // next markroot job
21+
markrootJobs uint32 // number of markroot jobs
22+
nproc uint32
23+
tstart int64
24+
nwait uint32
25+
ndone uint32
26+
}
27+
28+
type gcShardQueue1 struct {
29+
partial *workbuf
30+
full *workbuf
31+
n uintptr
32+
maxTree gcMaxTreeNodeVal
33+
}
34+
type gcShardQueue struct {
35+
gcShardQueue1
36+
pad [64 - unsafe.Sizeof(gcShardQueue1{})]byte
37+
}
38+
39+
const gcSortBufPointers = (64 << 10) / 8
40+
41+
type gcSortBuf struct {
42+
buf *gcSortArray
43+
tmp *gcSortArray
44+
n uintptr
45+
}
46+
47+
//go:notinheap
48+
type gcSortArray [gcSortBufPointers]uintptr
49+
50+
const (
51+
_DebugGC = 0
52+
_ConcurrentSweep = true
53+
_FinBlockSize = 4 * 1024
54+
sweepMinHeapDistance = 1024 * 1024
55+
gcShardShift = 2 + 20
56+
gcShardBytes = 1 << gcShardShift
57+
)
58+
59+
//go:notinheap
60+
type mheap struct {
61+
shardQueues []gcShardQueue
62+
_ uint32 // align uint64 fields on 32-bit for atomics
63+
pagesInUse uint64 // pages of spans in stats _MSpanInUse; R/W with mheap.lock
64+
spanBytesAlloc uint64 // bytes of spans allocated this cycle; updated atomically
65+
pagesSwept uint64 // pages swept this cycle; updated atomically
66+
sweepPagesPerByte float64 // proportional sweep ratio; written with lock, read without
67+
largefree uint64 // bytes freed for large objects (>maxsmallsize)
68+
nlargefree uint64 // number of frees for large objects (>maxsmallsize)
69+
nsmallfree [67]uint64 // number of frees for small objects (<=maxsmallsize)
70+
bitmap uintptr // Points to one byte past the end of the bitmap
71+
bitmap_mapped uintptr
72+
arena_start uintptr
73+
arena_used uintptr // always mHeap_Map{Bits,Spans} before updating
74+
arena_end uintptr
75+
arena_reserved bool
76+
}
77+
78+
var mheap_ mheap
79+
80+
type lfnode struct {
81+
next uint64
82+
pushcnt uintptr
83+
}
84+
type workbufhdr struct {
85+
node lfnode // must be first
86+
next *workbuf
87+
nobj int
88+
}
89+
90+
//go:notinheap
91+
type workbuf struct {
92+
workbufhdr
93+
obj [(2048 - unsafe.Sizeof(workbufhdr{})) / 8]uintptr
94+
}
95+
96+
//go:noinline
97+
func (b *workbuf) checkempty() {
98+
if b.nobj != 0 {
99+
b.nobj = 0
100+
}
101+
}
102+
func putempty(b *workbuf) {
103+
b.checkempty()
104+
lfstackpush(&work.empty, &b.node)
105+
}
106+
107+
//go:noinline
108+
func lfstackpush(head *uint64, node *lfnode) {
109+
}
110+
111+
//go:noinline
112+
func (q *gcShardQueue) add(qidx uintptr, ptrs []uintptr, spare *workbuf) *workbuf {
113+
return spare
114+
}
115+
116+
func (b *gcSortBuf) flush() {
117+
if b.n == 0 {
118+
return
119+
}
120+
const sortDigitBits = 11
121+
buf, tmp := b.buf[:b.n], b.tmp[:b.n]
122+
moreBits := true
123+
for shift := uint(gcShardShift); moreBits; shift += sortDigitBits {
124+
const k = 1 << sortDigitBits
125+
var pos [k]uint16
126+
nshift := shift + sortDigitBits
127+
nbits := buf[0] >> nshift
128+
moreBits = false
129+
for _, v := range buf {
130+
pos[(v>>shift)%k]++
131+
moreBits = moreBits || v>>nshift != nbits
132+
}
133+
var sum uint16
134+
for i, count := range &pos {
135+
pos[i] = sum
136+
sum += count
137+
}
138+
for _, v := range buf {
139+
digit := (v >> shift) % k
140+
tmp[pos[digit]] = v
141+
pos[digit]++
142+
}
143+
buf, tmp = tmp, buf
144+
}
145+
start := mheap_.arena_start
146+
i0 := 0
147+
shard0 := (buf[0] - start) / gcShardBytes
148+
var spare *workbuf
149+
for i, p := range buf {
150+
shard := (p - start) / gcShardBytes
151+
if shard != shard0 {
152+
spare = mheap_.shardQueues[shard0].add(shard0, buf[i0:i], spare)
153+
i0, shard0 = i, shard
154+
}
155+
}
156+
spare = mheap_.shardQueues[shard0].add(shard0, buf[i0:], spare)
157+
b.n = 0
158+
if spare != nil {
159+
putempty(spare)
160+
}
161+
}

0 commit comments

Comments
 (0)