From 488e2d18d9d8ac8f51a5e1ea297adaa50c9f4870 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Fri, 28 Jun 2024 17:15:48 +0000 Subject: [PATCH] runtime: more thorough map benchmarks 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 Reviewed-by: Michael Knyszek LUCI-TryBot-Result: Go LUCI Reviewed-by: Keith Randall Auto-Submit: Michael Pratt --- src/runtime/map_benchmark_test.go | 641 +++++++++++++++++++++++++++--- src/runtime/map_test.go | 213 ---------- 2 files changed, 587 insertions(+), 267 deletions(-) diff --git a/src/runtime/map_benchmark_test.go b/src/runtime/map_benchmark_test.go index 43d1accbb97..663abf6202b 100644 --- a/src/runtime/map_benchmark_test.go +++ b/src/runtime/map_benchmark_test.go @@ -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])) +} diff --git a/src/runtime/map_test.go b/src/runtime/map_test.go index 8a73b9ff6fd..0e1342f9049 100644 --- a/src/runtime/map_test.go +++ b/src/runtime/map_test.go @@ -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}