diff options
| -rw-r--r-- | src/cmd/compile/internal/bloop/bloop.go | 16 | ||||
| -rw-r--r-- | src/cmd/compile/internal/typecheck/dcl.go | 7 | ||||
| -rw-r--r-- | src/testing/benchmark_test.go | 61 | ||||
| -rw-r--r-- | test/escape_bloop.go | 64 |
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" +} |
