Skip to content

Commit 23e8f43

Browse files
committed
cmd/compile: restore return-in-loop loopvar optimization
but this time, correctly. children of Returns can have For/Range loops in them, and those must be visited. Includes test to verify that the optimization occurs, and also that the problematic case that broke the original optimization is now correctly handled. Change-Id: If5a94fd51c862d4bfb318fec78456b7b202f3fcd Reviewed-on: https://go-review.googlesource.com/c/go/+/472355 Run-TryBot: David Chase <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]>
1 parent ab28b83 commit 23e8f43

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

src/cmd/compile/internal/loopvar/loopvar.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ func ForCapture(fn *ir.Func) []*ir.Name {
5050
// will be transformed.
5151
possiblyLeaked := make(map[*ir.Name]bool)
5252

53+
// these enable an optimization of "escape" under return statements
54+
loopDepth := 0
55+
returnInLoopDepth := 0
56+
5357
// noteMayLeak is called for candidate variables in for range/3-clause, and
5458
// adds them (mapped to false) to possiblyLeaked.
5559
noteMayLeak := func(x ir.Node) {
@@ -95,6 +99,11 @@ func ForCapture(fn *ir.Func) []*ir.Name {
9599
scanChildrenThenTransform = func(n ir.Node) bool {
96100
switch x := n.(type) {
97101
case *ir.ClosureExpr:
102+
if returnInLoopDepth >= loopDepth {
103+
// This expression is a child of a return, which escapes all loops above
104+
// the return, but not those between this expression and the return.
105+
break
106+
}
98107
for _, cv := range x.Func.ClosureVars {
99108
v := cv.Canonical()
100109
if _, ok := possiblyLeaked[v]; ok {
@@ -103,6 +112,11 @@ func ForCapture(fn *ir.Func) []*ir.Name {
103112
}
104113

105114
case *ir.AddrExpr:
115+
if returnInLoopDepth >= loopDepth {
116+
// This expression is a child of a return, which escapes all loops above
117+
// the return, but not those between this expression and the return.
118+
break
119+
}
106120
// Explicitly note address-taken so that return-statements can be excluded
107121
y := ir.OuterValue(x.X)
108122
if y.Op() != ir.ONAME {
@@ -119,6 +133,11 @@ func ForCapture(fn *ir.Func) []*ir.Name {
119133
}
120134
}
121135

136+
case *ir.ReturnStmt:
137+
savedRILD := returnInLoopDepth
138+
returnInLoopDepth = loopDepth
139+
defer func() { returnInLoopDepth = savedRILD }()
140+
122141
case *ir.RangeStmt:
123142
if !(x.Def && x.DistinctVars) {
124143
// range loop must define its iteration variables AND have distinctVars.
@@ -127,7 +146,9 @@ func ForCapture(fn *ir.Func) []*ir.Name {
127146
}
128147
noteMayLeak(x.Key)
129148
noteMayLeak(x.Value)
149+
loopDepth++
130150
ir.DoChildren(n, scanChildrenThenTransform)
151+
loopDepth--
131152
x.Key = maybeReplaceVar(x.Key, x)
132153
x.Value = maybeReplaceVar(x.Value, x)
133154
x.DistinctVars = false
@@ -138,7 +159,9 @@ func ForCapture(fn *ir.Func) []*ir.Name {
138159
break
139160
}
140161
forAllDefInInit(x, noteMayLeak)
162+
loopDepth++
141163
ir.DoChildren(n, scanChildrenThenTransform)
164+
loopDepth--
142165
var leaked []*ir.Name
143166
// Collect the leaking variables for the much-more-complex transformation.
144167
forAllDefInInit(x, func(z ir.Node) {

src/cmd/compile/internal/loopvar/loopvar_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,41 @@ func TestLoopVarHashes(t *testing.T) {
206206
t.Errorf("Did not see expected value of m run")
207207
}
208208
}
209+
210+
func TestLoopVarOpt(t *testing.T) {
211+
switch runtime.GOOS {
212+
case "linux", "darwin":
213+
default:
214+
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
215+
}
216+
switch runtime.GOARCH {
217+
case "amd64", "arm64":
218+
default:
219+
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
220+
}
221+
222+
testenv.MustHaveGoBuild(t)
223+
gocmd := testenv.GoToolPath(t)
224+
225+
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-d=loopvar=2", "opt.go")
226+
cmd.Dir = filepath.Join("testdata")
227+
228+
b, err := cmd.CombinedOutput()
229+
m := string(b)
230+
231+
t.Logf(m)
232+
233+
yCount := strings.Count(m, "opt.go:16:6: transformed loop variable private escapes (loop inlined into ./opt.go:30)")
234+
nCount := strings.Count(m, "shared")
235+
236+
if yCount != 1 {
237+
t.Errorf("yCount=%d != 1", yCount)
238+
}
239+
if nCount > 0 {
240+
t.Errorf("nCount=%d > 0", nCount)
241+
}
242+
if err != nil {
243+
t.Errorf("err=%v != nil", err)
244+
}
245+
246+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2023 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 main
6+
7+
import (
8+
"fmt"
9+
"os"
10+
)
11+
12+
var is []func() int
13+
14+
func inline(j, k int) []*int {
15+
var a []*int
16+
for private := j; private < k; private++ {
17+
a = append(a, &private)
18+
}
19+
return a
20+
21+
}
22+
23+
//go:noinline
24+
func notinline(j, k int) ([]*int, *int) {
25+
for shared := j; shared < k; shared++ {
26+
if shared == k/2 {
27+
// want the call inlined, want "private" in that inline to be transformed,
28+
// (believe it ends up on init node of the return).
29+
// but do not want "shared" transformed,
30+
return inline(j, k), &shared
31+
}
32+
}
33+
return nil, &j
34+
}
35+
36+
func main() {
37+
a, p := notinline(2, 9)
38+
fmt.Printf("a[0]=%d,*p=%d\n", *a[0], *p)
39+
if *a[0] != 2 {
40+
os.Exit(1)
41+
}
42+
}

0 commit comments

Comments
 (0)