Skip to content

Instantly share code, notes, and snippets.

@Deleplace
Created November 17, 2022 13:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Deleplace/326d4c4a1db58bf6991fe22a264d289c to your computer and use it in GitHub Desktop.
Save Deleplace/326d4c4a1db58bf6991fe22a264d289c to your computer and use it in GitHub Desktop.
Is there a performance penalty when taking a func argument instead of a value argument, in concurrent maps operations?
package mapfunc
import (
"sync"
"testing"
"github.com/puzpuzpuz/xsync/v2"
)
const (
K = 1_000
)
// Stdlib (not generic, use type assertions)
func BenchmarkSyncMapLoadOrStore(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 3732423
var m sync.Map
for j := 0; j < K; j++ {
prev, _ := m.LoadOrStore(x, j)
if y := prev.(int); y < 0 {
panic(prev)
}
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
}
}
}
// xsync.MapOf.LoadOrStore : takes value argument, internally calls doCompute.
func BenchmarkPuzpuzpuzXsyncMapOfLoadOrStore(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 3732423
m := xsync.NewIntegerMapOf[int, int]()
for j := 0; j < K; j++ {
prev, _ := m.LoadOrStore(x, j)
if prev < 0 {
panic(prev)
}
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
}
}
}
// xsync.MapOf.LoadOrCompute : takes func() V argument, internally calls doCompute.
// inlining works well with anonymous func.
func BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 3732423
m := xsync.NewIntegerMapOf[int, int]()
for j := 0; j < K; j++ {
prev, _ := m.LoadOrCompute(x, func() int {
return j
})
if prev < 0 {
panic(prev)
}
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
}
}
}
// xsync.MapOf.LoadOrCompute : takes func() V argument, internally calls doCompute.
// all optimizations don't seem to work, maybe because compiler is too conservative about the (inferable) value of f.
func BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute2(b *testing.B) {
var f func() int
for i := 0; i < b.N; i++ {
x := 3732423
m := xsync.NewIntegerMapOf[int, int]()
for j := 0; j < K; j++ {
f = func() int {
return j
}
prev, _ := m.LoadOrCompute(x, f)
if prev < 0 {
panic(prev)
}
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
}
}
}
// xsync.MapOf.LoadOrCompute : takes func() V argument, internally calls doCompute.
// inlining seem to work, as the compiler knows the exact value of f.
func BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute3(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 3732423
m := xsync.NewIntegerMapOf[int, int]()
for j := 0; j < K; j++ {
f := func() int {
return j
}
prev, _ := m.LoadOrCompute(x, f)
if prev < 0 {
panic(prev)
}
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
}
}
}
@Deleplace
Copy link
Author

Results on my machine:

% go test -bench=.                                                           
goos: darwin
goarch: arm64
pkg: mapfunc
BenchmarkSyncMapLoadOrStore-10                   	   12090	     97791 ns/op
BenchmarkPuzpuzpuzXsyncMapOfLoadOrStore-10       	   24588	     48806 ns/op
BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute-10     	   24057	     49726 ns/op
BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute2-10    	   18361	     65078 ns/op
BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute3-10    	   24045	     49880 ns/op

Passing a func doesn't incur a perf penalty in BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute and BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute3, but it does in BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute2.

@Deleplace
Copy link
Author

Inlining says it does its job, even when storing to a variable:

% go test -bench=. -gcflags="-m=2" |& grep inlin | grep mapfunc_test | grep "\.func"
./mapfunc_test.go:64:34: can inline BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute.func1 with cost 2 as: func() int { return j }
./mapfunc_test.go:90:8: can inline BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute2.func1 with cost 2 as: func() int { return j }
./mapfunc_test.go:115:9: can inline BenchmarkPuzpuzpuzXsyncMapOfLoadOrCompute3.func1 with cost 2 as: func() int { return j }

@Deleplace
Copy link
Author

However `` seems to be suffering from the func literal escaping to the heap:

% go test -bench=. -gcflags="-m=2" |& grep mapfunc_test | grep "func literal"
./mapfunc_test.go:64:34: func literal does not escape
./mapfunc_test.go:90:8: func literal escapes to heap:
./mapfunc_test.go:90:8:   flow: f = &{storage for func literal}:
./mapfunc_test.go:90:8:     from func literal (spill) at ./mapfunc_test.go:90:8
./mapfunc_test.go:90:8:     from f = func literal (assign) at ./mapfunc_test.go:90:6
./mapfunc_test.go:89:7:   flow: {storage for func literal} = &j:
./mapfunc_test.go:90:8: func literal escapes to heap
./mapfunc_test.go:115:9: func literal does not escape

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment