assign := typecheck.AssignExpr(ir.NewAssignListStmt(call.Pos(), ir.OAS2, lhs, ir.Nodes{call})).(*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))))
+ }
return keepAliveAt(ns, assign)
}
var names ir.Nodes
preserveTmp := func(pos src.XPos, n ir.Node) ir.Node {
tmp := typecheck.TempAt(pos, curFn, n.Type())
- argTmps = append(argTmps, typecheck.AssignExpr(ir.NewAssignStmt(pos, tmp, n)))
+ assign := ir.NewAssignStmt(pos, tmp, n)
+ 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)
if base.Flag.LowerM > 1 {
base.WarnfAt(call.Pos(), "function arg will be kept alive")
}
}
-// 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")
"cmp"
"context"
"errors"
+ "internal/asan"
+ "internal/msan"
+ "internal/race"
+ "internal/testenv"
"runtime"
"slices"
"strings"
})
}
+// 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) {
--- /dev/null
+// 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"
+}