Skip to content

Commit

Permalink
Add NewMapOfWithHasher function (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
puzpuzpuz authored Jul 7, 2024
1 parent 55a8a3a commit e96db0e
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 28 deletions.
11 changes: 2 additions & 9 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ func HashString(s string, seed uint64) uint64 {
return hashString(s, seed)
}

func MakeHasher[T comparable]() func(T, uint64) uint64 {
return makeHasher[T]()
}

func NewMapOfWithHasher[K comparable, V any](
hasher func(K, uint64) uint64,
options ...func(*MapConfig),
) *MapOf[K, V] {
return newMapOf[K, V](hasher, options...)
func DefaultHasher[T comparable]() func(T, uint64) uint64 {
return defaultHasher[T]()
}
30 changes: 17 additions & 13 deletions mapof.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,14 @@ type entryOf[K comparable, V any] struct {
// NewMapOf creates a new MapOf instance configured with the given
// options.
func NewMapOf[K comparable, V any](options ...func(*MapConfig)) *MapOf[K, V] {
return newMapOf[K, V](makeHasher[K](), options...)
return NewMapOfWithHasher[K, V](defaultHasher[K](), options...)
}

// NewMapOfPresized creates a new MapOf instance with capacity enough
// to hold sizeHint entries. The capacity is treated as the minimal capacity
// meaning that the underlying hash table will never shrink to
// a smaller capacity. If sizeHint is zero or negative, the value
// is ignored.
//
// Deprecated: use NewMapOf in combination with WithPresize.
func NewMapOfPresized[K comparable, V any](sizeHint int) *MapOf[K, V] {
return NewMapOf[K, V](WithPresize(sizeHint))
}

func newMapOf[K comparable, V any](
// NewMapOf creates a new MapOf instance configured with the given
// hasher and options. The hash function is used instead of
// the built-in hash function configured when a map is created
// with the NewMapOf function.
func NewMapOfWithHasher[K comparable, V any](
hasher func(K, uint64) uint64,
options ...func(*MapConfig),
) *MapOf[K, V] {
Expand All @@ -125,6 +118,17 @@ func newMapOf[K comparable, V any](
return m
}

// NewMapOfPresized creates a new MapOf instance with capacity enough
// to hold sizeHint entries. The capacity is treated as the minimal capacity
// meaning that the underlying hash table will never shrink to
// a smaller capacity. If sizeHint is zero or negative, the value
// is ignored.
//
// Deprecated: use NewMapOf in combination with WithPresize.
func NewMapOfPresized[K comparable, V any](sizeHint int) *MapOf[K, V] {
return NewMapOf[K, V](WithPresize(sizeHint))
}

func newMapOfTable[K comparable, V any](minTableLen int) *mapOfTable[K, V] {
buckets := make([]bucketOfPadded, minTableLen)
for i := range buckets {
Expand Down
45 changes: 44 additions & 1 deletion mapof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,31 @@ func TestMapOfStore_StructKeys_StructValues(t *testing.T) {
}
}

func TestMapOfStore_HashCodeCollisions(t *testing.T) {
func TestMapOfWithHasher(t *testing.T) {
const numEntries = 10000
m := NewMapOfWithHasher[int, int](murmur3Finalizer)
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(i)
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}

func murmur3Finalizer(i int, _ uint64) uint64 {
h := uint64(i)
h = (h ^ (h >> 33)) * 0xff51afd7ed558ccd
h = (h ^ (h >> 33)) * 0xc4ceb9fe1a85ec53
return h ^ (h >> 33)
}

func TestMapOfWithHasher_HashCodeCollisions(t *testing.T) {
const numEntries = 1000
m := NewMapOfWithHasher[int, int](func(i int, _ uint64) uint64 {
// We intentionally use an awful hash function here to make sure
Expand Down Expand Up @@ -1282,6 +1306,25 @@ func BenchmarkMapOfInt_WarmUp(b *testing.B) {
}
}

func BenchmarkMapOfInt_Murmur3Finalizer_WarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
m := NewMapOfWithHasher[int, int](murmur3Finalizer, WithPresize(benchmarkNumEntries))
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(i, i)
}
b.ResetTimer()
benchmarkMapOfIntKeys(b, func(k int) (int, bool) {
return m.Load(k)
}, func(k int, v int) {
m.Store(k, v)
}, func(k int) {
m.Delete(k)
}, bc.readPercentage)
})
}
}

func BenchmarkIntMapStandard_NoWarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
if bc.readPercentage == 100 {
Expand Down
4 changes: 2 additions & 2 deletions util_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ func hashString(s string, seed uint64) uint64 {
//go:linkname runtime_memhash runtime.memhash
func runtime_memhash(p unsafe.Pointer, h, s uintptr) uintptr

// makeHasher creates a fast hash function for the given comparable type.
// defaultHasher creates a fast hash function for the given comparable type.
// The only limitation is that the type should not contain interfaces inside
// based on runtime.typehash.
func makeHasher[T comparable]() func(T, uint64) uint64 {
func defaultHasher[T comparable]() func(T, uint64) uint64 {
var zero T

if reflect.TypeOf(&zero).Elem().Kind() == reflect.Interface {
Expand Down
6 changes: 3 additions & 3 deletions util_hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func TestMakeHashFunc(t *testing.T) {

seed := MakeSeed()

hashString := MakeHasher[string]()
hashUser := MakeHasher[User]()
hashString := DefaultHasher[string]()
hashUser := DefaultHasher[User]()

hashUserMap := makeMapHasher[User]()

Expand Down Expand Up @@ -186,7 +186,7 @@ func BenchmarkMakeHashFunc(b *testing.B) {
}

func doBenchmarkMakeHashFunc[T comparable](b *testing.B, val T) {
hash := MakeHasher[T]()
hash := DefaultHasher[T]()
hashNativeMap := makeMapHasher[T]()
seed := MakeSeed()

Expand Down

0 comments on commit e96db0e

Please sign in to comment.