aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKeith Randall <khr@golang.org>2022-01-12 16:11:35 -0800
committerKeith Randall <khr@golang.org>2022-01-13 23:35:37 +0000
commite550c3054586a224d949cc8fa030bac0887bee51 (patch)
tree58c801f8818ce242c7f6094302361d333c99a855
parent4f6f68ee4b50162d3bd01efb9b8a5f6a10dc54af (diff)
downloadgo-e550c3054586a224d949cc8fa030bac0887bee51.tar.gz
go-e550c3054586a224d949cc8fa030bac0887bee51.zip
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 #50182 Change-Id: I40e893955c2e6871c54ccecf1b9f0cae17871b0d Reviewed-on: https://go-review.googlesource.com/c/go/+/378178 Trust: Keith Randall <khr@golang.org> Run-TryBot: Keith Randall <khr@golang.org> Trust: Dan Scales <danscales@google.com> Reviewed-by: Matthew Dempsky <mdempsky@google.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Dan Scales <danscales@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
-rw-r--r--src/cmd/compile/internal/escape/escape.go8
-rw-r--r--src/cmd/compile/internal/ir/expr.go3
-rw-r--r--src/cmd/compile/internal/noder/stencil.go15
-rw-r--r--src/cmd/compile/internal/test/issue50182_test.go62
4 files changed, 83 insertions, 5 deletions
diff --git a/src/cmd/compile/internal/escape/escape.go b/src/cmd/compile/internal/escape/escape.go
index 61e0121a40..c2145bdf91 100644
--- a/src/cmd/compile/internal/escape/escape.go
+++ b/src/cmd/compile/internal/escape/escape.go
@@ -293,6 +293,14 @@ func (b *batch) finish(fns []*ir.Func) {
// TODO(mdempsky): Update tests to expect this.
goDeferWrapper := n.Op() == ir.OCLOSURE && n.(*ir.ClosureExpr).Func.Wrapper()
+ if n.Op() == ir.OCONVIDATA && n.(*ir.ConvExpr).NonEscaping {
+ // The allocation for the data word of an interface is known to not escape.
+ // See issue 50182.
+ // (But we do still need to process that allocation, as pointers inside
+ // the data word may escape.)
+ loc.escapes = false
+ }
+
if loc.escapes {
if n.Op() == ir.ONAME {
if base.Flag.CompilingRuntime {
diff --git a/src/cmd/compile/internal/ir/expr.go b/src/cmd/compile/internal/ir/expr.go
index f526d987a7..68303c0581 100644
--- a/src/cmd/compile/internal/ir/expr.go
+++ b/src/cmd/compile/internal/ir/expr.go
@@ -250,7 +250,8 @@ func (n *ConstExpr) Val() constant.Value { return n.val }
// It may end up being a value or a type.
type ConvExpr struct {
miniExpr
- X Node
+ X Node
+ NonEscaping bool // The allocation needed for the conversion to interface is known not to escape
}
func NewConvExpr(pos src.XPos, op Op, typ *types.Type, x Node) *ConvExpr {
diff --git a/src/cmd/compile/internal/noder/stencil.go b/src/cmd/compile/internal/noder/stencil.go
index 4c6eaf3fb0..e5f59d0286 100644
--- a/src/cmd/compile/internal/noder/stencil.go
+++ b/src/cmd/compile/internal/noder/stencil.go
@@ -1228,7 +1228,11 @@ func (g *genInst) dictPass(info *instInfo) {
// we do a type assert to the type bound.
mse.X = assertToBound(info, info.dictParam, m.Pos(), mse.X, dst)
} else {
- mse.X = convertUsingDictionary(info, info.dictParam, m.Pos(), mse.X, m, dst)
+ mse.X = convertUsingDictionary(info, info.dictParam, m.Pos(), mse.X, m, dst, true)
+ // Note: we set nonEscaping==true, because we can assume the backing store for the
+ // interface conversion doesn't escape. The method call will immediately go to
+ // a wrapper function which copies all the data out of the interface value.
+ // (It only matters for non-pointer-shaped interface conversions. See issue 50182.)
}
transformDot(mse, false)
}
@@ -1254,7 +1258,7 @@ func (g *genInst) dictPass(info *instInfo) {
// Note: x's argument is still typed as a type parameter.
// m's argument now has an instantiated type.
if mce.X.Type().HasShape() || (mce.X.Type().IsInterface() && m.Type().HasShape()) {
- m = convertUsingDictionary(info, info.dictParam, m.Pos(), m.(*ir.ConvExpr).X, m, m.Type())
+ m = convertUsingDictionary(info, info.dictParam, m.Pos(), m.(*ir.ConvExpr).X, m, m.Type(), false)
}
case ir.ODOTTYPE, ir.ODOTTYPE2:
if !m.Type().HasShape() {
@@ -1347,7 +1351,9 @@ func findDictType(info *instInfo, t *types.Type) int {
// type dst, by returning a new set of nodes that make use of a dictionary entry. in is the
// instantiated node of the CONVIFACE node or XDOT node (for a bound method call) that is causing the
// conversion.
-func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v ir.Node, in ir.Node, dst *types.Type) ir.Node {
+// If nonEscaping is true, the caller guarantees that the backing store needed for the interface data
+// word will not escape.
+func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v ir.Node, in ir.Node, dst *types.Type, nonEscaping bool) ir.Node {
assert(v.Type().HasShape() || v.Type().IsInterface() && in.Type().HasShape())
assert(dst.IsInterface())
@@ -1417,6 +1423,7 @@ func convertUsingDictionary(info *instInfo, dictParam *ir.Name, pos src.XPos, v
// Figure out what the data field of the interface will be.
data := ir.NewConvExpr(pos, ir.OCONVIDATA, nil, v)
typed(types.Types[types.TUNSAFEPTR], data)
+ data.NonEscaping = nonEscaping
// Build an interface from the type and data parts.
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 {
// the type bound.
rcvr = assertToBound(info, dictVar, pos, rcvr, dst)
} else {
- rcvr = convertUsingDictionary(info, dictVar, pos, rcvr, m, dst)
+ rcvr = convertUsingDictionary(info, dictVar, pos, rcvr, m, dst, false)
}
dot := ir.NewSelectorExpr(pos, ir.ODOTINTER, rcvr, m.(*ir.SelectorExpr).Sel)
dot.Selection = typecheck.Lookdot1(dot, dot.Sel, dot.X.Type(), dot.X.Type().AllMethods(), 1)
diff --git a/src/cmd/compile/internal/test/issue50182_test.go b/src/cmd/compile/internal/test/issue50182_test.go
new file mode 100644
index 0000000000..cd277fa285
--- /dev/null
+++ b/src/cmd/compile/internal/test/issue50182_test.go
@@ -0,0 +1,62 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+ "fmt"
+ "sort"
+ "testing"
+)
+
+// Test that calling methods on generic types doesn't cause allocations.
+func genericSorted[T sort.Interface](data T) bool {
+ n := data.Len()
+ for i := n - 1; i > 0; i-- {
+ if data.Less(i, i-1) {
+ return false
+ }
+ }
+ return true
+}
+func TestGenericSorted(t *testing.T) {
+ var data = sort.IntSlice{-10, -5, 0, 1, 2, 3, 5, 7, 11, 100, 100, 100, 1000, 10000}
+ f := func() {
+ genericSorted(data)
+ }
+ if n := testing.AllocsPerRun(10, f); n > 0 {
+ t.Errorf("got %f allocs, want 0", n)
+ }
+}
+
+// Test that escape analysis correctly tracks escaping inside of methods
+// called on generic types.
+type fooer interface {
+ foo()
+}
+type P struct {
+ p *int
+ q int
+}
+
+var esc []*int
+
+func (p P) foo() {
+ esc = append(esc, p.p) // foo escapes the pointer from inside of p
+}
+func f[T fooer](t T) {
+ t.foo()
+}
+func TestGenericEscape(t *testing.T) {
+ for i := 0; i < 4; i++ {
+ var x int = 77 + i
+ var p P = P{p: &x}
+ f(p)
+ }
+ for i, p := range esc {
+ if got, want := *p, 77+i; got != want {
+ panic(fmt.Sprintf("entry %d: got %d, want %d", i, got, want))
+ }
+ }
+}