From b4e42929851225544fefc51bf692666c3eadbc14 Mon Sep 17 00:00:00 2001 From: JT Archie Date: Sun, 24 Mar 2024 22:05:39 -0600 Subject: [PATCH 1/2] use generic to allow 32 and 64 uint keys --- bits.go | 4 +- bits_test.go | 8 ++-- map.go | 101 ++++++++++++++++++++++++++--------------------- map_fuzz_test.go | 2 +- map_test.go | 52 ++++++++++++------------ 5 files changed, 90 insertions(+), 77 deletions(-) diff --git a/bits.go b/bits.go index f61013a..0aba7c5 100644 --- a/bits.go +++ b/bits.go @@ -40,8 +40,8 @@ func metaMatchEmpty(m *metadata) bitset { return hasZeroByte(castUint64(m) ^ hiBits) } -func nextMatch(b *bitset) uint32 { - s := uint32(bits.TrailingZeros64(uint64(*b))) +func nextMatch[S Size](b *bitset) S { + s := S(bits.TrailingZeros64(uint64(*b))) *b &= ^(1 << s) // clear bit |s| return s >> 3 // div by 8 } diff --git a/bits_test.go b/bits_test.go index b928d55..d92fec2 100644 --- a/bits_test.go +++ b/bits_test.go @@ -32,7 +32,7 @@ func TestMatchMetadata(t *testing.T) { for _, x := range meta { mask := metaMatchH2(&meta, h2(x)) assert.NotZero(t, mask) - assert.Equal(t, uint32(x), nextMatch(&mask)) + assert.Equal(t, uint32(x), nextMatch[uint32](&mask)) } }) t.Run("metaMatchEmpty", func(t *testing.T) { @@ -42,7 +42,7 @@ func TestMatchMetadata(t *testing.T) { meta[i] = empty mask = metaMatchEmpty(&meta) assert.NotZero(t, mask) - assert.Equal(t, uint32(i), nextMatch(&mask)) + assert.Equal(t, uint32(i), nextMatch[uint32](&mask)) meta[i] = int8(i) } }) @@ -51,14 +51,14 @@ func TestMatchMetadata(t *testing.T) { meta = newEmptyMetadata() mask := metaMatchEmpty(&meta) for i := range meta { - assert.Equal(t, uint32(i), nextMatch(&mask)) + assert.Equal(t, uint32(i), nextMatch[uint32](&mask)) } for i := 0; i < len(meta); i += 2 { meta[i] = int8(42) } mask = metaMatchH2(&meta, h2(42)) for i := 0; i < len(meta); i += 2 { - assert.Equal(t, uint32(i), nextMatch(&mask)) + assert.Equal(t, uint32(i), nextMatch[uint32](&mask)) } }) } diff --git a/map.go b/map.go index d2b188e..94ef70a 100644 --- a/map.go +++ b/map.go @@ -22,15 +22,19 @@ const ( maxLoadFactor = float32(maxAvgGroupLoad) / float32(groupSize) ) +type Size interface { + uint32 | uint64 +} + // Map is an open-addressing hash map // based on Abseil's flat_hash_map. -type Map[K comparable, V any] struct { +type Map[K comparable, V any, S Size] struct { ctrl []metadata groups []group[K, V] hash maphash.Hasher[K] - resident uint32 - dead uint32 - limit uint32 + resident S + dead S + limit S } // metadata is the h2 metadata array for a group. @@ -58,9 +62,18 @@ type h1 uint64 type h2 int8 // NewMap constructs a Map. -func NewMap[K comparable, V any](sz uint32) (m *Map[K, V]) { +func NewMap[K comparable, V any](sz uint32) (m *Map[K, V, uint32]) { + return newMap[K, V, uint32](sz) +} + +// NewMap constructs a Map. +func NewMap64[K comparable, V any](sz uint64) (m *Map[K, V, uint64]) { + return newMap[K, V, uint64](sz) +} + +func newMap[K comparable, V any, S Size](sz S) (m *Map[K, V, S]) { groups := numGroups(sz) - m = &Map[K, V]{ + m = &Map[K, V, S]{ ctrl: make([]metadata, groups), groups: make([]group[K, V], groups), hash: maphash.NewHasher[K](), @@ -73,13 +86,13 @@ func NewMap[K comparable, V any](sz uint32) (m *Map[K, V]) { } // Has returns true if |key| is present in |m|. -func (m *Map[K, V]) Has(key K) (ok bool) { +func (m *Map[K, V, S]) Has(key K) (ok bool) { hi, lo := splitHash(m.hash.Hash(key)) - g := probeStart(hi, len(m.groups)) + g := probeStart[S](hi, len(m.groups)) for { // inlined find loop matches := metaMatchH2(&m.ctrl[g], lo) for matches != 0 { - s := nextMatch(&matches) + s := nextMatch[S](&matches) if key == m.groups[g].keys[s] { ok = true return @@ -93,20 +106,20 @@ func (m *Map[K, V]) Has(key K) (ok bool) { return } g += 1 // linear probing - if g >= uint32(len(m.groups)) { + if g >= S(len(m.groups)) { g = 0 } } } // Get returns the |value| mapped by |key| if one exists. -func (m *Map[K, V]) Get(key K) (value V, ok bool) { +func (m *Map[K, V, S]) Get(key K) (value V, ok bool) { hi, lo := splitHash(m.hash.Hash(key)) - g := probeStart(hi, len(m.groups)) + g := probeStart[S](hi, len(m.groups)) for { // inlined find loop matches := metaMatchH2(&m.ctrl[g], lo) for matches != 0 { - s := nextMatch(&matches) + s := nextMatch[S](&matches) if key == m.groups[g].keys[s] { value, ok = m.groups[g].values[s], true return @@ -120,23 +133,23 @@ func (m *Map[K, V]) Get(key K) (value V, ok bool) { return } g += 1 // linear probing - if g >= uint32(len(m.groups)) { + if g >= S(len(m.groups)) { g = 0 } } } // Put attempts to insert |key| and |value| -func (m *Map[K, V]) Put(key K, value V) { +func (m *Map[K, V, S]) Put(key K, value V) { if m.resident >= m.limit { m.rehash(m.nextSize()) } hi, lo := splitHash(m.hash.Hash(key)) - g := probeStart(hi, len(m.groups)) + g := probeStart[S](hi, len(m.groups)) for { // inlined find loop matches := metaMatchH2(&m.ctrl[g], lo) for matches != 0 { - s := nextMatch(&matches) + s := nextMatch[S](&matches) if key == m.groups[g].keys[s] { // update m.groups[g].keys[s] = key m.groups[g].values[s] = value @@ -147,7 +160,7 @@ func (m *Map[K, V]) Put(key K, value V) { // stop probing if we see an empty slot matches = metaMatchEmpty(&m.ctrl[g]) if matches != 0 { // insert - s := nextMatch(&matches) + s := nextMatch[S](&matches) m.groups[g].keys[s] = key m.groups[g].values[s] = value m.ctrl[g][s] = int8(lo) @@ -155,20 +168,20 @@ func (m *Map[K, V]) Put(key K, value V) { return } g += 1 // linear probing - if g >= uint32(len(m.groups)) { + if g >= S(len(m.groups)) { g = 0 } } } // Delete attempts to remove |key|, returns true successful. -func (m *Map[K, V]) Delete(key K) (ok bool) { +func (m *Map[K, V, S]) Delete(key K) (ok bool) { hi, lo := splitHash(m.hash.Hash(key)) - g := probeStart(hi, len(m.groups)) + g := probeStart[S](hi, len(m.groups)) for { matches := metaMatchH2(&m.ctrl[g], lo) for matches != 0 { - s := nextMatch(&matches) + s := nextMatch[S](&matches) if key == m.groups[g].keys[s] { ok = true // optimization: if |m.ctrl[g]| contains any empty @@ -200,7 +213,7 @@ func (m *Map[K, V]) Delete(key K) (ok bool) { return } g += 1 // linear probing - if g >= uint32(len(m.groups)) { + if g >= S(len(m.groups)) { g = 0 } } @@ -211,12 +224,12 @@ func (m *Map[K, V]) Delete(key K) (ok bool) { // for un-mutated Maps, every key will be visited once. If the Map is // Mutated during iteration, mutations will be reflected on return from // Iter, but the set of keys visited by Iter is non-deterministic. -func (m *Map[K, V]) Iter(cb func(k K, v V) (stop bool)) { +func (m *Map[K, V, S]) Iter(cb func(k K, v V) (stop bool)) { // take a consistent view of the table in case // we rehash during iteration ctrl, groups := m.ctrl, m.groups // pick a random starting group - g := randIntN(len(groups)) + g := S(randIntN(len(groups))) for n := 0; n < len(groups); n++ { for s, c := range ctrl[g] { if c == empty || c == tombstone { @@ -228,14 +241,14 @@ func (m *Map[K, V]) Iter(cb func(k K, v V) (stop bool)) { } } g++ - if g >= uint32(len(groups)) { + if g >= S(len(groups)) { g = 0 } } } // Clear removes all elements from the Map. -func (m *Map[K, V]) Clear() { +func (m *Map[K, V, S]) Clear() { for i, c := range m.ctrl { for j := range c { m.ctrl[i][j] = empty @@ -254,24 +267,24 @@ func (m *Map[K, V]) Clear() { } // Count returns the number of elements in the Map. -func (m *Map[K, V]) Count() int { +func (m *Map[K, V, S]) Count() int { return int(m.resident - m.dead) } // Capacity returns the number of additional elements // the can be added to the Map before resizing. -func (m *Map[K, V]) Capacity() int { +func (m *Map[K, V, S]) Capacity() int { return int(m.limit - m.resident) } // find returns the location of |key| if present, or its insertion location if absent. // for performance, find is manually inlined into public methods. -func (m *Map[K, V]) find(key K, hi h1, lo h2) (g, s uint32, ok bool) { - g = probeStart(hi, len(m.groups)) +func (m *Map[K, V, S]) find(key K, hi h1, lo h2) (g, s S, ok bool) { + g = probeStart[S](hi, len(m.groups)) for { matches := metaMatchH2(&m.ctrl[g], lo) for matches != 0 { - s = nextMatch(&matches) + s = nextMatch[S](&matches) if key == m.groups[g].keys[s] { return g, s, true } @@ -280,25 +293,25 @@ func (m *Map[K, V]) find(key K, hi h1, lo h2) (g, s uint32, ok bool) { // stop probing if we see an empty slot matches = metaMatchEmpty(&m.ctrl[g]) if matches != 0 { - s = nextMatch(&matches) + s = nextMatch[S](&matches) return g, s, false } g += 1 // linear probing - if g >= uint32(len(m.groups)) { + if g >= S(len(m.groups)) { g = 0 } } } -func (m *Map[K, V]) nextSize() (n uint32) { - n = uint32(len(m.groups)) * 2 +func (m *Map[K, V, S]) nextSize() (n S) { + n = S(len(m.groups)) * 2 if m.dead >= (m.resident / 2) { - n = uint32(len(m.groups)) + n = S(len(m.groups)) } return } -func (m *Map[K, V]) rehash(n uint32) { +func (m *Map[K, V, S]) rehash(n S) { groups, ctrl := m.groups, m.ctrl m.groups = make([]group[K, V], n) m.ctrl = make([]metadata, n) @@ -319,13 +332,13 @@ func (m *Map[K, V]) rehash(n uint32) { } } -func (m *Map[K, V]) loadFactor() float32 { +func (m *Map[K, V, S]) loadFactor() float32 { slots := float32(len(m.groups) * groupSize) return float32(m.resident-m.dead) / slots } // numGroups returns the minimum number of groups needed to store |n| elems. -func numGroups(n uint32) (groups uint32) { +func numGroups[S Size](n S) (groups S) { groups = (n + maxAvgGroupLoad - 1) / maxAvgGroupLoad if groups == 0 { groups = 1 @@ -344,11 +357,11 @@ func splitHash(h uint64) (h1, h2) { return h1((h & h1Mask) >> 7), h2(h & h2Mask) } -func probeStart(hi h1, groups int) uint32 { - return fastModN(uint32(hi), uint32(groups)) +func probeStart[S Size](hi h1, groups int) S { + return fastModN(S(hi), S(groups)) } // lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ -func fastModN(x, n uint32) uint32 { - return uint32((uint64(x) * uint64(n)) >> 32) +func fastModN[S Size](x, n S) S { + return S((uint64(x) * uint64(n)) >> 32) } diff --git a/map_fuzz_test.go b/map_fuzz_test.go index bfc6a68..5b174dd 100644 --- a/map_fuzz_test.go +++ b/map_fuzz_test.go @@ -93,7 +93,7 @@ type hasher struct { seed uintptr } -func setConstSeed[K comparable, V any](m *Map[K, V], seed uintptr) { +func setConstSeed[K comparable, V any, S Size](m *Map[K, V, S], seed uintptr) { h := (*hasher)((unsafe.Pointer)(&m.hash)) h.seed = seed } diff --git a/map_test.go b/map_test.go index 503f4fe..8d68d9f 100644 --- a/map_test.go +++ b/map_test.go @@ -313,7 +313,7 @@ func testProbeStats[K comparable](t *testing.T, keys []K) { m.Put(key, i) } // todo: assert stat invariants? - stats := getProbeStats(t, m, keys) + stats := getProbeStats[K, int, uint32](t, m, keys) t.Log(fmtProbeStats(stats)) } t.Run("load_factor=0.5", func(t *testing.T) { @@ -339,20 +339,20 @@ func loadFactorSample(n uint32, targetLoad float32) (mapSz, sampleSz uint32) { return } -type probeStats struct { - groups uint32 +type probeStats[S Size] struct { + groups S loadFactor float32 - presentCnt uint32 - presentMin uint32 - presentMax uint32 + presentCnt S + presentMin S + presentMax S presentAvg float32 - absentCnt uint32 - absentMin uint32 - absentMax uint32 + absentCnt S + absentMin S + absentMax S absentAvg float32 } -func fmtProbeStats(s probeStats) string { +func fmtProbeStats[S Size](s probeStats[S]) string { g := fmt.Sprintf("groups=%d load=%f\n", s.groups, s.loadFactor) p := fmt.Sprintf("present(n=%d): min=%d max=%d avg=%f\n", s.presentCnt, s.presentMin, s.presentMax, s.presentAvg) @@ -361,21 +361,21 @@ func fmtProbeStats(s probeStats) string { return g + p + a } -func getProbeLength[K comparable, V any](t *testing.T, m *Map[K, V], key K) (length uint32, ok bool) { - var end uint32 +func getProbeLength[K comparable, V any, S Size](t *testing.T, m *Map[K, V, S], key K) (length S, ok bool) { + var end S hi, lo := splitHash(m.hash.Hash(key)) - start := probeStart(hi, len(m.groups)) + start := probeStart[S](hi, len(m.groups)) end, _, ok = m.find(key, hi, lo) if end < start { // wrapped - end += uint32(len(m.groups)) + end += S(len(m.groups)) } length = (end - start) + 1 require.True(t, length > 0) return } -func getProbeStats[K comparable, V any](t *testing.T, m *Map[K, V], keys []K) (stats probeStats) { - stats.groups = uint32(len(m.groups)) +func getProbeStats[K comparable, V any, S Size](t *testing.T, m *Map[K, V, S], keys []K) (stats probeStats[S]) { + stats.groups = S(len(m.groups)) stats.loadFactor = m.loadFactor() var presentSum, absentSum float32 stats.presentMin = math.MaxInt32 @@ -416,19 +416,19 @@ func getProbeStats[K comparable, V any](t *testing.T, m *Map[K, V], keys []K) (s } func TestNumGroups(t *testing.T) { - assert.Equal(t, expected(0), numGroups(0)) - assert.Equal(t, expected(1), numGroups(1)) + assert.Equal(t, expected[uint32](0), numGroups[uint32](0)) + assert.Equal(t, expected[uint32](1), numGroups[uint32](1)) // max load factor 0.875 - assert.Equal(t, expected(14), numGroups(14)) - assert.Equal(t, expected(15), numGroups(15)) - assert.Equal(t, expected(28), numGroups(28)) - assert.Equal(t, expected(29), numGroups(29)) - assert.Equal(t, expected(56), numGroups(56)) - assert.Equal(t, expected(57), numGroups(57)) + assert.Equal(t, expected[uint32](14), numGroups[uint32](14)) + assert.Equal(t, expected[uint32](15), numGroups[uint32](15)) + assert.Equal(t, expected[uint32](28), numGroups[uint32](28)) + assert.Equal(t, expected[uint32](29), numGroups[uint32](29)) + assert.Equal(t, expected[uint32](56), numGroups[uint32](56)) + assert.Equal(t, expected[uint32](57), numGroups[uint32](57)) } -func expected(x int) (groups uint32) { - groups = uint32(math.Ceil(float64(x) / float64(maxAvgGroupLoad))) +func expected[S Size](x int) (groups S) { + groups = S(math.Ceil(float64(x) / float64(maxAvgGroupLoad))) if groups == 0 { groups = 1 } From df30a34691ec442601d72d597e6c4ec5cca9c849 Mon Sep 17 00:00:00 2001 From: JT Archie Date: Thu, 30 May 2024 09:00:29 -0600 Subject: [PATCH 2/2] use constraints.Unsigned for allowable integers --- bits.go | 4 +++- go.mod | 1 + go.sum | 2 ++ map.go | 15 ++++++--------- map_fuzz_test.go | 3 ++- map_test.go | 15 ++++++++------- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/bits.go b/bits.go index 0aba7c5..fe411fe 100644 --- a/bits.go +++ b/bits.go @@ -19,6 +19,8 @@ package swiss import ( "math/bits" "unsafe" + + "golang.org/x/exp/constraints" ) const ( @@ -40,7 +42,7 @@ func metaMatchEmpty(m *metadata) bitset { return hasZeroByte(castUint64(m) ^ hiBits) } -func nextMatch[S Size](b *bitset) S { +func nextMatch[S constraints.Unsigned](b *bitset) S { s := S(bits.TrailingZeros64(uint64(*b))) *b &= ^(1 << s) // clear bit |s| return s >> 3 // div by 8 diff --git a/go.mod b/go.mod index 87f1c8f..232fc69 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0e9147a..357fbee 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/map.go b/map.go index 94ef70a..94d452b 100644 --- a/map.go +++ b/map.go @@ -16,19 +16,16 @@ package swiss import ( "github.com/dolthub/maphash" + "golang.org/x/exp/constraints" ) const ( maxLoadFactor = float32(maxAvgGroupLoad) / float32(groupSize) ) -type Size interface { - uint32 | uint64 -} - // Map is an open-addressing hash map // based on Abseil's flat_hash_map. -type Map[K comparable, V any, S Size] struct { +type Map[K comparable, V any, S constraints.Unsigned] struct { ctrl []metadata groups []group[K, V] hash maphash.Hasher[K] @@ -71,7 +68,7 @@ func NewMap64[K comparable, V any](sz uint64) (m *Map[K, V, uint64]) { return newMap[K, V, uint64](sz) } -func newMap[K comparable, V any, S Size](sz S) (m *Map[K, V, S]) { +func newMap[K comparable, V any, S constraints.Unsigned](sz S) (m *Map[K, V, S]) { groups := numGroups(sz) m = &Map[K, V, S]{ ctrl: make([]metadata, groups), @@ -338,7 +335,7 @@ func (m *Map[K, V, S]) loadFactor() float32 { } // numGroups returns the minimum number of groups needed to store |n| elems. -func numGroups[S Size](n S) (groups S) { +func numGroups[S constraints.Unsigned](n S) (groups S) { groups = (n + maxAvgGroupLoad - 1) / maxAvgGroupLoad if groups == 0 { groups = 1 @@ -357,11 +354,11 @@ func splitHash(h uint64) (h1, h2) { return h1((h & h1Mask) >> 7), h2(h & h2Mask) } -func probeStart[S Size](hi h1, groups int) S { +func probeStart[S constraints.Unsigned](hi h1, groups int) S { return fastModN(S(hi), S(groups)) } // lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ -func fastModN[S Size](x, n S) S { +func fastModN[S constraints.Unsigned](x, n S) S { return S((uint64(x) * uint64(n)) >> 32) } diff --git a/map_fuzz_test.go b/map_fuzz_test.go index 5b174dd..e7714d4 100644 --- a/map_fuzz_test.go +++ b/map_fuzz_test.go @@ -19,6 +19,7 @@ import ( "unsafe" "github.com/stretchr/testify/assert" + "golang.org/x/exp/constraints" ) func FuzzStringMap(f *testing.F) { @@ -93,7 +94,7 @@ type hasher struct { seed uintptr } -func setConstSeed[K comparable, V any, S Size](m *Map[K, V, S], seed uintptr) { +func setConstSeed[K comparable, V any, S constraints.Unsigned](m *Map[K, V, S], seed uintptr) { h := (*hasher)((unsafe.Pointer)(&m.hash)) h.seed = seed } diff --git a/map_test.go b/map_test.go index 8d68d9f..92d4c41 100644 --- a/map_test.go +++ b/map_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "golang.org/x/exp/constraints" "github.com/stretchr/testify/assert" ) @@ -339,7 +340,7 @@ func loadFactorSample(n uint32, targetLoad float32) (mapSz, sampleSz uint32) { return } -type probeStats[S Size] struct { +type probeStats[S constraints.Unsigned] struct { groups S loadFactor float32 presentCnt S @@ -352,7 +353,7 @@ type probeStats[S Size] struct { absentAvg float32 } -func fmtProbeStats[S Size](s probeStats[S]) string { +func fmtProbeStats[S constraints.Unsigned](s probeStats[S]) string { g := fmt.Sprintf("groups=%d load=%f\n", s.groups, s.loadFactor) p := fmt.Sprintf("present(n=%d): min=%d max=%d avg=%f\n", s.presentCnt, s.presentMin, s.presentMax, s.presentAvg) @@ -361,7 +362,7 @@ func fmtProbeStats[S Size](s probeStats[S]) string { return g + p + a } -func getProbeLength[K comparable, V any, S Size](t *testing.T, m *Map[K, V, S], key K) (length S, ok bool) { +func getProbeLength[K comparable, V any, S constraints.Unsigned](t *testing.T, m *Map[K, V, S], key K) (length S, ok bool) { var end S hi, lo := splitHash(m.hash.Hash(key)) start := probeStart[S](hi, len(m.groups)) @@ -374,12 +375,12 @@ func getProbeLength[K comparable, V any, S Size](t *testing.T, m *Map[K, V, S], return } -func getProbeStats[K comparable, V any, S Size](t *testing.T, m *Map[K, V, S], keys []K) (stats probeStats[S]) { +func getProbeStats[K comparable, V any, S constraints.Unsigned](t *testing.T, m *Map[K, V, S], keys []K) (stats probeStats[S]) { stats.groups = S(len(m.groups)) stats.loadFactor = m.loadFactor() var presentSum, absentSum float32 - stats.presentMin = math.MaxInt32 - stats.absentMin = math.MaxInt32 + stats.presentMin = math.MaxUint8 + stats.absentMin = math.MaxUint8 for _, key := range keys { l, ok := getProbeLength(t, m, key) if ok { @@ -427,7 +428,7 @@ func TestNumGroups(t *testing.T) { assert.Equal(t, expected[uint32](57), numGroups[uint32](57)) } -func expected[S Size](x int) (groups S) { +func expected[S constraints.Unsigned](x int) (groups S) { groups = S(math.Ceil(float64(x) / float64(maxAvgGroupLoad))) if groups == 0 { groups = 1