1
0
mirror of https://github.com/golang/go synced 2024-11-21 23:54:40 -07:00

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 <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>
This commit is contained in:
Michael Pratt 2024-06-28 17:15:48 +00:00 committed by Gopher Robot
parent a8e2ecc8b1
commit 488e2d18d9
2 changed files with 587 additions and 267 deletions

View 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]))
}

View 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}