Repository: puzpuzpuz/xsync
Pull request #49 (2022-10-22 08:45:52)

Support arbitrary comparable keys in MapOf
This PR is a continuation of #46

codecov-commenter: # [Codecov](https://codecov.io/gh/puzpuzpuz/xsync/pull/49?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov) Report
Base: **95.18**% // Head: **94.80**% // Decreases project coverage by **`-0.37%`** :warning:
> Coverage data is based on head [(`74831c3`)](https://codecov.io/gh/puzpuzpuz/xsync/pull/49?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov) compared to base [(`fb74891`)](https://codecov.io/gh/puzpuzpuz/xsync/commit/fb7489160937648d8d51da47dfaa7919a97f09a9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov).
> Patch coverage: 100.00% of modified lines in pull request are covered.

> :exclamation: Current head 74831c3 differs from pull request most recent head 5beb610. Consider uploading reports for the commit 5beb610 to get more accurate results

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


```diff
@@            Coverage Diff             @@
##             main      #49      +/-   ##
==========================================
- Coverage   95.18%   94.80%   -0.38%     
==========================================
  Files           6        6              
  Lines         830      866      +36     
==========================================
+ Hits          790      821      +31     
- Misses         32       36       +4     
- Partials        8        9       +1     
```


| [Impacted Files](https://codecov.io/gh/puzpuzpuz/xsync/pull/49?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov) | Coverage Δ | |
|---|---|---|
| [counter.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/49/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov#diff-Y291bnRlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [mapof.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/49/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov#diff-bWFwb2YuZ28=) | `93.11% <100.00%> (-1.05%)` | :arrow_down: |
| [rbmutex.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/49/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov#diff-cmJtdXRleC5nbw==) | `94.87% <100.00%> (ø)` | |
| [util.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/49/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov#diff-dXRpbC5nbw==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/puzpuzpuz/xsync/pull/49?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Pechkurov).


Patch:
diff --git a/README.md b/README.md
index ca36c04..abb2a01 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ s := m.Size()
 
 `Map` uses a modified version of Cache-Line Hash Table (CLHT) data structure: https://github.com/LPD-EPFL/CLHT
 
-CLHT is built around idea to organize the hash table in cache-line-sized buckets, so that on all modern CPUs update operations complete with minimal cache-line transfer. Also, `Get` operations involve no writes to shared memory, hence no mutexes or any other sort of locks. Due to this design, in all considered scenarios `Map` outperforms `sync.Map`.
+CLHT is built around idea to organize the hash table in cache-line-sized buckets, so that on all modern CPUs update operations complete with minimal cache-line transfer. Also, `Get`, `Range` and other read-only operations are obstruction-free and involve no writes to shared memory, hence no mutexes or any other sort of locks. Due to this design, in all considered scenarios `Map` outperforms `sync.Map`.
 
 One important difference with `sync.Map` is that only string keys are supported. That's because Golang standard library does not expose the built-in hash functions for `interface{}` values.
 
@@ -52,6 +52,21 @@ m.Store("foo", "bar")
 v, ok := m.Load("foo")
 ```
 
+One important difference with `Map` is that `MapOf` supports arbitrary `comparable` key types:
+
+```go
+type point struct {
+    x int
+    y int
+}
+// provide a hash function when creating the MapOf
+m := NewTypedMapOf[point, int](func(p point) uint64 {
+    return uint64(31*p.x + p.y)
+})
+m.Store(point{42, 42}, 42)
+v, ok := m.Load(point{42, 42})
+```
+
 ## MPMCQueue
 
 A `MPMCQeueue` is a bounded multi-producer multi-consumer concurrent queue.
diff --git a/counter.go b/counter.go
index 887d093..4d6fb36 100644
--- a/counter.go
+++ b/counter.go
@@ -54,7 +54,7 @@ func (c *Counter) Add(delta int64) {
 	if !ok {
 		t = new(ptoken)
 		// Since cstripes is a power of two, we can use & instead of %.
-		t.idx = uint32(hash64(uintptr(unsafe.Pointer(t))) & (cstripes - 1))
+		t.idx = uint32(mixhash64ptr(uintptr(unsafe.Pointer(t))) & (cstripes - 1))
 	}
 	stripe := &c.stripes[t.idx]
 	atomic.AddInt64(&stripe.c, delta)
diff --git a/mapof.go b/mapof.go
index 77dda58..d073dd9 100644
--- a/mapof.go
+++ b/mapof.go
@@ -30,19 +30,46 @@ import (
 // One important difference with sync.Map is that only string keys
 // are supported. That's because Golang standard library does not
 // expose the built-in hash functions for interface{} values.
-type MapOf[V any] struct {
+type MapOf[K comparable, V any] struct {
 	totalGrowths int64
 	totalShrinks int64
 	resizing     int64          // resize in progress flag; updated atomically
 	resizeMu     sync.Mutex     // only used along with resizeCond
 	resizeCond   sync.Cond      // used to wake up resize waiters (concurrent modifications)
 	table        unsafe.Pointer // *mapTable
+	hasher       func(K) uint64
 }
 
-// NewMapOf creates a new MapOf instance.
-func NewMapOf[V any]() *MapOf[V] {
-	m := &MapOf[V]{}
+// NewMapOf creates a new MapOf instance with string keys
+func NewMapOf[V any]() *MapOf[string, V] {
+	return NewTypedMapOf[string, V](func(k string) uint64 {
+		return maphash64(k)
+	})
+}
+
+// IntegerConstraint represents any integer type.
+type IntegerConstraint interface {
+	// Recreation of golang.org/x/exp/constraints.Integer to avoid taking a dependency on an
+	// experimental package.
+	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
+}
+
+// NewIntegerMapOf creates a new MapOf instance with integer typed keys.
+func NewIntegerMapOf[K IntegerConstraint, V any]() *MapOf[K, V] {
+	return NewTypedMapOf[K, V](func(k K) uint64 {
+		return uint64(k)
+	})
+}
+
+// NewTypedMapOf creates a new MapOf instance with arbitrarily typed keys.
+// Keys are hashed to uint64 using the hasher fn.
+func NewTypedMapOf[K comparable, V any](hasher func(K) uint64) *MapOf[K, V] {
+	m := &MapOf[K, V]{}
 	m.resizeCond = *sync.NewCond(&m.resizeMu)
+	m.hasher = func(k K) uint64 {
+		// Apply finalizer to mitigate poor quality hash functions.
+		return mixhash64(hasher(k))
+	}
 	table := newMapTable(minMapTableLen)
 	atomic.StorePointer(&m.table, unsafe.Pointer(table))
 	return m
@@ -51,8 +78,8 @@ func NewMapOf[V any]() *MapOf[V] {
 // Load returns the value stored in the map for a key, or nil if no
 // value is present.
 // The ok result indicates whether value was found in the map.
-func (m *MapOf[V]) Load(key string) (value V, ok bool) {
-	hash := maphash64(key)
+func (m *MapOf[K, V]) Load(key K) (value V, ok bool) {
+	hash := m.hasher(key)
 	table := (*mapTable)(atomic.LoadPointer(&m.table))
 	bidx := bucketIdx(table, hash)
 	b := &table.buckets[bidx]
@@ -67,7 +94,7 @@ func (m *MapOf[V]) Load(key string) (value V, ok bool) {
 			vp := atomic.LoadPointer(&b.values[i])
 			kp := atomic.LoadPointer(&b.keys[i])
 			if kp != nil && vp != nil {
-				if key == derefKey(kp) {
+				if key == derefTypedKey[K](kp) {
 					if uintptr(vp) == uintptr(atomic.LoadPointer(&b.values[i])) {
 						// Atomic snapshot succeeded.
 						return derefTypedValue[V](vp), true
@@ -86,14 +113,14 @@ func (m *MapOf[V]) Load(key string) (value V, ok bool) {
 }
 
 // Store sets the value for a key.
-func (m *MapOf[V]) Store(key string, value V) {
+func (m *MapOf[K, V]) Store(key K, value V) {
 	m.doStore(key, value, false)
 }
 
 // LoadOrStore returns the existing value for the key if present.
 // Otherwise, it stores and returns the given value.
 // The loaded result is true if the value was loaded, false if stored.
-func (m *MapOf[V]) LoadOrStore(key string, value V) (actual V, loaded bool) {
+func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
 	return m.doStore(key, value, true)
 }
 
@@ -101,11 +128,11 @@ func (m *MapOf[V]) LoadOrStore(key string, value V) (actual V, loaded bool) {
 // while setting the new value for the key.
 // Otherwise, it stores and returns the given value.
 // The loaded result is true if the value was loaded, false otherwise.
-func (m *MapOf[V]) LoadAndStore(key string, value V) (actual V, loaded bool) {
+func (m *MapOf[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) {
 	return m.doStore(key, value, false)
 }
 
-func (m *MapOf[V]) doStore(key string, value V, loadIfExists bool) (V, bool) {
+func (m *MapOf[K, V]) doStore(key K, value V, loadIfExists bool) (V, bool) {
 	// Read-only path.
 	if loadIfExists {
 		if v, ok := m.Load(key); ok {
@@ -113,7 +140,7 @@ func (m *MapOf[V]) doStore(key string, value V, loadIfExists bool) (V, bool) {
 		}
 	}
 	// Write path.
-	hash := maphash64(key)
+	hash := m.hasher(key)
 	for {
 	store_attempt:
 		var (
@@ -151,7 +178,7 @@ func (m *MapOf[V]) doStore(key string, value V, loadIfExists bool) (V, bool) {
 				if !topHashMatch(hash, topHashes, i) {
 					continue
 				}
-				if key == derefKey(b.keys[i]) {
+				if key == derefTypedKey[K](b.keys[i]) {
 					vp := b.values[i]
 					if loadIfExists {
 						unlockBucket(&rootb.topHashMutex)
@@ -206,16 +233,16 @@ func (m *MapOf[V]) doStore(key string, value V, loadIfExists bool) (V, bool) {
 	}
 }
 
-func (m *MapOf[V]) newerTableExists(table *mapTable) bool {
+func (m *MapOf[K, V]) newerTableExists(table *mapTable) bool {
 	curTablePtr := atomic.LoadPointer(&m.table)
 	return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table))
 }
 
-func (m *MapOf[V]) resizeInProgress() bool {
+func (m *MapOf[K, V]) resizeInProgress() bool {
 	return atomic.LoadInt64(&m.resizing) == 1
 }
 
-func (m *MapOf[V]) waitForResize() {
+func (m *MapOf[K, V]) waitForResize() {
 	m.resizeMu.Lock()
 	for m.resizeInProgress() {
 		m.resizeCond.Wait()
@@ -223,7 +250,7 @@ func (m *MapOf[V]) waitForResize() {
 	m.resizeMu.Unlock()
 }
 
-func (m *MapOf[V]) resize(table *mapTable, hint mapResizeHint) {
+func (m *MapOf[K, V]) resize(table *mapTable, hint mapResizeHint) {
 	var shrinkThreshold int64
 	tableLen := len(table.buckets)
 	// Fast path for shrink attempts.
@@ -262,7 +289,7 @@ func (m *MapOf[V]) resize(table *mapTable, hint mapResizeHint) {
 		panic(fmt.Sprintf("unexpected resize hint: %d", hint))
 	}
 	for i := 0; i < tableLen; i++ {
-		copied := copyBucket(&table.buckets[i], newTable)
+		copied := copyBucketOf(&table.buckets[i], newTable, m.hasher)
 		newTable.addSizePlain(uint64(i), copied)
 	}
 	// Publish the new table and wake up all waiters.
@@ -273,11 +300,32 @@ func (m *MapOf[V]) resize(table *mapTable, hint mapResizeHint) {
 	m.resizeMu.Unlock()
 }
 
+func copyBucketOf[K comparable](b *bucketPadded, destTable *mapTable, hasher func(K) uint64) (copied int) {
+	rootb := b
+	lockBucket(&rootb.topHashMutex)
+	for {
+		for i := 0; i < entriesPerMapBucket; i++ {
+			if b.keys[i] != nil {
+				hash := hasher(derefTypedKey[K](b.keys[i]))
+				bidx := bucketIdx(destTable, hash)
+				destb := &destTable.buckets[bidx]
+				appendToBucket(hash, b.keys[i], b.values[i], destb)
+				copied++
+			}
+		}
+		if b.next == nil {
+			unlockBucket(&rootb.topHashMutex)
+			return
+		}
+		b = (*bucketPadded)(b.next)
+	}
+}
+
 // LoadAndDelete deletes the value for a key, returning the previous
 // value if any. The loaded result reports whether the key was
 // present.
-func (m *MapOf[V]) LoadAndDelete(key string) (value V, loaded bool) {
-	hash := maphash64(key)
+func (m *MapOf[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
+	hash := m.hasher(key)
 	for {
 		hintNonEmpty := 0
 		table := (*mapTable)(atomic.LoadPointer(&m.table))
@@ -303,7 +351,7 @@ func (m *MapOf[V]) LoadAndDelete(key string) (value V, loaded bool) {
 				if kp == nil || !topHashMatch(hash, topHashes, i) {
 					continue
 				}
-				if key == derefKey(kp) {
+				if key == derefTypedKey[K](kp) {
 					vp := b.values[i]
 					// Deletion case. First we update the value, then the key.
 					// This is important for atomic snapshot states.
@@ -334,7 +382,7 @@ func (m *MapOf[V]) LoadAndDelete(key string) (value V, loaded bool) {
 }
 
 // Delete deletes the value for a key.
-func (m *MapOf[V]) Delete(key string) {
+func (m *MapOf[K, V]) Delete(key K) {
 	m.LoadAndDelete(key)
 }
 
@@ -350,7 +398,7 @@ func (m *MapOf[V]) Delete(key string) {
 // It is safe to modify the map while iterating it. However, the
 // concurrent modification rule apply, i.e. the changes may be not
 // reflected in the subsequently iterated entries.
-func (m *MapOf[V]) Range(f func(key string, value V) bool) {
+func (m *MapOf[K, V]) Range(f func(key K, value V) bool) {
 	var bentries [entriesPerMapBucket]rangeEntry
 	tablep := atomic.LoadPointer(&m.table)
 	table := *(*mapTable)(tablep)
@@ -359,7 +407,7 @@ func (m *MapOf[V]) Range(f func(key string, value V) bool) {
 		for {
 			n := copyRangeEntries(b, &bentries)
 			for j := 0; j < n; j++ {
-				k := derefKey(bentries[j].key)
+				k := derefTypedKey[K](bentries[j].key)
 				v := derefTypedValue[V](bentries[j].value)
 				if !f(k, v) {
 					return
@@ -375,17 +423,21 @@ func (m *MapOf[V]) Range(f func(key string, value V) bool) {
 }
 
 // Size returns current size of the map.
-func (m *MapOf[V]) Size() int {
+func (m *MapOf[K, V]) Size() int {
 	table := (*mapTable)(atomic.LoadPointer(&m.table))
 	return int(table.sumSize())
 }
 
+func derefTypedKey[K comparable](keyPtr unsafe.Pointer) (key K) {
+	return *(*K)(keyPtr)
+}
+
 func derefTypedValue[V any](valuePtr unsafe.Pointer) (val V) {
 	return (*(*interface{})(valuePtr)).(V)
 }
 
 // O(N) operation; use for debug purposes only
-func (m *MapOf[V]) stats() mapStats {
+func (m *MapOf[K, V]) stats() mapStats {
 	stats := mapStats{
 		TotalGrowths: atomic.LoadInt64(&m.totalGrowths),
 		TotalShrinks: atomic.LoadInt64(&m.totalShrinks),
diff --git a/rbmutex.go b/rbmutex.go
index 4d5350c..32beb73 100644
--- a/rbmutex.go
+++ b/rbmutex.go
@@ -59,7 +59,7 @@ func (m *RBMutex) RLock() *RToken {
 		if !ok {
 			t = new(RToken)
 			// Since rslots is a power of two, we can use & instead of %.
-			t.slot = uint32(hash64(uintptr(unsafe.Pointer(t))) & (rslots - 1))
+			t.slot = uint32(mixhash64ptr(uintptr(unsafe.Pointer(t))) & (rslots - 1))
 		}
 		if atomic.CompareAndSwapInt32(&m.readers[t.slot], 0, 1) {
 			if atomic.LoadInt32(&m.rbias) == 1 {
diff --git a/util.go b/util.go
index 3145262..5318f57 100644
--- a/util.go
+++ b/util.go
@@ -19,7 +19,7 @@ const (
 )
 
 // murmurhash3 64-bit finalizer
-func hash64(v uintptr) uint64 {
+func mixhash64ptr(v uintptr) uint64 {
 	x := uint64(v)
 	x = ((x >> 33) ^ x) * 0xff51afd7ed558ccd
 	x = ((x >> 33) ^ x) * 0xc4ceb9fe1a85ec53
@@ -27,6 +27,16 @@ func hash64(v uintptr) uint64 {
 	return x
 }
 
+// murmurhash3 64-bit finalizer
+//
+//lint:ignore U1000 used in MapOf
+func mixhash64(v uint64) uint64 {
+	v = ((v >> 33) ^ v) * 0xff51afd7ed558ccd
+	v = ((v >> 33) ^ v) * 0xc4ceb9fe1a85ec53
+	v = (v >> 33) ^ v
+	return v
+}
+
 // exposes the built-in memhash function
 func maphash64(s string) uint64 {
 	if s == "" {


Test patch:
diff --git a/export_mapof_test.go b/export_mapof_test.go
index e8d191c..952ff77 100644
--- a/export_mapof_test.go
+++ b/export_mapof_test.go
@@ -3,6 +3,6 @@
 
 package xsync
 
-func CollectMapOfStats[V any](m *MapOf[V]) MapStats {
+func CollectMapOfStats[K comparable, V any](m *MapOf[K, V]) MapStats {
 	return MapStats{m.stats()}
 }
diff --git a/mapof_test.go b/mapof_test.go
index 029e127..e965099 100644
--- a/mapof_test.go
+++ b/mapof_test.go
@@ -7,6 +7,7 @@ import (
 	"math"
 	"math/rand"
 	"strconv"
+	"sync"
 	"testing"
 	"time"
 
@@ -253,6 +254,93 @@ func TestMapOfSerialStore(t *testing.T) {
 	}
 }
 
+func TestIntegerMapOfSerialStore(t *testing.T) {
+	const numEntries = 128
+	m := NewIntegerMapOf[int, int]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(i, i)
+	}
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(i)
+		if !ok {
+			t.Errorf("value not found for %d", i)
+		}
+		if v != i {
+			t.Errorf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
+func TestTypedMapOfSerialStore_StructKeys_IntValues(t *testing.T) {
+	type foo struct {
+		x int
+		y int
+	}
+	const numEntries = 128
+	m := NewTypedMapOf[foo, int](func(f foo) uint64 {
+		return uint64(31*f.x + f.y)
+	})
+	for i := 0; i < numEntries; i++ {
+		m.Store(foo{i, -i}, i)
+	}
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(foo{i, -i})
+		if !ok {
+			t.Errorf("value not found for %d", i)
+		}
+		if v != i {
+			t.Errorf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
+func TestTypedMapOfSerialStore_StructKeys_StructValues(t *testing.T) {
+	type foo struct {
+		x int
+		y int
+	}
+	const numEntries = 128
+	m := NewTypedMapOf[foo, foo](func(f foo) uint64 {
+		return uint64(31*f.x + f.y)
+	})
+	for i := 0; i < numEntries; i++ {
+		m.Store(foo{i, -i}, foo{-i, i})
+	}
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(foo{i, -i})
+		if !ok {
+			t.Errorf("value not found for %d", i)
+		}
+		if v.x != -i {
+			t.Errorf("x value does not match for %d: %v", i, v)
+		}
+		if v.y != i {
+			t.Errorf("y value does not match for %d: %v", i, v)
+		}
+	}
+}
+
+func TestTypedMapOfSerialStore_HashCodeCollisions(t *testing.T) {
+	const numEntries = 1000
+	m := NewTypedMapOf[int, int](func(i int) uint64 {
+		// We intentionally use an awful hash function here to make sure
+		// that the map copes with key collisions.
+		return 42
+	})
+	for i := 0; i < numEntries; i++ {
+		m.Store(i, i)
+	}
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(i)
+		if !ok {
+			t.Errorf("value not found for %d", i)
+		}
+		if v != i {
+			t.Errorf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
 func TestMapOfSerialLoadOrStore(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
@@ -280,6 +368,40 @@ func TestMapOfSerialStoreThenDelete(t *testing.T) {
 	}
 }
 
+func TestIntegerMapOfSerialStoreThenDelete(t *testing.T) {
+	const numEntries = 1000
+	m := NewIntegerMapOf[int32, int32]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(int32(i), int32(i))
+	}
+	for i := 0; i < numEntries; i++ {
+		m.Delete(int32(i))
+		if _, ok := m.Load(int32(i)); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
+func TestTypedMapOfSerialStoreThenDelete(t *testing.T) {
+	type foo struct {
+		x int
+		y int
+	}
+	const numEntries = 1000
+	m := NewTypedMapOf[foo, string](func(f foo) uint64 {
+		return uint64(31*f.x + f.y)
+	})
+	for i := 0; i < numEntries; i++ {
+		m.Store(foo{i, 42}, strconv.Itoa(i))
+	}
+	for i := 0; i < numEntries; i++ {
+		m.Delete(foo{i, 42})
+		if _, ok := m.Load(foo{i, 42}); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
 func TestMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
@@ -296,6 +418,44 @@ func TestMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
 	}
 }
 
+func TestIntegerMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
+	const numEntries = 1000
+	m := NewIntegerMapOf[int, int]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(i, i)
+	}
+	for i := 0; i < numEntries; i++ {
+		if _, loaded := m.LoadAndDelete(i); !loaded {
+			t.Errorf("value was not found for %d", i)
+		}
+		if _, ok := m.Load(i); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
+func TestTypedMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
+	type foo struct {
+		x int
+		y int
+	}
+	const numEntries = 1000
+	m := NewTypedMapOf[foo, int](func(f foo) uint64 {
+		return uint64(31*f.x + f.y)
+	})
+	for i := 0; i < numEntries; i++ {
+		m.Store(foo{42, i}, i)
+	}
+	for i := 0; i < numEntries; i++ {
+		if _, loaded := m.LoadAndDelete(foo{42, i}); !loaded {
+			t.Errorf("value was not found for %d", i)
+		}
+		if _, ok := m.Load(foo{42, i}); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
 func TestMapOfSize(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
@@ -386,7 +546,7 @@ func TestMapOfResize_CounterLenLimit(t *testing.T) {
 	}
 }
 
-func parallelSeqTypedStorer(t *testing.T, m *MapOf[int], storeEach, numIters, numEntries int, cdone chan bool) {
+func parallelSeqTypedStorer(t *testing.T, m *MapOf[string, int], storeEach, numIters, numEntries int, cdone chan bool) {
 	for i := 0; i < numIters; i++ {
 		for j := 0; j < numEntries; j++ {
 			if storeEach == 0 || j%storeEach == 0 {
@@ -432,7 +592,7 @@ func TestMapOfParallelStores(t *testing.T) {
 	}
 }
 
-func parallelRandTypedStorer(t *testing.T, m *MapOf[int], numIters, numEntries int, cdone chan bool) {
+func parallelRandTypedStorer(t *testing.T, m *MapOf[string, int], numIters, numEntries int, cdone chan bool) {
 	r := rand.New(rand.NewSource(time.Now().UnixNano()))
 	for i := 0; i < numIters; i++ {
 		j := r.Intn(numEntries)
@@ -445,7 +605,7 @@ func parallelRandTypedStorer(t *testing.T, m *MapOf[int], numIters, numEntries i
 	cdone <- true
 }
 
-func parallelRandTypedDeleter(t *testing.T, m *MapOf[int], numIters, numEntries int, cdone chan bool) {
+func parallelRandTypedDeleter(t *testing.T, m *MapOf[string, int], numIters, numEntries int, cdone chan bool) {
 	r := rand.New(rand.NewSource(time.Now().UnixNano()))
 	for i := 0; i < numIters; i++ {
 		j := r.Intn(numEntries)
@@ -458,7 +618,7 @@ func parallelRandTypedDeleter(t *testing.T, m *MapOf[int], numIters, numEntries
 	cdone <- true
 }
 
-func parallelTypedLoader(t *testing.T, m *MapOf[int], numIters, numEntries int, cdone chan bool) {
+func parallelTypedLoader(t *testing.T, m *MapOf[string, int], numIters, numEntries int, cdone chan bool) {
 	for i := 0; i < numIters; i++ {
 		for j := 0; j < numEntries; j++ {
 			// Due to atomic snapshots we must either see no entry, or a "<j>"/j pair.
@@ -512,7 +672,7 @@ func BenchmarkMapOf_NoWarmUp(b *testing.B) {
 		}
 		b.Run(bc.name, func(b *testing.B) {
 			m := NewMapOf[int]()
-			benchmarkMapInt(b, func(k string) (int, bool) {
+			benchmarkMapOfStringKeys(b, func(k string) (int, bool) {
 				return m.Load(k)
 			}, func(k string, v int) {
 				m.Store(k, v)
@@ -530,7 +690,7 @@ func BenchmarkMapOf_WarmUp(b *testing.B) {
 			for i := 0; i < benchmarkNumEntries; i++ {
 				m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i)
 			}
-			benchmarkMapInt(b, func(k string) (int, bool) {
+			benchmarkMapOfStringKeys(b, func(k string) (int, bool) {
 				return m.Load(k)
 			}, func(k string, v int) {
 				m.Store(k, v)
@@ -541,7 +701,7 @@ func BenchmarkMapOf_WarmUp(b *testing.B) {
 	}
 }
 
-func benchmarkMapInt(
+func benchmarkMapOfStringKeys(
 	b *testing.B,
 	loadFn func(k string) (int, bool),
 	storeFn func(k string, v int),
@@ -568,6 +728,119 @@ func benchmarkMapInt(
 	})
 }
 
+func BenchmarkIntegerMapOf_NoWarmUp(b *testing.B) {
+	for _, bc := range benchmarkCases {
+		if bc.readPercentage == 100 {
+			// This benchmark doesn't make sense without a warm-up.
+			continue
+		}
+		b.Run(bc.name, func(b *testing.B) {
+			m := NewIntegerMapOf[int, int]()
+			benchmarkMapOfIntegerKeys(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 BenchmarkIntegerMapOf_WarmUp(b *testing.B) {
+	for _, bc := range benchmarkCases {
+		b.Run(bc.name, func(b *testing.B) {
+			m := NewIntegerMapOf[int, int]()
+			for i := 0; i < benchmarkNumEntries; i++ {
+				m.Store(i, i)
+			}
+			benchmarkMapOfIntegerKeys(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 BenchmarkIntegerMapStandard_NoWarmUp(b *testing.B) {
+	for _, bc := range benchmarkCases {
+		if bc.readPercentage == 100 {
+			// This benchmark doesn't make sense without a warm-up.
+			continue
+		}
+		b.Run(bc.name, func(b *testing.B) {
+			var m sync.Map
+			benchmarkMapOfIntegerKeys(b, func(k int) (value int, ok bool) {
+				v, ok := m.Load(k)
+				if ok {
+					return v.(int), ok
+				} else {
+					return 0, false
+				}
+			}, func(k int, v int) {
+				m.Store(k, v)
+			}, func(k int) {
+				m.Delete(k)
+			}, bc.readPercentage)
+		})
+	}
+}
+
+// This is a nice scenario for sync.Map since a lot of updates
+// will hit the readOnly part of the map.
+func BenchmarkIntegerMapStandard_WarmUp(b *testing.B) {
+	for _, bc := range benchmarkCases {
+		b.Run(bc.name, func(b *testing.B) {
+			var m sync.Map
+			for i := 0; i < benchmarkNumEntries; i++ {
+				m.Store(i, i)
+			}
+			benchmarkMapOfIntegerKeys(b, func(k int) (value int, ok bool) {
+				v, ok := m.Load(k)
+				if ok {
+					return v.(int), ok
+				} else {
+					return 0, false
+				}
+			}, func(k int, v int) {
+				m.Store(k, v)
+			}, func(k int) {
+				m.Delete(k)
+			}, bc.readPercentage)
+		})
+	}
+}
+
+func benchmarkMapOfIntegerKeys(
+	b *testing.B,
+	loadFn func(k int) (int, bool),
+	storeFn func(k int, v int),
+	deleteFn func(k int),
+	readPercentage int,
+) {
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		r := rand.New(rand.NewSource(time.Now().UnixNano()))
+		// convert percent to permille to support 99% case
+		storeThreshold := 10 * readPercentage
+		deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2)
+		for pb.Next() {
+			op := r.Intn(1000)
+			i := r.Intn(benchmarkNumEntries)
+			if op >= deleteThreshold {
+				deleteFn(i)
+			} else if op >= storeThreshold {
+				storeFn(i, i)
+			} else {
+				loadFn(i)
+			}
+		}
+	})
+}
+
 func BenchmarkMapOfRange(b *testing.B) {
 	m := NewMapOf[int]()
 	for i := 0; i < benchmarkNumEntries; i++ {