]> Cypherpunks repositories - gostls13.git/commitdiff
runtime: more thorough map benchmarks
authorMichael Pratt <mpratt@google.com>
Fri, 28 Jun 2024 17:15:48 +0000 (17:15 +0000)
committerGopher Robot <gobot@golang.org>
Fri, 18 Oct 2024 23:13:43 +0000 (23:13 +0000)
Based on the benchmarks in github.com/cockroachlabs/swiss.

For #54766.

Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest-swissmap
Change-Id: I9ad925d3272c671e21ec04eb2da5ebd8f0fc6a28
Reviewed-on: https://go-review.googlesource.com/c/go/+/596295
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>

src/runtime/map_benchmark_test.go
src/runtime/map_test.go

index 43d1accbb97cd67575a01280515bfeadd2465d2f..663abf6202be9b961bbc384fde72a12e26bfa128 100644 (file)
@@ -5,11 +5,15 @@
 package runtime_test
 
 import (
+       "encoding/binary"
        "fmt"
        "math/rand"
+       "runtime"
+       "slices"
        "strconv"
        "strings"
        "testing"
+       "unsafe"
 )
 
 const size = 10
@@ -206,17 +210,6 @@ func benchmarkMapStringKeysEight(b *testing.B, keySize int) {
        }
 }
 
-func BenchmarkIntMap(b *testing.B) {
-       m := make(map[int]bool)
-       for i := 0; i < 8; i++ {
-               m[i] = true
-       }
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               _, _ = m[7]
-       }
-}
-
 func BenchmarkMapFirst(b *testing.B) {
        for n := 1; n <= 16; n++ {
                b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
@@ -333,27 +326,6 @@ func BenchmarkNewSmallMap(b *testing.B) {
        }
 }
 
-func BenchmarkMapIter(b *testing.B) {
-       m := make(map[int]bool)
-       for i := 0; i < 8; i++ {
-               m[i] = true
-       }
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               for range m {
-               }
-       }
-}
-
-func BenchmarkMapIterEmpty(b *testing.B) {
-       m := make(map[int]bool)
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               for range m {
-               }
-       }
-}
-
 func BenchmarkSameLengthMap(b *testing.B) {
        // long strings, same length, differ in first few
        // and last few bytes.
@@ -368,28 +340,6 @@ func BenchmarkSameLengthMap(b *testing.B) {
        }
 }
 
-type BigKey [3]int64
-
-func BenchmarkBigKeyMap(b *testing.B) {
-       m := make(map[BigKey]bool)
-       k := BigKey{3, 4, 5}
-       m[k] = true
-       for i := 0; i < b.N; i++ {
-               _ = m[k]
-       }
-}
-
-type BigVal [3]int64
-
-func BenchmarkBigValMap(b *testing.B) {
-       m := make(map[BigKey]BigVal)
-       k := BigKey{3, 4, 5}
-       m[k] = BigVal{6, 7, 8}
-       for i := 0; i < b.N; i++ {
-               _ = m[k]
-       }
-}
-
 func BenchmarkSmallKeyMap(b *testing.B) {
        m := make(map[int16]bool)
        m[5] = true
@@ -538,3 +488,586 @@ func BenchmarkNewEmptyMapHintGreaterThan8(b *testing.B) {
                _ = make(map[int]int, hintGreaterThan8)
        }
 }
+
+func benchSizes(f func(b *testing.B, n int)) func(*testing.B) {
+       var cases = []int{
+               0,
+               6,
+               12,
+               18,
+               24,
+               30,
+               64,
+               128,
+               256,
+               512,
+               1024,
+               2048,
+               4096,
+               8192,
+               1 << 16,
+               1 << 18,
+               1 << 20,
+               1 << 22,
+       }
+
+       return func(b *testing.B) {
+               for _, n := range cases {
+                       b.Run("len="+strconv.Itoa(n), func(b *testing.B) {
+                               f(b, n)
+                       })
+               }
+       }
+}
+
+// A 16 byte type.
+type smallType [16]byte
+
+// A 512 byte type.
+type mediumType [1 << 9]byte
+
+// A 4KiB type.
+type bigType [1 << 12]byte
+
+type mapBenchmarkKeyType interface {
+       int32 | int64 | string | smallType | mediumType | bigType | *int32
+}
+
+type mapBenchmarkElemType interface {
+       mapBenchmarkKeyType | []int32
+}
+
+func genIntValues[T int | int32 | int64](start, end int) []T {
+       vals := make([]T, 0, end-start)
+       for i := start; i < end; i++ {
+               vals = append(vals, T(i))
+       }
+       return vals
+}
+
+func genStringValues(start, end int) []string {
+       vals := make([]string, 0, end-start)
+       for i := start; i < end; i++ {
+               vals = append(vals, strconv.Itoa(i))
+       }
+       return vals
+}
+
+func genSmallValues(start, end int) []smallType {
+       vals := make([]smallType, 0, end-start)
+       for i := start; i < end; i++ {
+               var v smallType
+               binary.NativeEndian.PutUint64(v[:], uint64(i))
+               vals = append(vals, v)
+       }
+       return vals
+}
+
+func genMediumValues(start, end int) []mediumType {
+       vals := make([]mediumType, 0, end-start)
+       for i := start; i < end; i++ {
+               var v mediumType
+               binary.NativeEndian.PutUint64(v[:], uint64(i))
+               vals = append(vals, v)
+       }
+       return vals
+}
+
+func genBigValues(start, end int) []bigType {
+       vals := make([]bigType, 0, end-start)
+       for i := start; i < end; i++ {
+               var v bigType
+               binary.NativeEndian.PutUint64(v[:], uint64(i))
+               vals = append(vals, v)
+       }
+       return vals
+}
+
+func genPtrValues[T any](start, end int) []*T {
+       // Start and end don't mean much. Each pointer by definition has a
+       // unique identity.
+       vals := make([]*T, 0, end-start)
+       for i := start; i < end; i++ {
+               v := new(T)
+               vals = append(vals, v)
+       }
+       return vals
+}
+
+func genIntSliceValues[T int | int32 | int64](start, end int) [][]T {
+       vals := make([][]T, 0, end-start)
+       for i := start; i < end; i++ {
+               vals = append(vals, []T{T(i)})
+       }
+       return vals
+}
+
+func genValues[T mapBenchmarkElemType](start, end int) []T {
+       var t T
+       switch any(t).(type) {
+       case int32:
+               return any(genIntValues[int32](start, end)).([]T)
+       case int64:
+               return any(genIntValues[int64](start, end)).([]T)
+       case string:
+               return any(genStringValues(start, end)).([]T)
+       case smallType:
+               return any(genSmallValues(start, end)).([]T)
+       case mediumType:
+               return any(genMediumValues(start, end)).([]T)
+       case bigType:
+               return any(genBigValues(start, end)).([]T)
+       case *int32:
+               return any(genPtrValues[int32](start, end)).([]T)
+       case []int32:
+               return any(genIntSliceValues[int32](start, end)).([]T)
+       default:
+               panic("unreachable")
+       }
+}
+
+// Avoid inlining to force a heap allocation.
+//
+//go:noinline
+func newSink[T mapBenchmarkElemType]() *T {
+       return new(T)
+}
+
+// Return a new maps filled with keys and elems. Both slices must be the same length.
+func fillMap[K mapBenchmarkKeyType, E mapBenchmarkElemType](keys []K, elems []E) map[K]E {
+       m := make(map[K]E, len(keys))
+       for i := range keys {
+               m[keys[i]] = elems[i]
+       }
+       return m
+}
+
+func iterCount(b *testing.B, n int) int {
+       // Divide b.N by n so that the ns/op reports time per element,
+       // not time per full map iteration. This makes benchmarks of
+       // different map sizes more comparable.
+       //
+       // If size is zero we still need to do iterations.
+       if n == 0 {
+               return b.N
+       }
+       return b.N / n
+}
+
+func checkAllocSize[K, E any](b *testing.B, n int) {
+       var k K
+       size := uint64(n) * uint64(unsafe.Sizeof(k))
+       var e E
+       size += uint64(n) * uint64(unsafe.Sizeof(e))
+
+       if size >= 1<<30 {
+               b.Skipf("Total key+elem size %d exceeds 1GiB", size)
+       }
+}
+
+func benchmarkMapIter[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       iterations := iterCount(b, n)
+       sinkK := newSink[K]()
+       sinkE := newSink[E]()
+       b.ResetTimer()
+
+       for i := 0; i < iterations; i++ {
+               for k, e := range m {
+                       *sinkK = k
+                       *sinkE = e
+               }
+       }
+}
+
+func BenchmarkMapIter(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapIter[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapIter[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapIter[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapIter[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapIter[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapIter[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapIter[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapIter[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapIter[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapIter[int32, *int32]))
+}
+
+func benchmarkMapAccessHit[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't access empty map")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       sink := newSink[E]()
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               *sink = m[k[i%n]]
+       }
+}
+
+func BenchmarkMapAccessHit(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAccessHit[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAccessHit[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAccessHit[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAccessHit[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAccessHit[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAccessHit[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAccessHit[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAccessHit[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAccessHit[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAccessHit[int32, *int32]))
+}
+
+var sinkOK bool
+
+func benchmarkMapAccessMiss[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       if n == 0 { // Create a lookup values for empty maps.
+               n = 1
+       }
+       w := genValues[K](n, 2*n)
+       b.ResetTimer()
+
+       var ok bool
+       for i := 0; i < b.N; i++ {
+               _, ok = m[w[i%n]]
+       }
+
+       sinkOK = ok
+}
+
+func BenchmarkMapAccessMiss(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAccessMiss[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAccessMiss[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAccessMiss[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAccessMiss[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAccessMiss[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAccessMiss[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAccessMiss[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAccessMiss[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAccessMiss[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAccessMiss[int32, *int32]))
+}
+
+// Assign to a key that already exists.
+func benchmarkMapAssignExists[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't assign to existing keys in empty map")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               m[k[i%n]] = e[i%n]
+       }
+}
+
+func BenchmarkMapAssignExists(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignExists[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignExists[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignExists[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignExists[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignExists[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignExists[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAssignExists[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAssignExists[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAssignExists[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAssignExists[int32, *int32]))
+}
+
+// Fill a map of size n with no hint. Time is per-key. A new map is created
+// every n assignments.
+//
+// TODO(prattmic): Results don't make much sense if b.N < n.
+// TODO(prattmic): Measure distribution of assign time to reveal the grow
+// latency.
+func benchmarkMapAssignFillNoHint[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't create empty map via assignment")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       b.ResetTimer()
+
+       var m map[K]E
+       for i := 0; i < b.N; i++ {
+               if i%n == 0 {
+                       m = make(map[K]E)
+               }
+               m[k[i%n]] = e[i%n]
+       }
+}
+
+func BenchmarkMapAssignFillNoHint(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignFillNoHint[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignFillNoHint[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignFillNoHint[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignFillNoHint[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignFillNoHint[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignFillNoHint[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAssignFillNoHint[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAssignFillNoHint[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAssignFillNoHint[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAssignFillNoHint[int32, *int32]))
+}
+
+// Identical to benchmarkMapAssignFillNoHint, but additionally measures the
+// latency of each mapassign to report tail latency due to map grow.
+func benchmarkMapAssignGrowLatency[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't create empty map via assignment")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+
+       // Store the run time of each mapassign. Keeping the full data rather
+       // than a histogram provides higher precision. b.N tends to be <10M, so
+       // the memory requirement isn't too bad.
+       sample := make([]int64, b.N)
+
+       b.ResetTimer()
+
+       var m map[K]E
+       for i := 0; i < b.N; i++ {
+               if i%n == 0 {
+                       m = make(map[K]E)
+               }
+               start := runtime.Nanotime()
+               m[k[i%n]] = e[i%n]
+               end := runtime.Nanotime()
+               sample[i] = end - start
+       }
+
+       b.StopTimer()
+
+       slices.Sort(sample)
+       // TODO(prattmic): Grow is so rare that even p99.99 often doesn't
+       // display a grow case. Switch to a more direct measure of grow cases
+       // only?
+       b.ReportMetric(float64(sample[int(float64(len(sample))*0.5)]), "p50-ns/op")
+       b.ReportMetric(float64(sample[int(float64(len(sample))*0.99)]), "p99-ns/op")
+       b.ReportMetric(float64(sample[int(float64(len(sample))*0.999)]), "p99.9-ns/op")
+       b.ReportMetric(float64(sample[int(float64(len(sample))*0.9999)]), "p99.99-ns/op")
+       b.ReportMetric(float64(sample[len(sample)-1]), "p100-ns/op")
+}
+
+func BenchmarkMapAssignGrowLatency(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignGrowLatency[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignGrowLatency[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignGrowLatency[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignGrowLatency[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignGrowLatency[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignGrowLatency[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAssignGrowLatency[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAssignGrowLatency[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAssignGrowLatency[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAssignGrowLatency[int32, *int32]))
+}
+
+// Fill a map of size n with size hint. Time is per-key. A new map is created
+// every n assignments.
+//
+// TODO(prattmic): Results don't make much sense if b.N < n.
+func benchmarkMapAssignFillHint[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't create empty map via assignment")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       b.ResetTimer()
+
+       var m map[K]E
+       for i := 0; i < b.N; i++ {
+               if i%n == 0 {
+                       m = make(map[K]E, n)
+               }
+               m[k[i%n]] = e[i%n]
+       }
+}
+
+func BenchmarkMapAssignFillHint(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignFillHint[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignFillHint[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignFillHint[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignFillHint[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignFillHint[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignFillHint[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAssignFillHint[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAssignFillHint[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAssignFillHint[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAssignFillHint[int32, *int32]))
+}
+
+// Fill a map of size n, reusing the same map. Time is per-key. The map is
+// cleared every n assignments.
+//
+// TODO(prattmic): Results don't make much sense if b.N < n.
+func benchmarkMapAssignFillClear[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't create empty map via assignment")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               if i%n == 0 {
+                       clear(m)
+               }
+               m[k[i%n]] = e[i%n]
+       }
+}
+
+func BenchmarkMapAssignFillClear(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignFillClear[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignFillClear[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignFillClear[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignFillClear[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignFillClear[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignFillClear[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapAssignFillClear[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapAssignFillClear[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapAssignFillClear[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapAssignFillClear[int32, *int32]))
+}
+
+// Modify values using +=.
+func benchmarkMapAssignAddition[K mapBenchmarkKeyType, E int32 | int64 | string](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't modify empty map via assignment")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               m[k[i%n]] += e[i%n]
+       }
+}
+
+func BenchmarkMapAssignAddition(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapAssignAddition[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapAssignAddition[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapAssignAddition[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapAssignAddition[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapAssignAddition[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapAssignAddition[bigType, int32]))
+}
+
+// Modify values append.
+func benchmarkMapAssignAppend[K mapBenchmarkKeyType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't modify empty map via append")
+       }
+       checkAllocSize[K, []int32](b, n)
+       k := genValues[K](0, n)
+       e := genValues[[]int32](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               m[k[i%n]] = append(m[k[i%n]], e[i%n][0])
+       }
+}
+
+func BenchmarkMapAssignAppend(b *testing.B) {
+       b.Run("Key=int32/Elem=[]int32", benchSizes(benchmarkMapAssignAppend[int32]))
+       b.Run("Key=int64/Elem=[]int32", benchSizes(benchmarkMapAssignAppend[int64]))
+       b.Run("Key=string/Elem=[]int32", benchSizes(benchmarkMapAssignAppend[string]))
+}
+
+func benchmarkMapDelete[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't delete from empty map")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               if len(m) == 0 {
+                       b.StopTimer()
+                       for j := range k {
+                               m[k[j]] = e[j]
+                       }
+                       b.StartTimer()
+               }
+               delete(m, k[i%n])
+       }
+}
+
+func BenchmarkMapDelete(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapDelete[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapDelete[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapDelete[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapDelete[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapDelete[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapDelete[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapDelete[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapDelete[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapDelete[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapDelete[int32, *int32]))
+}
+
+// Use iterator to pop an element. We want this to be fast, see
+// https://go.dev/issue/8412.
+func benchmarkMapPop[K mapBenchmarkKeyType, E mapBenchmarkElemType](b *testing.B, n int) {
+       if n == 0 {
+               b.Skip("can't delete from empty map")
+       }
+       checkAllocSize[K, E](b, n)
+       k := genValues[K](0, n)
+       e := genValues[E](0, n)
+       m := fillMap(k, e)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               if len(m) == 0 {
+                       // We'd like to StopTimer while refilling the map, but
+                       // it is way too expensive and thus makes the benchmark
+                       // take a long time. See https://go.dev/issue/20875.
+                       for j := range k {
+                               m[k[j]] = e[j]
+                       }
+               }
+               for key := range m {
+                       delete(m, key)
+                       break
+               }
+       }
+}
+
+func BenchmarkMapPop(b *testing.B) {
+       b.Run("Key=int32/Elem=int32", benchSizes(benchmarkMapPop[int32, int32]))
+       b.Run("Key=int64/Elem=int64", benchSizes(benchmarkMapPop[int64, int64]))
+       b.Run("Key=string/Elem=string", benchSizes(benchmarkMapPop[string, string]))
+       b.Run("Key=smallType/Elem=int32", benchSizes(benchmarkMapPop[smallType, int32]))
+       b.Run("Key=mediumType/Elem=int32", benchSizes(benchmarkMapPop[mediumType, int32]))
+       b.Run("Key=bigType/Elem=int32", benchSizes(benchmarkMapPop[bigType, int32]))
+       b.Run("Key=bigType/Elem=bigType", benchSizes(benchmarkMapPop[bigType, bigType]))
+       b.Run("Key=int32/Elem=bigType", benchSizes(benchmarkMapPop[int32, bigType]))
+       b.Run("Key=*int32/Elem=int32", benchSizes(benchmarkMapPop[*int32, int32]))
+       b.Run("Key=int32/Elem=*int32", benchSizes(benchmarkMapPop[int32, *int32]))
+}
index 8a73b9ff6fdd503ae52e73657a3462630bc58480..0e1342f90496451a54842b49454eab039fe868a6 100644 (file)
@@ -639,27 +639,6 @@ func TestIgnoreBogusMapHint(t *testing.T) {
        }
 }
 
-func benchmarkMapPop(b *testing.B, n int) {
-       m := map[int]int{}
-       for i := 0; i < b.N; i++ {
-               for j := 0; j < n; j++ {
-                       m[j] = j
-               }
-               for j := 0; j < n; j++ {
-                       // Use iterator to pop an element.
-                       // We want this to be fast, see issue 8412.
-                       for k := range m {
-                               delete(m, k)
-                               break
-                       }
-               }
-       }
-}
-
-func BenchmarkMapPop100(b *testing.B)   { benchmarkMapPop(b, 100) }
-func BenchmarkMapPop1000(b *testing.B)  { benchmarkMapPop(b, 1000) }
-func BenchmarkMapPop10000(b *testing.B) { benchmarkMapPop(b, 10000) }
-
 var testNonEscapingMapVariable int = 8
 
 func TestNonEscapingMap(t *testing.T) {
@@ -698,198 +677,6 @@ func TestNonEscapingMap(t *testing.T) {
 
 }
 
-func benchmarkMapAssignInt32(b *testing.B, n int) {
-       a := make(map[int32]int)
-       for i := 0; i < b.N; i++ {
-               a[int32(i&(n-1))] = i
-       }
-}
-
-func benchmarkMapOperatorAssignInt32(b *testing.B, n int) {
-       a := make(map[int32]int)
-       for i := 0; i < b.N; i++ {
-               a[int32(i&(n-1))] += i
-       }
-}
-
-func benchmarkMapAppendAssignInt32(b *testing.B, n int) {
-       a := make(map[int32][]int)
-       b.ReportAllocs()
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               key := int32(i & (n - 1))
-               a[key] = append(a[key], i)
-       }
-}
-
-func benchmarkMapDeleteInt32(b *testing.B, n int) {
-       a := make(map[int32]int, n)
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               if len(a) == 0 {
-                       b.StopTimer()
-                       for j := i; j < i+n; j++ {
-                               a[int32(j)] = j
-                       }
-                       b.StartTimer()
-               }
-               delete(a, int32(i))
-       }
-}
-
-func benchmarkMapAssignInt64(b *testing.B, n int) {
-       a := make(map[int64]int)
-       for i := 0; i < b.N; i++ {
-               a[int64(i&(n-1))] = i
-       }
-}
-
-func benchmarkMapOperatorAssignInt64(b *testing.B, n int) {
-       a := make(map[int64]int)
-       for i := 0; i < b.N; i++ {
-               a[int64(i&(n-1))] += i
-       }
-}
-
-func benchmarkMapAppendAssignInt64(b *testing.B, n int) {
-       a := make(map[int64][]int)
-       b.ReportAllocs()
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               key := int64(i & (n - 1))
-               a[key] = append(a[key], i)
-       }
-}
-
-func benchmarkMapDeleteInt64(b *testing.B, n int) {
-       a := make(map[int64]int, n)
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               if len(a) == 0 {
-                       b.StopTimer()
-                       for j := i; j < i+n; j++ {
-                               a[int64(j)] = j
-                       }
-                       b.StartTimer()
-               }
-               delete(a, int64(i))
-       }
-}
-
-func benchmarkMapAssignStr(b *testing.B, n int) {
-       k := make([]string, n)
-       for i := 0; i < len(k); i++ {
-               k[i] = strconv.Itoa(i)
-       }
-       b.ResetTimer()
-       a := make(map[string]int)
-       for i := 0; i < b.N; i++ {
-               a[k[i&(n-1)]] = i
-       }
-}
-
-func benchmarkMapOperatorAssignStr(b *testing.B, n int) {
-       k := make([]string, n)
-       for i := 0; i < len(k); i++ {
-               k[i] = strconv.Itoa(i)
-       }
-       b.ResetTimer()
-       a := make(map[string]string)
-       for i := 0; i < b.N; i++ {
-               key := k[i&(n-1)]
-               a[key] += key
-       }
-}
-
-func benchmarkMapAppendAssignStr(b *testing.B, n int) {
-       k := make([]string, n)
-       for i := 0; i < len(k); i++ {
-               k[i] = strconv.Itoa(i)
-       }
-       a := make(map[string][]string)
-       b.ReportAllocs()
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               key := k[i&(n-1)]
-               a[key] = append(a[key], key)
-       }
-}
-
-func benchmarkMapDeleteStr(b *testing.B, n int) {
-       i2s := make([]string, n)
-       for i := 0; i < n; i++ {
-               i2s[i] = strconv.Itoa(i)
-       }
-       a := make(map[string]int, n)
-       b.ResetTimer()
-       k := 0
-       for i := 0; i < b.N; i++ {
-               if len(a) == 0 {
-                       b.StopTimer()
-                       for j := 0; j < n; j++ {
-                               a[i2s[j]] = j
-                       }
-                       k = i
-                       b.StartTimer()
-               }
-               delete(a, i2s[i-k])
-       }
-}
-
-func benchmarkMapDeletePointer(b *testing.B, n int) {
-       i2p := make([]*int, n)
-       for i := 0; i < n; i++ {
-               i2p[i] = new(int)
-       }
-       a := make(map[*int]int, n)
-       b.ResetTimer()
-       k := 0
-       for i := 0; i < b.N; i++ {
-               if len(a) == 0 {
-                       b.StopTimer()
-                       for j := 0; j < n; j++ {
-                               a[i2p[j]] = j
-                       }
-                       k = i
-                       b.StartTimer()
-               }
-               delete(a, i2p[i-k])
-       }
-}
-
-func runWith(f func(*testing.B, int), v ...int) func(*testing.B) {
-       return func(b *testing.B) {
-               for _, n := range v {
-                       b.Run(strconv.Itoa(n), func(b *testing.B) { f(b, n) })
-               }
-       }
-}
-
-func BenchmarkMapAssign(b *testing.B) {
-       b.Run("Int32", runWith(benchmarkMapAssignInt32, 1<<8, 1<<16))
-       b.Run("Int64", runWith(benchmarkMapAssignInt64, 1<<8, 1<<16))
-       b.Run("Str", runWith(benchmarkMapAssignStr, 1<<8, 1<<16))
-}
-
-func BenchmarkMapOperatorAssign(b *testing.B) {
-       b.Run("Int32", runWith(benchmarkMapOperatorAssignInt32, 1<<8, 1<<16))
-       b.Run("Int64", runWith(benchmarkMapOperatorAssignInt64, 1<<8, 1<<16))
-       b.Run("Str", runWith(benchmarkMapOperatorAssignStr, 1<<8, 1<<16))
-}
-
-func BenchmarkMapAppendAssign(b *testing.B) {
-       b.Run("Int32", runWith(benchmarkMapAppendAssignInt32, 1<<8, 1<<16))
-       b.Run("Int64", runWith(benchmarkMapAppendAssignInt64, 1<<8, 1<<16))
-       b.Run("Str", runWith(benchmarkMapAppendAssignStr, 1<<8, 1<<16))
-}
-
-func BenchmarkMapDelete(b *testing.B) {
-       b.Run("Int32", runWith(benchmarkMapDeleteInt32, 100, 1000, 10000))
-       b.Run("Int64", runWith(benchmarkMapDeleteInt64, 100, 1000, 10000))
-       b.Run("Str", runWith(benchmarkMapDeleteStr, 100, 1000, 10000))
-       b.Run("Pointer", runWith(benchmarkMapDeletePointer, 100, 1000, 10000))
-}
-
 func TestDeferDeleteSlow(t *testing.T) {
        ks := []complex128{0, 1, 2, 3}