From b11e461ff4e33f4a8a5cce444470b94cc5264115 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Fri, 6 Mar 2026 00:35:45 +0100 Subject: [PATCH] perf: preallocate maps and slices in CountValues, UniqKeys, UniqValues, FilterKeys, FilterValues, FilterKeysErr, FilterValuesErr (#833) * perf: preallocate maps and slices in CountValues, UniqKeys, UniqValues, FilterKeys, FilterValues, FilterKeysErr, FilterValuesErr Add size hints to map and slice allocations to avoid repeated grow-and-copy reallocations: - CountValues: make(map[T]int, len(collection)) - UniqKeys/UniqValues: make([]K, 0, size) - FilterKeys/FilterValues/FilterKeysErr/FilterValuesErr: make([]K, 0, len(in)) * oops --- benchmark/core_benchmark_test.go | 72 ----------------------------- benchmark/map_benchmark_test.go | 78 +++++++++++++++----------------- map.go | 12 ++--- slice.go | 2 +- 4 files changed, 43 insertions(+), 121 deletions(-) diff --git a/benchmark/core_benchmark_test.go b/benchmark/core_benchmark_test.go index 58b9047..fd09ea6 100644 --- a/benchmark/core_benchmark_test.go +++ b/benchmark/core_benchmark_test.go @@ -67,18 +67,6 @@ func BenchmarkValues(b *testing.B) { } } -func BenchmarkUniqValues(b *testing.B) { - for _, n := range coreLengths { - m1 := genMap(n) - m2 := genMap(n) - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = lo.UniqValues(m1, m2) - } - }) - } -} - func BenchmarkValueOr(b *testing.B) { m := genMap(100) b.Run("hit", func(b *testing.B) { @@ -289,28 +277,6 @@ func BenchmarkFilterMapToSlice(b *testing.B) { } } -func BenchmarkFilterKeys(b *testing.B) { - for _, n := range coreLengths { - m := genMap(n) - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = lo.FilterKeys(m, func(_ string, v int) bool { return v%2 == 0 }) - } - }) - } -} - -func BenchmarkFilterValues(b *testing.B) { - for _, n := range coreLengths { - m := genMap(n) - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = lo.FilterValues(m, func(_ string, v int) bool { return v%2 == 0 }) - } - }) - } -} - // --------------------------------------------------------------------------- // find.go // --------------------------------------------------------------------------- @@ -738,18 +704,6 @@ func BenchmarkIntersectBy(b *testing.B) { } } -func BenchmarkDifference(b *testing.B) { - for _, n := range coreLengths { - a := genSliceInt(n) - c := genSliceInt(n) - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = lo.Difference(a, c) - } - }) - } -} - func BenchmarkUnion(b *testing.B) { for _, n := range coreLengths { a := genSliceInt(n) @@ -1052,32 +1006,6 @@ func BenchmarkCoreToSlicePtr(b *testing.B) { } } -func BenchmarkFromSlicePtr(b *testing.B) { - for _, n := range coreLengths { - ptrs := lo.ToSlicePtr(genSliceInt(n)) - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = lo.FromSlicePtr(ptrs) - } - }) - } -} - -func BenchmarkFromSlicePtrOr(b *testing.B) { - for _, n := range coreLengths { - ptrs := lo.ToSlicePtr(genSliceInt(n)) - // sprinkle nils - for j := 0; j < n/10; j++ { - ptrs[j*10] = nil - } - b.Run(strconv.Itoa(n), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = lo.FromSlicePtrOr(ptrs, -1) - } - }) - } -} - func BenchmarkToAnySlice(b *testing.B) { for _, n := range coreLengths { ints := genSliceInt(n) diff --git a/benchmark/map_benchmark_test.go b/benchmark/map_benchmark_test.go index f585122..82f0a19 100644 --- a/benchmark/map_benchmark_test.go +++ b/benchmark/map_benchmark_test.go @@ -74,52 +74,46 @@ func BenchmarkMap(b *testing.B) { }) } -// @TODO: also apply to UniqValues. func BenchmarkUniqKeys(b *testing.B) { m := []map[int64]int64{ - mapGenerator(100000), - mapGenerator(100000), - mapGenerator(100000), + mapGenerator(1000), + mapGenerator(1000), + mapGenerator(1000), } - - // allocate just in time + ordered - b.Run("lo.UniqKeys.jit-alloc", func(b *testing.B) { + b.Run("lo.UniqKeys", func(b *testing.B) { for n := 0; n < b.N; n++ { - seen := make(map[int64]struct{}) - result := make([]int64, 0) - - for i := range m { - for k := range m[i] { - if _, exists := seen[k]; exists { - continue - } - seen[k] = struct{}{} - result = append(result, k) //nolint:staticcheck - } - } - } - }) - - // preallocate + unordered - b.Run("lo.UniqKeys.preallocate", func(b *testing.B) { - for n := 0; n < b.N; n++ { - size := 0 - for i := range m { - size += len(m[i]) - } - seen := make(map[int64]struct{}, size) - - for i := range m { - for k := range m[i] { - seen[k] = struct{}{} - } - } - - result := make([]int64, 0, len(seen)) - - for k := range seen { - result = append(result, k) //nolint:staticcheck - } + _ = lo.UniqKeys(m...) + } + }) +} + +func BenchmarkUniqValues(b *testing.B) { + m := []map[int64]int64{ + mapGenerator(1000), + mapGenerator(1000), + mapGenerator(1000), + } + b.Run("lo.UniqValues", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = lo.UniqValues(m...) + } + }) +} + +func BenchmarkFilterKeys(b *testing.B) { + m := mapGenerator(1000) + b.Run("lo.FilterKeys", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = lo.FilterKeys(m, func(k, v int64) bool { return k%2 == 0 }) + } + }) +} + +func BenchmarkFilterValues(b *testing.B) { + m := mapGenerator(1000) + b.Run("lo.FilterValues", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = lo.FilterValues(m, func(k, v int64) bool { return v%2 == 0 }) } }) } diff --git a/map.go b/map.go index 69d26be..8ae14c5 100644 --- a/map.go +++ b/map.go @@ -27,7 +27,7 @@ func UniqKeys[K comparable, V any](in ...map[K]V) []K { } seen := make(map[K]struct{}, size) - result := make([]K, 0) + result := make([]K, 0, size) for i := range in { for k := range in[i] { @@ -76,7 +76,7 @@ func UniqValues[K, V comparable](in ...map[K]V) []V { } seen := make(map[V]struct{}, size) - result := make([]V, 0) + result := make([]V, 0, size) for i := range in { for _, v := range in[i] { @@ -465,7 +465,7 @@ func FilterMapToSliceErr[K comparable, V, R any](in map[K]V, iteratee func(key K // It is a mix of lo.Filter() and lo.Keys(). // Play: https://go.dev/play/p/OFlKXlPrBAe func FilterKeys[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) []K { - result := make([]K, 0) + result := make([]K, 0, len(in)) for k, v := range in { if predicate(k, v) { @@ -480,7 +480,7 @@ func FilterKeys[K comparable, V any](in map[K]V, predicate func(key K, value V) // It is a mix of lo.Filter() and lo.Values(). // Play: https://go.dev/play/p/YVD5r_h-LX- func FilterValues[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) []V { - result := make([]V, 0) + result := make([]V, 0, len(in)) for k, v := range in { if predicate(k, v) { @@ -498,7 +498,7 @@ func FilterValues[K comparable, V any](in map[K]V, predicate func(key K, value V // The order of the keys in the input map is not specified. // Play: https://go.dev/play/p/j2gUQzCTu4t func FilterKeysErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]K, error) { - result := make([]K, 0) + result := make([]K, 0, len(in)) for k, v := range in { ok, err := predicate(k, v) @@ -520,7 +520,7 @@ func FilterKeysErr[K comparable, V any](in map[K]V, predicate func(key K, value // The order of the keys in the input map is not specified. // Play: https://go.dev/play/p/hKvHlqLzbdE func FilterValuesErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]V, error) { - result := make([]V, 0) + result := make([]V, 0, len(in)) for k, v := range in { ok, err := predicate(k, v) diff --git a/slice.go b/slice.go index 8c8565d..08522cd 100644 --- a/slice.go +++ b/slice.go @@ -1003,7 +1003,7 @@ func CountByErr[T any](collection []T, predicate func(item T) (bool, error)) (in // CountValues counts the number of each element in the collection. // Play: https://go.dev/play/p/-p-PyLT4dfy func CountValues[T comparable](collection []T) map[T]int { - result := make(map[T]int) + result := make(map[T]int, len(collection)) for i := range collection { result[collection[i]]++