Skip to content

Commit 9d622d8

Browse files
randall77jproberts
authored andcommitted
cmd/compile: stop interface conversions for generic method calls from allocating
Let T be a type parameter, and say we instantiate it with S, a type that isn't pointer-like (e.g. a pair of ints, or as in 50182, a slice). Then to call a method m on a variable of type T, the compiler does essentially: var v T = ... i := (interface{m()})(v) i.m() The conversion at that second line allocates, as we need to make the data word for an interface. And in the general case, that interface may live an arbitrarily long time. But in this case, we know it doesn't. The data word of i has type *S. When we call i.m, we can't call S.m directly. It is expecting an S, not a *S. We call through a wrapper defined on *S, which looks like: func (p *S) m() { var s S = *p s.m() } The value passed in for p is exactly the data word mentioned above. It never escapes anywhere - the wrapper copies a type S variable out of *p and p is dead after that. That means that in the situation where we build an interface for the explicit purpose of calling a method on it, and use that built interface nowhere else, the allocation of the data word for that interface is known to die before the call returns and thus can be stack allocated. One tricky case is that although the allocation of the backing store of the interface conversion doesn't escape, pointers we store *inside* that allocation might escape (in fact they definitely will, unless we can devirtualize the receiver). Fixes golang#50182 Change-Id: I40e893955c2e6871c54ccecf1b9f0cae17871b0d Reviewed-on: https://go-review.googlesource.com/c/go/+/378178 Trust: Keith Randall <[email protected]> Run-TryBot: Keith Randall <[email protected]> Trust: Dan Scales <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]> Reviewed-by: David Chase <[email protected]> Reviewed-by: Dan Scales <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 49855f0 commit 9d622d8

File tree

4 files changed

+83
-5
lines changed

4 files changed

+83
-5
lines changed

src/cmd/compile/internal/escape/escape.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ func (b *batch) finish(fns []*ir.Func) {
293293
// TODO(mdempsky): Update tests to expect this.
294294
goDeferWrapper := n.Op() == ir.OCLOSURE && n.(*ir.ClosureExpr).Func.Wrapper()
295295

296+
if n.Op() == ir.OCONVIDATA && n.(*ir.ConvExpr).NonEscaping {
297+
// The allocation for the data word of an interface is known to not escape.
298+
// See issue 50182.
299+
// (But we do still need to process that allocation, as pointers inside
300+
// the data word may escape.)
301+
loc.escapes = false
302+
}
303+
296304
if loc.escapes {
297305
if n.Op() == ir.ONAME {
298306
if base.Flag.CompilingRuntime {

src/cmd/compile/internal/ir/expr.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ func (n *ConstExpr) Val() constant.Value { return n.val }
250250
// It may end up being a value or a type.
251251
type ConvExpr struct {
252252
miniExpr
253-
X Node
253+
X Node
254+
NonEscaping bool // The allocation needed for the conversion to interface is known not to escape
254255
}
255256

256257
func NewConvExpr(pos src.XPos, op Op, typ *types.Type, x Node) *ConvExpr {

src/cmd/compile/internal/noder/stencil.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,7 +1228,11 @@ func (g *genInst) dictPass(info *instInfo) {
12281228
// we do a type assert to the type bound.
12291229
mse.X = assertToBound(info, info.dictParam, m.Pos(), mse.X, dst)
12301230
} else {
1231-
mse.X = convertUsingDictionary(info, info.dictParam, m.Pos(), mse.X, m, dst)
1231+
mse.X = convertUsingDictionary(info, info.dictParam, m.Pos(), mse.X, m, dst, true)
1232+
// Note: we set nonEscaping==true, because we can assume the backing store for the
1233+
// interface conversion doesn't escape. The method call will immediately go to
1234+
// a wrapper function which copies all the data out of the interface value.
1235+
// (It only matters for non-pointer-shaped interface conversions. See issue 50182.)
12321236
}
12331237
transformDot(mse, false)
12341238
}
@@ -1254,7 +1258,7 @@ func (g *genInst) dictPass(info *instInfo) {
12541258
// Note: x's argument is still typed as a type parameter.
12551259
// m's argument now has an instantiated type.
12561260
if mce.X.Type().HasShape() || (mce.X.Type().IsInterface() && m.Type().HasShape()) {
1257-
m = convertUsingDictionary(info, info.dictParam, m.Pos(), m.(*ir.ConvExpr).X, m, m.Type())
1261+
m = convertUsingDictionary(info, info.dictParam, m.Pos(), m.(*ir.ConvExpr).X, m, m.Type(), false)
12581262
}
12591263
case ir.ODOTTYPE, ir.ODOTTYPE2:
12601264
if !m.Type().HasShape() {
@@ -1347,7 +1351,9 @@ func findDictType(info *instInfo, t *types.Type) int {
13471351
// type dst, by returning a new set of nodes that make use of a dictionary entry. in is the
13481352
// instantiated node of the CONVIFACE node or XDOT node (for a bound method call) that is causing the
13491353
// conversion.
1350-
func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v ir.Node, in ir.Node, dst *types.Type) ir.Node {
1354+
// If nonEscaping is true, the caller guarantees that the backing store needed for the interface data
1355+
// word will not escape.
1356+
func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v ir.Node, in ir.Node, dst *types.Type, nonEscaping bool) ir.Node {
13511357
assert(v.Type().HasShape() || v.Type().IsInterface() && in.Type().HasShape())
13521358
assert(dst.IsInterface())
13531359

@@ -1417,6 +1423,7 @@ func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v
14171423
// Figure out what the data field of the interface will be.
14181424
data := ir.NewConvExpr(pos, ir.OCONVIDATA, nil, v)
14191425
typed(types.Types[types.TUNSAFEPTR], data)
1426+
data.NonEscaping = nonEscaping
14201427

14211428
// Build an interface from the type and data parts.
14221429
var i ir.Node = ir.NewBinaryExpr(pos, ir.OEFACE, rt, data)
@@ -2147,7 +2154,7 @@ func (g *genInst) buildClosure2(info *instInfo, m ir.Node) ir.Node {
21472154
// the type bound.
21482155
rcvr = assertToBound(info, dictVar, pos, rcvr, dst)
21492156
} else {
2150-
rcvr = convertUsingDictionary(info, dictVar, pos, rcvr, m, dst)
2157+
rcvr = convertUsingDictionary(info, dictVar, pos, rcvr, m, dst, false)
21512158
}
21522159
dot := ir.NewSelectorExpr(pos, ir.ODOTINTER, rcvr, m.(*ir.SelectorExpr).Sel)
21532160
dot.Selection = typecheck.Lookdot1(dot, dot.Sel, dot.X.Type(), dot.X.Type().AllMethods(), 1)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2021 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 test
6+
7+
import (
8+
"fmt"
9+
"sort"
10+
"testing"
11+
)
12+
13+
// Test that calling methods on generic types doesn't cause allocations.
14+
func genericSorted[T sort.Interface](data T) bool {
15+
n := data.Len()
16+
for i := n - 1; i > 0; i-- {
17+
if data.Less(i, i-1) {
18+
return false
19+
}
20+
}
21+
return true
22+
}
23+
func TestGenericSorted(t *testing.T) {
24+
var data = sort.IntSlice{-10, -5, 0, 1, 2, 3, 5, 7, 11, 100, 100, 100, 1000, 10000}
25+
f := func() {
26+
genericSorted(data)
27+
}
28+
if n := testing.AllocsPerRun(10, f); n > 0 {
29+
t.Errorf("got %f allocs, want 0", n)
30+
}
31+
}
32+
33+
// Test that escape analysis correctly tracks escaping inside of methods
34+
// called on generic types.
35+
type fooer interface {
36+
foo()
37+
}
38+
type P struct {
39+
p *int
40+
q int
41+
}
42+
43+
var esc []*int
44+
45+
func (p P) foo() {
46+
esc = append(esc, p.p) // foo escapes the pointer from inside of p
47+
}
48+
func f[T fooer](t T) {
49+
t.foo()
50+
}
51+
func TestGenericEscape(t *testing.T) {
52+
for i := 0; i < 4; i++ {
53+
var x int = 77 + i
54+
var p P = P{p: &x}
55+
f(p)
56+
}
57+
for i, p := range esc {
58+
if got, want := *p, 77+i; got != want {
59+
panic(fmt.Sprintf("entry %d: got %d, want %d", i, got, want))
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)