]> Cypherpunks repositories - gostls13.git/commitdiff
sync: reduce OnceFunc (and variants) heap allocations
authorBrian Byrne <bdbyrne@gmail.com>
Sun, 28 Jul 2024 14:42:54 +0000 (07:42 -0700)
committerGopher Robot <gobot@golang.org>
Tue, 25 Feb 2025 19:52:33 +0000 (11:52 -0800)
The lifetime of the variables are identical; capture
them in a single struct to avoid individual allocations.
The inner closure can also avoid allocation by using the
capture of the outer closure.

Escape analysis for OnceValues:

  /go/src/sync/oncefunc.go:74:29: moved to heap: sync.f
  /go/src/sync/oncefunc.go:76:3: moved to heap: sync.once
  /go/src/sync/oncefunc.go:77:3: moved to heap: sync.valid
  /go/src/sync/oncefunc.go:78:3: moved to heap: sync.p
  /go/src/sync/oncefunc.go:79:3: moved to heap: sync.r1
  /go/src/sync/oncefunc.go:80:3: moved to heap: sync.r2
  /go/src/sync/oncefunc.go:82:7: func literal escapes to heap
  /go/src/sync/oncefunc.go:83:9: func literal does not escape
  /go/src/sync/oncefunc.go:93:9: func literal escapes to heap

After provided changes:

  /go/src/sync/oncefunc.go:86:2: moved to heap: sync.d
  /go/src/sync/oncefunc.go:96:9: func literal escapes to heap
  /go/src/sync/oncefunc.go:99:13: func literal does not escape
  /go/src/sync/oncefunc.go:100:10: func literal does not escape

Change-Id: Ib06e650fd427b57e0bdbdf1fe759fe436104ff79
Reviewed-on: https://go-review.googlesource.com/c/go/+/601596
Auto-Submit: Austin Clements <austin@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Austin Clements <austin@google.com>
src/cmd/compile/internal/test/inl_test.go
src/sync/oncefunc.go
src/sync/oncefunc_test.go

index f1f6c34bfc8f9a78e15197896db999bffbbc40ae..6119c2b8365ca5ba981742d88d458cf2ad8e1cfe 100644 (file)
@@ -190,7 +190,7 @@ func TestIntendedInlining(t *testing.T) {
                        // Both OnceFunc and its returned closure need to be inlinable so
                        // that the returned closure can be inlined into the caller of OnceFunc.
                        "OnceFunc",
-                       "OnceFunc.func2", // The returned closure.
+                       "OnceFunc.func1", // The returned closure.
                        // TODO(austin): It would be good to check OnceValue and OnceValues,
                        // too, but currently they aren't reported because they have type
                        // parameters and aren't instantiated in sync.
index db286283d13303430845786ad73a4f0b0bd64061..2c49efeef8175fa1e41859f4614e30ba62a41c34 100644 (file)
@@ -9,29 +9,32 @@ package sync
 //
 // If f panics, the returned function will panic with the same value on every call.
 func OnceFunc(f func()) func() {
-       var (
+       // Use a struct so that there's a single heap allocation.
+       d := struct {
+               f     func()
                once  Once
                valid bool
                p     any
-       )
-       // Construct the inner closure just once to reduce costs on the fast path.
-       g := func() {
-               defer func() {
-                       p = recover()
-                       if !valid {
-                               // Re-panic immediately so on the first call the user gets a
-                               // complete stack trace into f.
-                               panic(p)
-                       }
-               }()
-               f()
-               f = nil      // Do not keep f alive after invoking it.
-               valid = true // Set only if f does not panic.
+       }{
+               f: f,
        }
        return func() {
-               once.Do(g)
-               if !valid {
-                       panic(p)
+               d.once.Do(func() {
+                       defer func() {
+                               d.p = recover()
+                               if !d.valid {
+                                       // Re-panic immediately so on the first
+                                       // call the user gets a complete stack
+                                       // trace into f.
+                                       panic(d.p)
+                               }
+                       }()
+                       d.f()
+                       d.f = nil      // Do not keep f alive after invoking it.
+                       d.valid = true // Set only if f does not panic.
+               })
+               if !d.valid {
+                       panic(d.p)
                }
        }
 }
@@ -41,29 +44,32 @@ func OnceFunc(f func()) func() {
 //
 // If f panics, the returned function will panic with the same value on every call.
 func OnceValue[T any](f func() T) func() T {
-       var (
+       // Use a struct so that there's a single heap allocation.
+       d := struct {
+               f      func() T
                once   Once
                valid  bool
                p      any
                result T
-       )
-       g := func() {
-               defer func() {
-                       p = recover()
-                       if !valid {
-                               panic(p)
-                       }
-               }()
-               result = f()
-               f = nil
-               valid = true
+       }{
+               f: f,
        }
        return func() T {
-               once.Do(g)
-               if !valid {
-                       panic(p)
+               d.once.Do(func() {
+                       defer func() {
+                               d.p = recover()
+                               if !d.valid {
+                                       panic(d.p)
+                               }
+                       }()
+                       d.result = d.f()
+                       d.f = nil
+                       d.valid = true
+               })
+               if !d.valid {
+                       panic(d.p)
                }
-               return result
+               return d.result
        }
 }
 
@@ -72,29 +78,32 @@ func OnceValue[T any](f func() T) func() T {
 //
 // If f panics, the returned function will panic with the same value on every call.
 func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
-       var (
+       // Use a struct so that there's a single heap allocation.
+       d := struct {
+               f     func() (T1, T2)
                once  Once
                valid bool
                p     any
                r1    T1
                r2    T2
-       )
-       g := func() {
-               defer func() {
-                       p = recover()
-                       if !valid {
-                               panic(p)
-                       }
-               }()
-               r1, r2 = f()
-               f = nil
-               valid = true
+       }{
+               f: f,
        }
        return func() (T1, T2) {
-               once.Do(g)
-               if !valid {
-                       panic(p)
+               d.once.Do(func() {
+                       defer func() {
+                               d.p = recover()
+                               if !d.valid {
+                                       panic(d.p)
+                               }
+                       }()
+                       d.r1, d.r2 = d.f()
+                       d.f = nil
+                       d.valid = true
+               })
+               if !d.valid {
+                       panic(d.p)
                }
-               return r1, r2
+               return d.r1, d.r2
        }
 }
index daf094571f054b2a03b5a865da5a39fdb9d2a89f..743a816b651191ddc105803eb0ce465d6f10c82c 100644 (file)
@@ -19,22 +19,30 @@ import (
 
 func TestOnceFunc(t *testing.T) {
        calls := 0
-       f := sync.OnceFunc(func() { calls++ })
+       of := func() { calls++ }
+       f := sync.OnceFunc(of)
        allocs := testing.AllocsPerRun(10, f)
        if calls != 1 {
                t.Errorf("want calls==1, got %d", calls)
        }
        if allocs != 0 {
-               t.Errorf("want 0 allocations per call, got %v", allocs)
+               t.Errorf("want 0 allocations per call to f, got %v", allocs)
+       }
+       allocs = testing.AllocsPerRun(10, func() {
+               f = sync.OnceFunc(of)
+       })
+       if allocs > 2 {
+               t.Errorf("want at most 2 allocations per call to OnceFunc, got %v", allocs)
        }
 }
 
 func TestOnceValue(t *testing.T) {
        calls := 0
-       f := sync.OnceValue(func() int {
+       of := func() int {
                calls++
                return calls
-       })
+       }
+       f := sync.OnceValue(of)
        allocs := testing.AllocsPerRun(10, func() { f() })
        value := f()
        if calls != 1 {
@@ -44,16 +52,23 @@ func TestOnceValue(t *testing.T) {
                t.Errorf("want value==1, got %d", value)
        }
        if allocs != 0 {
-               t.Errorf("want 0 allocations per call, got %v", allocs)
+               t.Errorf("want 0 allocations per call to f, got %v", allocs)
+       }
+       allocs = testing.AllocsPerRun(10, func() {
+               f = sync.OnceValue(of)
+       })
+       if allocs > 2 {
+               t.Errorf("want at most 2 allocations per call to OnceValue, got %v", allocs)
        }
 }
 
 func TestOnceValues(t *testing.T) {
        calls := 0
-       f := sync.OnceValues(func() (int, int) {
+       of := func() (int, int) {
                calls++
                return calls, calls + 1
-       })
+       }
+       f := sync.OnceValues(of)
        allocs := testing.AllocsPerRun(10, func() { f() })
        v1, v2 := f()
        if calls != 1 {
@@ -63,7 +78,13 @@ func TestOnceValues(t *testing.T) {
                t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2)
        }
        if allocs != 0 {
-               t.Errorf("want 0 allocations per call, got %v", allocs)
+               t.Errorf("want 0 allocations per call to f, got %v", allocs)
+       }
+       allocs = testing.AllocsPerRun(10, func() {
+               f = sync.OnceValues(of)
+       })
+       if allocs > 2 {
+               t.Errorf("want at most 2 allocations per call to OnceValues, got %v", allocs)
        }
 }
 
@@ -234,6 +255,8 @@ var (
        onceFunc = sync.OnceFunc(func() {})
 
        onceFuncOnce sync.Once
+
+       onceFuncFunc func()
 )
 
 func doOnceFunc() {
@@ -267,6 +290,12 @@ func BenchmarkOnceFunc(b *testing.B) {
                        f()
                }
        })
+       b.Run("v=Make", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       onceFuncFunc = sync.OnceFunc(func() {})
+               }
+       })
 }
 
 var (
@@ -274,6 +303,8 @@ var (
 
        onceValueOnce  sync.Once
        onceValueValue int
+
+       onceValueFunc func() int
 )
 
 func doOnceValue() int {
@@ -310,4 +341,80 @@ func BenchmarkOnceValue(b *testing.B) {
                        }
                }
        })
+       b.Run("v=Make", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       onceValueFunc = sync.OnceValue(func() int { return 42 })
+               }
+       })
+}
+
+const (
+       onceValuesWant1 = 42
+       onceValuesWant2 = true
+)
+
+var (
+       onceValues = sync.OnceValues(func() (int, bool) {
+               return onceValuesWant1, onceValuesWant2
+       })
+
+       onceValuesOnce   sync.Once
+       onceValuesValue1 int
+       onceValuesValue2 bool
+
+       onceValuesFunc func() (int, bool)
+)
+
+func doOnceValues() (int, bool) {
+       onceValuesOnce.Do(func() {
+               onceValuesValue1 = onceValuesWant1
+               onceValuesValue2 = onceValuesWant2
+       })
+       return onceValuesValue1, onceValuesValue2
+}
+
+func BenchmarkOnceValues(b *testing.B) {
+       // See BenchmarkOnceFunc
+       b.Run("v=Once", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       if got1, got2 := doOnceValues(); got1 != onceValuesWant1 {
+                               b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
+                       } else if got2 != onceValuesWant2 {
+                               b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
+                       }
+               }
+       })
+       b.Run("v=Global", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       if got1, got2 := onceValues(); got1 != onceValuesWant1 {
+                               b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
+                       } else if got2 != onceValuesWant2 {
+                               b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
+                       }
+               }
+       })
+       b.Run("v=Local", func(b *testing.B) {
+               b.ReportAllocs()
+               onceValues := sync.OnceValues(func() (int, bool) {
+                       return onceValuesWant1, onceValuesWant2
+               })
+               for i := 0; i < b.N; i++ {
+                       if got1, got2 := onceValues(); got1 != onceValuesWant1 {
+                               b.Fatalf("value 1: got %d, want %d", got1, onceValuesWant1)
+                       } else if got2 != onceValuesWant2 {
+                               b.Fatalf("value 2: got %v, want %v", got2, onceValuesWant2)
+                       }
+               }
+       })
+       b.Run("v=Make", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       onceValuesFunc = sync.OnceValues(func() (int, bool) {
+                               return onceValuesWant1, onceValuesWant2
+                       })
+               }
+       })
 }