Skip to content

Commit e779bfa

Browse files
committed
cmd/compile: better modeling of escape across loop levels
Brief background on "why heap allocate". Things can be forced to the heap for the following reasons: 1) address published, hence lifetime unknown. 2) size unknown/too large, cannot be stack allocated 3) multiplicity unknown/too large, cannot be stack allocated 4) reachable from heap (not necessarily published) The bug here is a case of failing to enforce 4) when an object Y was reachable from a heap allocation X forced because of 3). It was found in the case of a closure allocated within a loop (X) and assigned to a variable outside the loop (multiplicity unknown) where the closure also captured a map (Y) declared outside the loop (reachable from heap). Note the variable declared outside the loop (Y) is not published, has known size, and known multiplicity (one). The only reason for heap allocation is that it was reached from a heap allocated item (X), but because that was not forced by publication, it has to be tracked by loop level, but escape-loop level was not tracked and thus a bug results. The fix is that when a heap allocation is newly discovered, use its looplevel as the minimum loop level for downstream escape flooding. Every attempt to generalize this bug to X-in-loop- references-Y-outside loop succeeded, so the fix was aimed to be general. Anywhere that loop level forces heap allocation, the loop level is tracked. This is not yet tested for all possible X and Y, but it is correctness- conservative and because it caused only one trivial regression in the escape tests, it is probably also performance-conservative. The new test checks the following: 1) in the map case, that if fn escapes, so does the map. 2) in the map case, if fn does not escape, neither does the map. 3) in the &x case, that if fn escapes, so does &x. 4) in the &x case, if fn does not escape, neither does &x. Fixes #13799. Change-Id: Ie280bef2bb86ec869c7c206789d0b68f080c3fdb Reviewed-on: https://go-review.googlesource.com/18234 Run-TryBot: David Chase <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 0409328 commit e779bfa

File tree

4 files changed

+232
-18
lines changed

4 files changed

+232
-18
lines changed

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

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,13 @@ func (l Level) guaranteedDereference() int {
299299
}
300300

301301
type NodeEscState struct {
302-
Curfn *Node
303-
Escflowsrc *NodeList // flow(this, src)
304-
Escretval *NodeList // on OCALLxxx, list of dummy return values
305-
Escloopdepth int32 // -1: global, 0: return variables, 1:function top level, increased inside function for every loop or label to mark scopes
306-
Esclevel Level
307-
Walkgen uint32
302+
Curfn *Node
303+
Escflowsrc *NodeList // flow(this, src)
304+
Escretval *NodeList // on OCALLxxx, list of dummy return values
305+
Escloopdepth int32 // -1: global, 0: return variables, 1:function top level, increased inside function for every loop or label to mark scopes
306+
Esclevel Level
307+
Walkgen uint32
308+
Maxextraloopdepth int32
308309
}
309310

310311
func (e *EscState) nodeEscState(n *Node) *NodeEscState {
@@ -1579,7 +1580,13 @@ func funcOutputAndInput(dst, src *Node) bool {
15791580
src.Op == ONAME && src.Class == PPARAM && src.Name.Curfn == dst.Name.Curfn
15801581
}
15811582

1583+
const NOTALOOPDEPTH = -1
1584+
15821585
func escwalk(e *EscState, level Level, dst *Node, src *Node) {
1586+
escwalkBody(e, level, dst, src, NOTALOOPDEPTH)
1587+
}
1588+
1589+
func escwalkBody(e *EscState, level Level, dst *Node, src *Node, extraloopdepth int32) {
15831590
if src.Op == OLITERAL {
15841591
return
15851592
}
@@ -1590,16 +1597,29 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
15901597
// convergence.
15911598
level = level.min(srcE.Esclevel)
15921599
if level == srcE.Esclevel {
1593-
return
1600+
// Have we been here already with an extraloopdepth,
1601+
// or is the extraloopdepth provided no improvement on
1602+
// what's already been seen?
1603+
if srcE.Maxextraloopdepth >= extraloopdepth || srcE.Escloopdepth >= extraloopdepth {
1604+
return
1605+
}
1606+
srcE.Maxextraloopdepth = extraloopdepth
15941607
}
1608+
} else { // srcE.Walkgen < e.walkgen -- first time, reset this.
1609+
srcE.Maxextraloopdepth = NOTALOOPDEPTH
15951610
}
15961611

15971612
srcE.Walkgen = e.walkgen
15981613
srcE.Esclevel = level
1614+
modSrcLoopdepth := srcE.Escloopdepth
1615+
1616+
if extraloopdepth > modSrcLoopdepth {
1617+
modSrcLoopdepth = extraloopdepth
1618+
}
15991619

16001620
if Debug['m'] > 1 {
1601-
fmt.Printf("escwalk: level:%d depth:%d %.*s op=%v %v(%v) scope:%v[%d]\n",
1602-
level, e.pdepth, e.pdepth, "\t\t\t\t\t\t\t\t\t\t", Oconv(int(src.Op), 0), Nconv(src, obj.FmtShort), Jconv(src, obj.FmtShort), e.curfnSym(src), srcE.Escloopdepth)
1621+
fmt.Printf("escwalk: level:%d depth:%d %.*s op=%v %v(%v) scope:%v[%d] extraloopdepth=%v\n",
1622+
level, e.pdepth, e.pdepth, "\t\t\t\t\t\t\t\t\t\t", Oconv(int(src.Op), 0), Nconv(src, obj.FmtShort), Jconv(src, obj.FmtShort), e.curfnSym(src), srcE.Escloopdepth, extraloopdepth)
16031623
}
16041624

16051625
e.pdepth++
@@ -1638,7 +1658,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
16381658
}
16391659
}
16401660

1641-
leaks = level.int() <= 0 && level.guaranteedDereference() <= 0 && dstE.Escloopdepth < srcE.Escloopdepth
1661+
leaks = level.int() <= 0 && level.guaranteedDereference() <= 0 && dstE.Escloopdepth < modSrcLoopdepth
16421662

16431663
switch src.Op {
16441664
case ONAME:
@@ -1650,7 +1670,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
16501670
Warnl(int(src.Lineno), "leaking param content: %v", Nconv(src, obj.FmtShort))
16511671
} else {
16521672
Warnl(int(src.Lineno), "leaking param content: %v level=%v dst.eld=%v src.eld=%v dst=%v",
1653-
Nconv(src, obj.FmtShort), level, dstE.Escloopdepth, srcE.Escloopdepth, Nconv(dst, obj.FmtShort))
1673+
Nconv(src, obj.FmtShort), level, dstE.Escloopdepth, modSrcLoopdepth, Nconv(dst, obj.FmtShort))
16541674
}
16551675
}
16561676
} else {
@@ -1660,7 +1680,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
16601680
Warnl(int(src.Lineno), "leaking param: %v", Nconv(src, obj.FmtShort))
16611681
} else {
16621682
Warnl(int(src.Lineno), "leaking param: %v level=%v dst.eld=%v src.eld=%v dst=%v",
1663-
Nconv(src, obj.FmtShort), level, dstE.Escloopdepth, srcE.Escloopdepth, Nconv(dst, obj.FmtShort))
1683+
Nconv(src, obj.FmtShort), level, dstE.Escloopdepth, modSrcLoopdepth, Nconv(dst, obj.FmtShort))
16641684
}
16651685
}
16661686
}
@@ -1686,15 +1706,17 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
16861706
}
16871707
if Debug['m'] > 1 {
16881708
Warnl(int(src.Lineno), "%v escapes to heap, level=%v, dst.eld=%v, src.eld=%v",
1689-
Nconv(p, obj.FmtShort), level, dstE.Escloopdepth, srcE.Escloopdepth)
1709+
Nconv(p, obj.FmtShort), level, dstE.Escloopdepth, modSrcLoopdepth)
16901710
} else {
16911711
Warnl(int(src.Lineno), "%v escapes to heap", Nconv(p, obj.FmtShort))
16921712
}
16931713
}
1714+
escwalkBody(e, level.dec(), dst, src.Left, modSrcLoopdepth)
1715+
extraloopdepth = modSrcLoopdepth // passes to recursive case, seems likely a no-op
1716+
} else {
1717+
escwalk(e, level.dec(), dst, src.Left)
16941718
}
16951719

1696-
escwalk(e, level.dec(), dst, src.Left)
1697-
16981720
case OAPPEND:
16991721
escwalk(e, level, dst, src.List.N)
17001722

@@ -1704,6 +1726,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
17041726
if Debug['m'] != 0 {
17051727
Warnl(int(src.Lineno), "%v escapes to heap", Nconv(src, obj.FmtShort))
17061728
}
1729+
extraloopdepth = modSrcLoopdepth
17071730
}
17081731
// similar to a slice arraylit and its args.
17091732
level = level.dec()
@@ -1737,6 +1760,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
17371760
if Debug['m'] != 0 {
17381761
Warnl(int(src.Lineno), "%v escapes to heap", Nconv(src, obj.FmtShort))
17391762
}
1763+
extraloopdepth = modSrcLoopdepth
17401764
}
17411765

17421766
case ODOT,
@@ -1778,7 +1802,7 @@ func escwalk(e *EscState, level Level, dst *Node, src *Node) {
17781802
recurse:
17791803
level = level.copy()
17801804
for ll := srcE.Escflowsrc; ll != nil; ll = ll.Next {
1781-
escwalk(e, level, dst, ll.N)
1805+
escwalkBody(e, level, dst, ll.N, extraloopdepth)
17821806
}
17831807

17841808
e.pdepth--

test/escape2.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@ func foo126() {
12041204
// loopdepth 1
12051205
var i int // ERROR "moved to heap: i$"
12061206
func() { // ERROR "foo126 func literal does not escape$"
1207-
px = &i // ERROR "&i escapes to heap$"
1207+
px = &i // ERROR "&i escapes to heap$" "leaking closure reference i"
12081208
}()
12091209
}
12101210
_ = px

test/escape2n.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@ func foo126() {
12041204
// loopdepth 1
12051205
var i int // ERROR "moved to heap: i$"
12061206
func() { // ERROR "foo126 func literal does not escape$"
1207-
px = &i // ERROR "&i escapes to heap$"
1207+
px = &i // ERROR "&i escapes to heap$" "leaking closure reference i"
12081208
}()
12091209
}
12101210
_ = px

test/fixedbugs/issue13799.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// errorcheck -0 -m -l
2+
3+
// Copyright 2015 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+
// Test, using compiler diagnostic flags, that the escape analysis is working.
8+
// Compiles but does not run. Inlining is disabled.
9+
// Registerization is disabled too (-N), which should
10+
// have no effect on escape analysis.
11+
12+
package main
13+
14+
import "fmt"
15+
16+
func main() {
17+
// Just run test over and over again. This main func is just for
18+
// convenience; if test were the main func, we could also trigger
19+
// the panic just by running the program over and over again
20+
// (sometimes it takes 1 time, sometimes it takes ~4,000+).
21+
for iter := 0; ; iter++ {
22+
if iter%50 == 0 {
23+
fmt.Println(iter) // ERROR "iter escapes to heap$" "main ... argument does not escape$"
24+
}
25+
test1(iter)
26+
test2(iter)
27+
test3(iter)
28+
test4(iter)
29+
test5(iter)
30+
test6(iter)
31+
}
32+
}
33+
34+
func test1(iter int) {
35+
36+
const maxI = 500
37+
m := make(map[int][]int) // ERROR "make\(map\[int\]\[\]int\) escapes to heap$"
38+
39+
// The panic seems to be triggered when m is modified inside a
40+
// closure that is both recursively called and reassigned to in a
41+
// loop.
42+
43+
// Cause of bug -- escape of closure failed to escape (shared) data structures
44+
// of map. Assign to fn declared outside of loop triggers escape of closure.
45+
// Heap -> stack pointer eventually causes badness when stack reallocation
46+
// occurs.
47+
48+
var fn func() // ERROR "moved to heap: fn$"
49+
for i := 0; i < maxI; i++ { // ERROR "moved to heap: i$"
50+
// var fn func() // this makes it work, because fn stays off heap
51+
j := 0 // ERROR "moved to heap: j$"
52+
fn = func() { // ERROR "func literal escapes to heap$"
53+
m[i] = append(m[i], 0) // ERROR "&i escapes to heap$"
54+
if j < 25 { // ERROR "&j escapes to heap$"
55+
j++
56+
fn() // ERROR "&fn escapes to heap$"
57+
}
58+
}
59+
fn()
60+
}
61+
62+
if len(m) != maxI {
63+
panic(fmt.Sprintf("iter %d: maxI = %d, len(m) = %d", iter, maxI, len(m))) // ERROR "iter escapes to heap$" "len\(m\) escapes to heap$" "maxI escapes to heap$" "test1 ... argument does not escape$"
64+
}
65+
}
66+
67+
func test2(iter int) {
68+
69+
const maxI = 500
70+
m := make(map[int][]int) // ERROR "test2 make\(map\[int\]\[\]int\) does not escape$"
71+
72+
// var fn func()
73+
for i := 0; i < maxI; i++ {
74+
var fn func() // this makes it work, because fn stays off heap
75+
j := 0
76+
fn = func() { // ERROR "test2 func literal does not escape$"
77+
m[i] = append(m[i], 0)
78+
if j < 25 {
79+
j++
80+
fn()
81+
}
82+
}
83+
fn()
84+
}
85+
86+
if len(m) != maxI {
87+
panic(fmt.Sprintf("iter %d: maxI = %d, len(m) = %d", iter, maxI, len(m))) // ERROR "iter escapes to heap$" "len\(m\) escapes to heap$" "maxI escapes to heap$" "test2 ... argument does not escape$"
88+
}
89+
}
90+
91+
func test3(iter int) {
92+
93+
const maxI = 500
94+
var x int // ERROR "moved to heap: x$"
95+
m := &x // ERROR "&x escapes to heap$"
96+
97+
var fn func() // ERROR "moved to heap: fn$"
98+
for i := 0; i < maxI; i++ {
99+
// var fn func() // this makes it work, because fn stays off heap
100+
j := 0 // ERROR "moved to heap: j$"
101+
fn = func() { // ERROR "func literal escapes to heap$"
102+
if j < 100 { // ERROR "&j escapes to heap$"
103+
j++
104+
fn() // ERROR "&fn escapes to heap$"
105+
} else {
106+
*m = *m + 1
107+
}
108+
}
109+
fn()
110+
}
111+
112+
if *m != maxI {
113+
panic(fmt.Sprintf("iter %d: maxI = %d, *m = %d", iter, maxI, *m)) // ERROR "\*m escapes to heap$" "iter escapes to heap$" "maxI escapes to heap$" "test3 ... argument does not escape$"
114+
}
115+
}
116+
117+
func test4(iter int) {
118+
119+
const maxI = 500
120+
var x int
121+
m := &x // ERROR "test4 &x does not escape$"
122+
123+
// var fn func()
124+
for i := 0; i < maxI; i++ {
125+
var fn func() // this makes it work, because fn stays off heap
126+
j := 0
127+
fn = func() { // ERROR "test4 func literal does not escape$"
128+
if j < 100 {
129+
j++
130+
fn()
131+
} else {
132+
*m = *m + 1
133+
}
134+
}
135+
fn()
136+
}
137+
138+
if *m != maxI {
139+
panic(fmt.Sprintf("iter %d: maxI = %d, *m = %d", iter, maxI, *m)) // ERROR "\*m escapes to heap$" "iter escapes to heap$" "maxI escapes to heap$" "test4 ... argument does not escape$"
140+
}
141+
}
142+
143+
type str struct {
144+
m *int
145+
}
146+
147+
func recur1(j int, s *str) { // ERROR "recur1 s does not escape"
148+
if j < 100 {
149+
j++
150+
recur1(j, s)
151+
} else {
152+
*s.m++
153+
}
154+
}
155+
156+
func test5(iter int) {
157+
158+
const maxI = 500
159+
var x int // ERROR "moved to heap: x$"
160+
m := &x // ERROR "&x escapes to heap$"
161+
162+
var fn *str
163+
for i := 0; i < maxI; i++ {
164+
// var fn *str // this makes it work, because fn stays off heap
165+
fn = &str{m} // ERROR "&str literal escapes to heap"
166+
recur1(0, fn)
167+
}
168+
169+
if *m != maxI {
170+
panic(fmt.Sprintf("iter %d: maxI = %d, *m = %d", iter, maxI, *m)) // ERROR "\*m escapes to heap$" "iter escapes to heap$" "maxI escapes to heap$" "test5 ... argument does not escape$"
171+
}
172+
}
173+
174+
func test6(iter int) {
175+
176+
const maxI = 500
177+
var x int
178+
m := &x // ERROR "&x does not escape$"
179+
180+
// var fn *str
181+
for i := 0; i < maxI; i++ {
182+
var fn *str // this makes it work, because fn stays off heap
183+
fn = &str{m} // ERROR "&str literal does not escape"
184+
recur1(0, fn)
185+
}
186+
187+
if *m != maxI {
188+
panic(fmt.Sprintf("iter %d: maxI = %d, *m = %d", iter, maxI, *m)) // ERROR "\*m escapes to heap$" "iter escapes to heap$" "maxI escapes to heap$" "test6 ... argument does not escape$"
189+
}
190+
}

0 commit comments

Comments
 (0)