aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cmd/compile/internal/bloop/bloop.go16
-rw-r--r--src/cmd/compile/internal/typecheck/dcl.go7
-rw-r--r--src/testing/benchmark_test.go61
-rw-r--r--test/escape_bloop.go64
4 files changed, 145 insertions, 3 deletions
diff --git a/src/cmd/compile/internal/bloop/bloop.go b/src/cmd/compile/internal/bloop/bloop.go
index 56fe9a424d..69c889e858 100644
--- a/src/cmd/compile/internal/bloop/bloop.go
+++ b/src/cmd/compile/internal/bloop/bloop.go
@@ -203,6 +203,10 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) {
ir.NewAssignListStmt(n.Pos(), ir.OAS2, lhs,
[]ir.Node{n})).(*ir.AssignListStmt)
assign.Def = true
+ for _, tmp := range lhs {
+ // Place temp declarations in the loop body to help escape analysis.
+ assign.PtrInit().Append(typecheck.Stmt(ir.NewDecl(assign.Pos(), ir.ODCL, tmp.(*ir.Name))))
+ }
curNode = assign
plural := ""
if len(results) > 1 {
@@ -232,7 +236,11 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) {
} else {
// We need a temporary to save this arg.
tmp := typecheck.TempAt(elem.Pos(), curFn, elem.Type())
- argTmps = append(argTmps, typecheck.AssignExpr(ir.NewAssignStmt(elem.Pos(), tmp, elem)))
+ assign := ir.NewAssignStmt(elem.Pos(), tmp, elem)
+ assign.Def = true
+ // Place temp declarations in the loop body to help escape analysis.
+ assign.PtrInit().Append(typecheck.Stmt(ir.NewDecl(assign.Pos(), ir.ODCL, tmp)))
+ argTmps = append(argTmps, typecheck.AssignExpr(assign))
names = append(names, tmp)
s.List[i] = tmp
if base.Flag.LowerM > 1 {
@@ -245,7 +253,11 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) {
// expressions, we need to assign them to temps and change the original arg to reference
// them.
tmp := typecheck.TempAt(n.Pos(), curFn, a.Type())
- argTmps = append(argTmps, typecheck.AssignExpr(ir.NewAssignStmt(n.Pos(), tmp, a)))
+ assign := ir.NewAssignStmt(n.Pos(), tmp, a)
+ assign.Def = true
+ // Place temp declarations in the loop body to help escape analysis.
+ assign.PtrInit().Append(typecheck.Stmt(ir.NewDecl(assign.Pos(), ir.ODCL, tmp)))
+ argTmps = append(argTmps, typecheck.AssignExpr(assign))
names = append(names, tmp)
n.Args[i] = tmp
if base.Flag.LowerM > 1 {
diff --git a/src/cmd/compile/internal/typecheck/dcl.go b/src/cmd/compile/internal/typecheck/dcl.go
index 4a847e8558..ce3b9b4366 100644
--- a/src/cmd/compile/internal/typecheck/dcl.go
+++ b/src/cmd/compile/internal/typecheck/dcl.go
@@ -42,7 +42,12 @@ func CheckFuncStack() {
}
}
-// make a new Node off the books.
+// TempAt makes a new Node off the books.
+//
+// N.B., the new Node is a function-local variable defaulting to function scope.
+// It helps in some cases if an ODCL is also created and placed in a narrower scope,
+// such as if the variable can be used in a loop body and potentially escape.
+// TODO: Consider some mechanism to more conveniently create a block scoped temporary.
func TempAt(pos src.XPos, curfn *ir.Func, typ *types.Type) *ir.Name {
if curfn == nil {
base.FatalfAt(pos, "no curfn for TempAt")
diff --git a/src/testing/benchmark_test.go b/src/testing/benchmark_test.go
index e2dd24c839..a21daf7d12 100644
--- a/src/testing/benchmark_test.go
+++ b/src/testing/benchmark_test.go
@@ -9,6 +9,10 @@ import (
"cmp"
"context"
"errors"
+ "internal/asan"
+ "internal/msan"
+ "internal/race"
+ "internal/testenv"
"runtime"
"slices"
"strings"
@@ -157,6 +161,63 @@ func TestBenchmarkContext(t *testing.T) {
})
}
+// Some auxiliary functions for measuring allocations in a b.Loop benchmark below,
+// where in this case mid-stack inlining allows stack allocation of a slice.
+// This is based on the example in go.dev/issue/73137.
+
+func newX() []byte {
+ out := make([]byte, 8)
+ return use1(out)
+}
+
+//go:noinline
+func use1(out []byte) []byte {
+ return out
+}
+
+// An auxiliary function for measuring allocations with a simple function argument
+// in the b.Loop body.
+
+//go:noinline
+func use2(x any) {}
+
+func TestBenchmarkBLoopAllocs(t *testing.T) {
+ testenv.SkipIfOptimizationOff(t)
+ if race.Enabled || asan.Enabled || msan.Enabled {
+ t.Skip("skipping in case sanitizers alter allocation behavior")
+ }
+
+ t.Run("call-result", func(t *testing.T) {
+ bRet := testing.Benchmark(func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ newX()
+ }
+ })
+ if bRet.N == 0 {
+ t.Fatalf("benchmark reported 0 iterations")
+ }
+ if bRet.AllocsPerOp() != 0 {
+ t.Errorf("want 0 allocs, got %d", bRet.AllocsPerOp())
+ }
+ })
+
+ t.Run("call-arg", func(t *testing.T) {
+ bRet := testing.Benchmark(func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ use2(make([]byte, 1000))
+ }
+ })
+ if bRet.N == 0 {
+ t.Fatalf("benchmark reported 0 iterations")
+ }
+ if bRet.AllocsPerOp() != 0 {
+ t.Errorf("want 0 allocs, got %d", bRet.AllocsPerOp())
+ }
+ })
+}
+
func ExampleB_RunParallel() {
// Parallel benchmark for text/template.Template.Execute on a single object.
testing.Benchmark(func(b *testing.B) {
diff --git a/test/escape_bloop.go b/test/escape_bloop.go
new file mode 100644
index 0000000000..b0d79ecf1f
--- /dev/null
+++ b/test/escape_bloop.go
@@ -0,0 +1,64 @@
+// errorcheck -0 -m
+
+// Copyright 2026 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.
+
+// Test b.Loop escape analysis behavior.
+
+package bloop
+
+import (
+ "testing"
+)
+
+// An example where mid-stack inlining allows stack allocation of a slice.
+// This is from the example in go.dev/issue/73137.
+
+func NewX(x int) []byte { // ERROR "can inline NewX"
+ out := make([]byte, 8) // ERROR "make\(\[\]byte, 8\) escapes to heap"
+ return use1(out)
+}
+
+//go:noinline
+func use1(out []byte) []byte { // ERROR "leaking param: out to result ~r0 level=0"
+ return out
+}
+
+//go:noinline
+func BenchmarkBloop(b *testing.B) { // ERROR "leaking param: b"
+ for b.Loop() { // ERROR "inlining call to testing.\(\*B\).Loop"
+ NewX(42) // ERROR "make\(\[\]byte, 8\) does not escape" "inlining call to NewX"
+ }
+}
+
+// A traditional b.N benchmark using a sink variable for comparison,
+// also from the example in go.dev/issue/73137.
+
+var sink byte
+
+//go:noinline
+func BenchmarkBN(b *testing.B) { // ERROR "b does not escape"
+ for i := 0; i < b.N; i++ {
+ out := NewX(42) // ERROR "make\(\[\]byte, 8\) does not escape" "inlining call to NewX"
+ sink = out[0]
+ }
+}
+
+// An example showing behavior of a simple function argument in the b.Loop body.
+
+//go:noinline
+func use2(x any) {} // ERROR "x does not escape"
+
+//go:noinline
+func BenchmarkBLoopFunctionArg(b *testing.B) { // ERROR "leaking param: b"
+ for b.Loop() { // ERROR "inlining call to testing.\(\*B\).Loop"
+ use2(42) // ERROR "42 does not escape"
+ }
+}
+
+// A similar call outside of b.Loop for comparison.
+
+func simpleFunctionArg() { // ERROR "can inline simpleFunctionArg"
+ use2(42) // ERROR "42 does not escape"
+}