Repository: puzpuzpuz/xsync
Pull request #137 (2024-07-07 14:02:39)

Add NewMapOfWithHasher function
Adds `NewMapOfWithHasher` function to support custom hash functions:
```go
m := NewMapOfWithHasher[int, int](func(i int, _ uint64) uint64 {
	// Murmur3 finalizer. No DDOS protection as it does not support seed.
	h := uint64(i)
	h = (h ^ (h >> 33)) * 0xff51afd7ed558ccd
	h = (h ^ (h >> 33)) * 0xc4ceb9fe1a85ec53
	return h ^ (h >> 33)
})
```

Some custom hash functions may be faster than the built-in function if the lack of DDOS protection is fine.

Murmur3 finalizer:
```bash
BenchmarkMapOfInt_Murmur3Finalizer_WarmUp/reads=100%-8         	525864650	         2.240 ns/op	 446360938 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_Murmur3Finalizer_WarmUp/reads=99%-8          	383333918	         3.127 ns/op	 319827294 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_Murmur3Finalizer_WarmUp/reads=90%-8          	267635385	         4.535 ns/op	 220506863 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_Murmur3Finalizer_WarmUp/reads=75%-8          	181292007	         6.448 ns/op	 155092697 ops/s	       2 B/op	       0 allocs/op
```

Built-in hash function:
```bash
BenchmarkMapOfInt_WarmUp/reads=100%-8         	431097415	         2.858 ns/op	 349837551 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_WarmUp/reads=99%-8          	307244330	         3.951 ns/op	 253072903 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_WarmUp/reads=90%-8          	226392990	         5.306 ns/op	 188477583 ops/s	       0 B/op	       0 allocs/op
BenchmarkMapOfInt_WarmUp/reads=75%-8          	159236962	         7.513 ns/op	 133108546 ops/s	       2 B/op	       0 allocs/op
```

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/puzpuzpuz/xsync/pull/137?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.55%. Comparing base [(`55a8a3a`)](https://app.codecov.io/gh/puzpuzpuz/xsync/commit/55a8a3a618905f39ba9979dd6e15f3e37c44774e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov) to head [(`03661d7`)](https://app.codecov.io/gh/puzpuzpuz/xsync/commit/03661d71acafd6d3723278c184dbc2988ca7ba1d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #137      +/-   ##
==========================================
+ Coverage   97.87%   98.55%   +0.68%     
==========================================
  Files           8        8              
  Lines        1174     1174              
==========================================
+ Hits         1149     1157       +8     
+ Misses         21       14       -7     
+ Partials        4        3       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/puzpuzpuz/xsync/pull/137?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov).


Patch:
diff --git a/mapof.go b/mapof.go
index 9b8737c..b7acb2f 100644
--- a/mapof.go
+++ b/mapof.go
@@ -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] {
@@ -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 {
diff --git a/util_hash.go b/util_hash.go
index 9588dcd..9aa6597 100644
--- a/util_hash.go
+++ b/util_hash.go
@@ -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 {


Test patch:
diff --git a/export_test.go b/export_test.go
index 728a06b..5e7cb39 100644
--- a/export_test.go
+++ b/export_test.go
@@ -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]()
 }
diff --git a/mapof_test.go b/mapof_test.go
index 849841a..ffbb85d 100644
--- a/mapof_test.go
+++ b/mapof_test.go
@@ -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
@@ -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 {
diff --git a/util_hash_test.go b/util_hash_test.go
index 70dd496..50a576a 100644
--- a/util_hash_test.go
+++ b/util_hash_test.go
@@ -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]()
 
@@ -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()