Repository: puzpuzpuz/xsync
Pull request #69 (2022-10-29 13:33:05)

Add Compute method to Map
A follow-up of #68

codecov-commenter: # [Codecov](https://codecov.io/gh/puzpuzpuz/xsync/pull/69?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov) Report
Base: **96.80**% // Head: **97.03**% // Increases project coverage by **`+0.22%`** :tada:
> Coverage data is based on head [(`5e00787`)](https://codecov.io/gh/puzpuzpuz/xsync/pull/69?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov) compared to base [(`f7778ac`)](https://codecov.io/gh/puzpuzpuz/xsync/commit/f7778acc27ed6aeeb3c6ca6c52fe8e487f4099be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov).
> Patch coverage: 100.00% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
+ Coverage   96.80%   97.03%   +0.22%     
==========================================
  Files           6        6              
  Lines         909      912       +3     
==========================================
+ Hits          880      885       +5     
+ Misses         24       22       -2     
  Partials        5        5              
```


| [Impacted Files](https://codecov.io/gh/puzpuzpuz/xsync/pull/69?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov) | Coverage Δ | |
|---|---|---|
| [map.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov#diff-bWFwLmdv) | `97.01% <100.00%> (+0.22%)` | :arrow_up: |
| [mapof.go](https://codecov.io/gh/puzpuzpuz/xsync/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+Pechkurov#diff-bWFwb2YuZ28=) | `96.38% <100.00%> (+0.33%)` | :arrow_up: |

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=Andrei+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=Andrei+Pechkurov)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/puzpuzpuz/xsync/pull/69?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrei+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=Andrei+Pechkurov).


Patch:
diff --git a/map.go b/map.go
index 04ac7ea..005e006 100644
--- a/map.go
+++ b/map.go
@@ -181,12 +181,13 @@ func (m *Map) Load(key string) (value interface{}, ok bool) {
 
 // Store sets the value for a key.
 func (m *Map) Store(key string, value interface{}) {
-	m.doStore(
+	m.doCompute(
 		key,
-		func() interface{} {
-			return value
+		func(interface{}, bool) (interface{}, bool) {
+			return value, false
 		},
 		false,
+		false,
 	)
 }
 
@@ -194,12 +195,13 @@ func (m *Map) Store(key string, value interface{}) {
 // Otherwise, it stores and returns the given value.
 // The loaded result is true if the value was loaded, false if stored.
 func (m *Map) LoadOrStore(key string, value interface{}) (actual interface{}, loaded bool) {
-	return m.doStore(
+	return m.doCompute(
 		key,
-		func() interface{} {
-			return value
+		func(interface{}, bool) (interface{}, bool) {
+			return value, false
 		},
 		true,
+		false,
 	)
 }
 
@@ -209,12 +211,13 @@ func (m *Map) LoadOrStore(key string, value interface{}) (actual interface{}, lo
 // The loaded result is true if the existing value was loaded,
 // false otherwise.
 func (m *Map) LoadAndStore(key string, value interface{}) (actual interface{}, loaded bool) {
-	return m.doStore(
+	return m.doCompute(
 		key,
-		func() interface{} {
-			return value
+		func(interface{}, bool) (interface{}, bool) {
+			return value, false
 		},
 		false,
+		false,
 	)
 }
 
@@ -223,23 +226,75 @@ func (m *Map) LoadAndStore(key string, value interface{}) (actual interface{}, l
 // returns the computed value. The loaded result is true if the value
 // was loaded, false if stored.
 func (m *Map) LoadOrCompute(key string, valueFn func() interface{}) (actual interface{}, loaded bool) {
-	return m.doStore(key, valueFn, true)
+	return m.doCompute(
+		key,
+		func(interface{}, bool) (interface{}, bool) {
+			return valueFn(), false
+		},
+		true,
+		false,
+	)
+}
+
+// Compute either sets the computed new value for the key or deletes
+// the value for the key. When the delete result of the valueFn function
+// is set to true, the value will be deleted, if it exists. When delete
+// is set to false, the value is updated to the newValue.
+// The ok result indicates whether value was computed and stored, thus, is
+// present in the map. The actual result contains the new value in cases where
+// the value was computed and stored. See the example for a few use cases.
+func (m *Map) Compute(
+	key string,
+	valueFn func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool),
+) (actual interface{}, ok bool) {
+	return m.doCompute(key, valueFn, false, true)
+}
+
+// LoadAndDelete deletes the value for a key, returning the previous
+// value if any. The loaded result reports whether the key was
+// present.
+func (m *Map) LoadAndDelete(key string) (value interface{}, loaded bool) {
+	return m.doCompute(
+		key,
+		func(value interface{}, loaded bool) (interface{}, bool) {
+			return value, true
+		},
+		false,
+		false,
+	)
+}
+
+// Delete deletes the value for a key.
+func (m *Map) Delete(key string) {
+	m.doCompute(
+		key,
+		func(value interface{}, loaded bool) (interface{}, bool) {
+			return value, true
+		},
+		false,
+		false,
+	)
 }
 
-func (m *Map) doStore(key string, valueFn func() interface{}, loadIfExists bool) (interface{}, bool) {
+func (m *Map) doCompute(
+	key string,
+	valueFn func(oldValue interface{}, loaded bool) (interface{}, bool),
+	loadIfExists, computeOnly bool,
+) (interface{}, bool) {
 	// Read-only path.
 	if loadIfExists {
 		if v, ok := m.Load(key); ok {
-			return v, true
+			return v, !computeOnly
 		}
 	}
 	// Write path.
 	for {
-	store_attempt:
+	compute_attempt:
 		var (
 			emptyb   *bucketPadded
 			emptyidx int
 		)
+		hintNonEmpty := 0
 		table := (*mapTable)(atomic.LoadPointer(&m.table))
 		tableLen := len(table.buckets)
 		hash := hashString(table.seed, key)
@@ -250,13 +305,13 @@ func (m *Map) doStore(key string, valueFn func() interface{}, loadIfExists bool)
 		if m.newerTableExists(table) {
 			// Someone resized the table. Go for another attempt.
 			unlockBucket(&rootb.topHashMutex)
-			goto store_attempt
+			goto compute_attempt
 		}
 		if m.resizeInProgress() {
 			// Resize is in progress. Wait, then go for another attempt.
 			unlockBucket(&rootb.topHashMutex)
 			m.waitForResize()
-			goto store_attempt
+			goto compute_attempt
 		}
 		for {
 			topHashes := atomic.LoadUint64(&b.topHashMutex)
@@ -275,53 +330,91 @@ func (m *Map) doStore(key string, valueFn func() interface{}, loadIfExists bool)
 					vp := b.values[i]
 					if loadIfExists {
 						unlockBucket(&rootb.topHashMutex)
-						return derefValue(vp), true
+						return derefValue(vp), !computeOnly
+					}
+					// In-place update/delete.
+					// We get a copy of the value via an interface{} on each call,
+					// thus the live value pointers are unique. Otherwise atomic
+					// snapshot won't be correct in case of multiple Store calls
+					// using the same value.
+					oldValue := derefValue(vp)
+					newValue, del := valueFn(oldValue, true)
+					if del {
+						// Deletion.
+						// First we update the value, then the key.
+						// This is important for atomic snapshot states.
+						atomic.StoreUint64(&b.topHashMutex, eraseTopHash(topHashes, i))
+						atomic.StorePointer(&b.values[i], nil)
+						atomic.StorePointer(&b.keys[i], nil)
+						leftEmpty := false
+						if hintNonEmpty == 0 {
+							leftEmpty = isEmpty(b)
+						}
+						unlockBucket(&rootb.topHashMutex)
+						table.addSize(bidx, -1)
+						// Might need to shrink the table.
+						if leftEmpty {
+							m.resize(table, mapShrinkHint)
+						}
+						return oldValue, !computeOnly
 					}
-					// In-place update case. Luckily we get a copy of the value
-					// interface{} on each call, thus the live value pointers are
-					// unique. Otherwise atomic snapshot won't be correct in case
-					// of multiple Store calls using the same value.
-					value := valueFn()
-					nvp := unsafe.Pointer(&value)
+					nvp := unsafe.Pointer(&newValue)
 					if assertionsEnabled && vp == nvp {
 						panic("non-unique value pointer")
 					}
 					atomic.StorePointer(&b.values[i], nvp)
 					unlockBucket(&rootb.topHashMutex)
+					if computeOnly {
+						// Compute expects the new value to be returned.
+						return newValue, true
+					}
 					// LoadAndStore expects the old value to be returned.
-					return derefValue(vp), true
+					return oldValue, true
 				}
+				hintNonEmpty++
 			}
 			if b.next == nil {
 				if emptyb != nil {
-					// Insertion case. First we update the value, then the key.
+					// Insertion into an existing bucket.
+					var zeroedV interface{}
+					newValue, del := valueFn(zeroedV, false)
+					if del {
+						unlockBucket(&rootb.topHashMutex)
+						return zeroedV, false
+					}
+					// First we update the value, then the key.
 					// This is important for atomic snapshot states.
 					topHashes = atomic.LoadUint64(&emptyb.topHashMutex)
 					atomic.StoreUint64(&emptyb.topHashMutex, storeTopHash(hash, topHashes, emptyidx))
-					value := valueFn()
-					atomic.StorePointer(&emptyb.values[emptyidx], unsafe.Pointer(&value))
+					atomic.StorePointer(&emptyb.values[emptyidx], unsafe.Pointer(&newValue))
 					atomic.StorePointer(&emptyb.keys[emptyidx], unsafe.Pointer(&key))
 					unlockBucket(&rootb.topHashMutex)
 					table.addSize(bidx, 1)
-					return value, false
+					return newValue, computeOnly
 				}
 				growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor
 				if table.sumSize() > int64(growThreshold) {
 					// Need to grow the table. Then go for another attempt.
 					unlockBucket(&rootb.topHashMutex)
 					m.resize(table, mapGrowHint)
-					goto store_attempt
+					goto compute_attempt
 				}
-				// Create and append a new bucket.
+				// Insertion into a new bucket.
+				var zeroedV interface{}
+				newValue, del := valueFn(zeroedV, false)
+				if del {
+					unlockBucket(&rootb.topHashMutex)
+					return newValue, false
+				}
+				// Create and append the bucket.
 				newb := new(bucketPadded)
 				newb.keys[0] = unsafe.Pointer(&key)
-				value := valueFn()
-				newb.values[0] = unsafe.Pointer(&value)
+				newb.values[0] = unsafe.Pointer(&newValue)
 				newb.topHashMutex = storeTopHash(hash, newb.topHashMutex, 0)
 				atomic.StorePointer(&b.next, unsafe.Pointer(newb))
 				unlockBucket(&rootb.topHashMutex)
 				table.addSize(bidx, 1)
-				return value, false
+				return newValue, computeOnly
 			}
 			b = (*bucketPadded)(b.next)
 		}
@@ -444,71 +537,6 @@ func appendToBucket(hash uint64, keyPtr, valPtr unsafe.Pointer, b *bucketPadded)
 	}
 }
 
-// LoadAndDelete deletes the value for a key, returning the previous
-// value if any. The loaded result reports whether the key was
-// present.
-func (m *Map) LoadAndDelete(key string) (value interface{}, loaded bool) {
-	for {
-		hintNonEmpty := 0
-		table := (*mapTable)(atomic.LoadPointer(&m.table))
-		hash := hashString(table.seed, key)
-		bidx := bucketIdx(table, hash)
-		rootb := &table.buckets[bidx]
-		b := rootb
-		lockBucket(&rootb.topHashMutex)
-		if m.newerTableExists(table) {
-			// Someone resized the table. Go for another attempt.
-			unlockBucket(&rootb.topHashMutex)
-			continue
-		}
-		if m.resizeInProgress() {
-			// Resize is in progress. Wait, then go for another attempt.
-			unlockBucket(&rootb.topHashMutex)
-			m.waitForResize()
-			continue
-		}
-		for {
-			topHashes := atomic.LoadUint64(&b.topHashMutex)
-			for i := 0; i < entriesPerMapBucket; i++ {
-				kp := b.keys[i]
-				if kp == nil || !topHashMatch(hash, topHashes, i) {
-					continue
-				}
-				if key == derefKey(kp) {
-					vp := b.values[i]
-					// Deletion case. First we update the value, then the key.
-					// This is important for atomic snapshot states.
-					atomic.StoreUint64(&b.topHashMutex, eraseTopHash(topHashes, i))
-					atomic.StorePointer(&b.values[i], nil)
-					atomic.StorePointer(&b.keys[i], nil)
-					leftEmpty := false
-					if hintNonEmpty == 0 {
-						leftEmpty = isEmpty(b)
-					}
-					unlockBucket(&rootb.topHashMutex)
-					table.addSize(bidx, -1)
-					// Might need to shrink the table.
-					if leftEmpty {
-						m.resize(table, mapShrinkHint)
-					}
-					return derefValue(vp), true
-				}
-				hintNonEmpty++
-			}
-			if b.next == nil {
-				unlockBucket(&rootb.topHashMutex)
-				return
-			}
-			b = (*bucketPadded)(b.next)
-		}
-	}
-}
-
-// Delete deletes the value for a key.
-func (m *Map) Delete(key string) {
-	m.LoadAndDelete(key)
-}
-
 func isEmpty(rootb *bucketPadded) bool {
 	b := rootb
 	for {
diff --git a/mapof.go b/mapof.go
index 025df82..ef73400 100644
--- a/mapof.go
+++ b/mapof.go
@@ -110,7 +110,7 @@ func (m *MapOf[K, V]) Load(key K) (value V, ok bool) {
 
 // Store sets the value for a key.
 func (m *MapOf[K, V]) Store(key K, value V) {
-	m.doStore(
+	m.doCompute(
 		key,
 		func(V, bool) (V, bool) {
 			return value, false
@@ -124,7 +124,7 @@ func (m *MapOf[K, V]) Store(key K, value V) {
 // Otherwise, it stores and returns the given value.
 // The loaded result is true if the value was loaded, false if stored.
 func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
-	return m.doStore(
+	return m.doCompute(
 		key,
 		func(V, bool) (V, bool) {
 			return value, false
@@ -140,7 +140,7 @@ func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
 // The loaded result is true if the existing value was loaded,
 // false otherwise.
 func (m *MapOf[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) {
-	return m.doStore(
+	return m.doCompute(
 		key,
 		func(V, bool) (V, bool) {
 			return value, false
@@ -155,7 +155,7 @@ func (m *MapOf[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) {
 // returns the computed value. The loaded result is true if the value
 // was loaded, false if stored.
 func (m *MapOf[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded bool) {
-	return m.doStore(
+	return m.doCompute(
 		key,
 		func(V, bool) (V, bool) {
 			return valueFn(), false
@@ -165,16 +165,60 @@ func (m *MapOf[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded b
 	)
 }
 
-func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, bool), loadIfExists, computedOk bool) (V, bool) {
+// Compute either sets the computed new value for the key or deletes
+// the value for the key. When the delete result of the valueFn function
+// is set to true, the value will be deleted, if it exists. When delete
+// is set to false, the value is updated to the newValue.
+// The ok result indicates whether value was computed and stored, thus, is
+// present in the map. The actual result contains the new value in cases where
+// the value was computed and stored. See the example for a few use cases.
+func (m *MapOf[K, V]) Compute(
+	key K,
+	valueFn func(oldValue V, loaded bool) (newValue V, delete bool),
+) (actual V, ok bool) {
+	return m.doCompute(key, valueFn, false, true)
+}
+
+// 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[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
+	return m.doCompute(
+		key,
+		func(value V, loaded bool) (V, bool) {
+			return value, true
+		},
+		false,
+		false,
+	)
+}
+
+// Delete deletes the value for a key.
+func (m *MapOf[K, V]) Delete(key K) {
+	m.doCompute(
+		key,
+		func(value V, loaded bool) (V, bool) {
+			return value, true
+		},
+		false,
+		false,
+	)
+}
+
+func (m *MapOf[K, V]) doCompute(
+	key K,
+	valueFn func(oldValue V, loaded bool) (V, bool),
+	loadIfExists, computeOnly bool,
+) (V, bool) {
 	// Read-only path.
 	if loadIfExists {
 		if v, ok := m.Load(key); ok {
-			return v, !computedOk
+			return v, !computeOnly
 		}
 	}
 	// Write path.
 	for {
-	store_attempt:
+	compute_attempt:
 		var (
 			emptyb   *bucketPadded
 			emptyidx int
@@ -190,13 +234,13 @@ func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, b
 		if m.newerTableExists(table) {
 			// Someone resized the table. Go for another attempt.
 			unlockBucket(&rootb.topHashMutex)
-			goto store_attempt
+			goto compute_attempt
 		}
 		if m.resizeInProgress() {
 			// Resize is in progress. Wait, then go for another attempt.
 			unlockBucket(&rootb.topHashMutex)
 			m.waitForResize()
-			goto store_attempt
+			goto compute_attempt
 		}
 		for {
 			topHashes := atomic.LoadUint64(&b.topHashMutex)
@@ -215,15 +259,18 @@ func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, b
 					vp := b.values[i]
 					if loadIfExists {
 						unlockBucket(&rootb.topHashMutex)
-						return derefTypedValue[V](vp), !computedOk
+						return derefTypedValue[V](vp), !computeOnly
 					}
-					// In-place update case. We get a copy of the value via an
-					// interface{} on each call, thus the live value pointers are
-					// unique. Otherwise atomic snapshot won't be correct in case
-					// of multiple Store calls using the same value.
-					value, del := valueFn(derefTypedValue[V](vp), true)
+					// In-place update/delete.
+					// We get a copy of the value via an interface{} on each call,
+					// thus the live value pointers are unique. Otherwise atomic
+					// snapshot won't be correct in case of multiple Store calls
+					// using the same value.
+					oldValue := derefTypedValue[V](vp)
+					newValue, del := valueFn(oldValue, true)
 					if del {
-						// Deletion case. First we update the value, then the key.
+						// Deletion.
+						// First we update the value, then the key.
 						// This is important for atomic snapshot states.
 						atomic.StoreUint64(&b.topHashMutex, eraseTopHash(topHashes, i))
 						atomic.StorePointer(&b.values[i], nil)
@@ -238,36 +285,35 @@ func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, b
 						if leftEmpty {
 							m.resize(table, mapShrinkHint)
 						}
-						if computedOk {
-							return value, false
-						}
-						return derefTypedValue[V](vp), true
+						return oldValue, !computeOnly
 					}
-					var wv interface{} = value
+					var wv interface{} = newValue
 					nvp := unsafe.Pointer(&wv)
 					if assertionsEnabled && vp == nvp {
 						panic("non-unique value pointer")
 					}
 					atomic.StorePointer(&b.values[i], nvp)
 					unlockBucket(&rootb.topHashMutex)
-					// LoadAndStore expects the old value to be returned.
-					if computedOk {
-						return value, true
+					if computeOnly {
+						// Compute expects the new value to be returned.
+						return newValue, true
 					}
-					return derefTypedValue[V](vp), true
+					// LoadAndStore expects the old value to be returned.
+					return oldValue, true
 				}
 				hintNonEmpty++
 			}
 			if b.next == nil {
-				var zeroedV V
-				value, del := valueFn(zeroedV, false)
-				if del {
-					unlockBucket(&rootb.topHashMutex)
-					return value, false
-				}
-				var wv interface{} = value
 				if emptyb != nil {
-					// Insertion case. First we update the value, then the key.
+					// Insertion into an existing bucket.
+					var zeroedV V
+					newValue, del := valueFn(zeroedV, false)
+					if del {
+						unlockBucket(&rootb.topHashMutex)
+						return zeroedV, false
+					}
+					var wv interface{} = newValue
+					// First we update the value, then the key.
 					// This is important for atomic snapshot states.
 					topHashes = atomic.LoadUint64(&emptyb.topHashMutex)
 					atomic.StoreUint64(&emptyb.topHashMutex, storeTopHash(hash, topHashes, emptyidx))
@@ -275,16 +321,24 @@ func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, b
 					atomic.StorePointer(&emptyb.keys[emptyidx], unsafe.Pointer(&key))
 					unlockBucket(&rootb.topHashMutex)
 					table.addSize(bidx, 1)
-					return value, computedOk
+					return newValue, computeOnly
 				}
 				growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor
 				if table.sumSize() > int64(growThreshold) {
 					// Need to grow the table. Then go for another attempt.
 					unlockBucket(&rootb.topHashMutex)
 					m.resize(table, mapGrowHint)
-					goto store_attempt
+					goto compute_attempt
+				}
+				// Insertion into a new bucket.
+				var zeroedV V
+				newValue, del := valueFn(zeroedV, false)
+				if del {
+					unlockBucket(&rootb.topHashMutex)
+					return newValue, false
 				}
-				// Create and append a new bucket.
+				var wv interface{} = newValue
+				// Create and append the bucket.
 				newb := new(bucketPadded)
 				newb.keys[0] = unsafe.Pointer(&key)
 				newb.values[0] = unsafe.Pointer(&wv)
@@ -292,24 +346,13 @@ func (m *MapOf[K, V]) doStore(key K, valueFn func(oldValue V, loaded bool) (V, b
 				atomic.StorePointer(&b.next, unsafe.Pointer(newb))
 				unlockBucket(&rootb.topHashMutex)
 				table.addSize(bidx, 1)
-				return value, computedOk
+				return newValue, computeOnly
 			}
 			b = (*bucketPadded)(b.next)
 		}
 	}
 }
 
-// Compute either sets the computed new value for the key or deletes
-// the value for the key. When the delete result of the valueFn function
-// is set to true, the value will be deleted, if it exists. When delete
-// is set to false, the value is updated to the newValue.
-// The ok result indicates whether value was computed and stored, thus, is
-// present in the map. The actual result contains the new value in cases where
-// the value was computed and stored. See the example for a few use cases.
-func (m *MapOf[K, V]) Compute(key K, valueFn func(oldValue V, loaded bool) (newValue V, delete bool)) (actual V, ok bool) {
-	return m.doStore(key, valueFn, false, true)
-}
-
 func (m *MapOf[K, V]) newerTableExists(table *mapTable) bool {
 	curTablePtr := atomic.LoadPointer(&m.table)
 	return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table))
@@ -403,71 +446,6 @@ func copyBucketOf[K comparable](b *bucketPadded, destTable *mapTable, hasher fun
 	}
 }
 
-// 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[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
-	for {
-		hintNonEmpty := 0
-		table := (*mapTable)(atomic.LoadPointer(&m.table))
-		hash := m.hasher(table.seed, key)
-		bidx := bucketIdx(table, hash)
-		rootb := &table.buckets[bidx]
-		b := rootb
-		lockBucket(&rootb.topHashMutex)
-		if m.newerTableExists(table) {
-			// Someone resized the table. Go for another attempt.
-			unlockBucket(&rootb.topHashMutex)
-			continue
-		}
-		if m.resizeInProgress() {
-			// Resize is in progress. Wait, then go for another attempt.
-			unlockBucket(&rootb.topHashMutex)
-			m.waitForResize()
-			continue
-		}
-		for {
-			topHashes := atomic.LoadUint64(&b.topHashMutex)
-			for i := 0; i < entriesPerMapBucket; i++ {
-				kp := b.keys[i]
-				if kp == nil || !topHashMatch(hash, topHashes, i) {
-					continue
-				}
-				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.
-					atomic.StoreUint64(&b.topHashMutex, eraseTopHash(topHashes, i))
-					atomic.StorePointer(&b.values[i], nil)
-					atomic.StorePointer(&b.keys[i], nil)
-					leftEmpty := false
-					if hintNonEmpty == 0 {
-						leftEmpty = isEmpty(b)
-					}
-					unlockBucket(&rootb.topHashMutex)
-					table.addSize(bidx, -1)
-					// Might need to shrink the table.
-					if leftEmpty {
-						m.resize(table, mapShrinkHint)
-					}
-					return derefTypedValue[V](vp), true
-				}
-				hintNonEmpty++
-			}
-			if b.next == nil {
-				unlockBucket(&rootb.topHashMutex)
-				return
-			}
-			b = (*bucketPadded)(b.next)
-		}
-	}
-}
-
-// Delete deletes the value for a key.
-func (m *MapOf[K, V]) Delete(key K) {
-	m.LoadAndDelete(key)
-}
-
 // Range calls f sequentially for each key and value present in the
 // map. If f returns false, range stops the iteration.
 //


Test patch:
diff --git a/counter_test.go b/counter_test.go
index 01681b3..e65765b 100644
--- a/counter_test.go
+++ b/counter_test.go
@@ -12,7 +12,7 @@ func TestCounterInc(t *testing.T) {
 	var c Counter
 	for i := 0; i < 100; i++ {
 		if v := c.Value(); v != int64(i) {
-			t.Errorf("got %v, want %d", v, i)
+			t.Fatalf("got %v, want %d", v, i)
 		}
 		c.Inc()
 	}
@@ -22,7 +22,7 @@ func TestCounterDec(t *testing.T) {
 	var c Counter
 	for i := 0; i < 100; i++ {
 		if v := c.Value(); v != int64(-i) {
-			t.Errorf("got %v, want %d", v, -i)
+			t.Fatalf("got %v, want %d", v, -i)
 		}
 		c.Dec()
 	}
@@ -32,7 +32,7 @@ func TestCounterAdd(t *testing.T) {
 	var c Counter
 	for i := 0; i < 100; i++ {
 		if v := c.Value(); v != int64(i*42) {
-			t.Errorf("got %v, want %d", v, i*42)
+			t.Fatalf("got %v, want %d", v, i*42)
 		}
 		c.Add(42)
 	}
@@ -42,11 +42,11 @@ func TestCounterReset(t *testing.T) {
 	var c Counter
 	c.Add(42)
 	if v := c.Value(); v != 42 {
-		t.Errorf("got %v, want %d", v, 42)
+		t.Fatalf("got %v, want %d", v, 42)
 	}
 	c.Reset()
 	if v := c.Value(); v != 0 {
-		t.Errorf("got %v, want %d", v, 0)
+		t.Fatalf("got %v, want %d", v, 0)
 	}
 }
 
@@ -71,7 +71,7 @@ func doTestParallelIncrementors(t *testing.T, numModifiers, gomaxprocs int) {
 	}
 	expected := int64(numModifiers * numIncs)
 	if v := c.Value(); v != expected {
-		t.Errorf("got %d, want %d", v, expected)
+		t.Fatalf("got %d, want %d", v, expected)
 	}
 }
 
diff --git a/example_test.go b/example_test.go
index 59a21d8..b68d4de 100644
--- a/example_test.go
+++ b/example_test.go
@@ -58,7 +58,7 @@ func ExampleMapOf_Compute() {
 	// v: 84, ok: true
 	fmt.Printf("v: %v, ok: %v\n", v, ok)
 
-	// Compare existing value, and set new value or keep old value, and store the oldVal
+	// Set a new value or keep the old value conditionally.
 	var oldVal int
 	minVal := 63
 	v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) {
diff --git a/map_test.go b/map_test.go
index e586096..45c3090 100644
--- a/map_test.go
+++ b/map_test.go
@@ -44,7 +44,7 @@ func init() {
 func TestMap_BucketStructSize(t *testing.T) {
 	size := unsafe.Sizeof(BucketPadded{})
 	if size != 64 {
-		t.Errorf("size of 64B (one cache line) is expected, got: %d", size)
+		t.Fatalf("size of 64B (one cache line) is expected, got: %d", size)
 	}
 }
 
@@ -107,13 +107,13 @@ func TestMap_MissingEntry(t *testing.T) {
 	m := NewMap()
 	v, ok := m.Load("foo")
 	if ok {
-		t.Errorf("value was not expected: %v", v)
+		t.Fatalf("value was not expected: %v", v)
 	}
 	if deleted, loaded := m.LoadAndDelete("foo"); loaded {
-		t.Errorf("value was not expected %v", deleted)
+		t.Fatalf("value was not expected %v", deleted)
 	}
 	if actual, loaded := m.LoadOrStore("foo", "bar"); loaded {
-		t.Errorf("value was not expected %v", actual)
+		t.Fatalf("value was not expected %v", actual)
 	}
 }
 
@@ -125,7 +125,7 @@ func TestMap_EmptyStringKey(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if vs, ok := v.(string); ok && vs != "foobar" {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -137,7 +137,7 @@ func TestMapStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -149,7 +149,7 @@ func TestMapLoadOrStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -162,7 +162,7 @@ func TestMapLoadOrStore_NonNilValue(t *testing.T) {
 		t.Error("no value was expected")
 	}
 	if v != newv {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	newv2 := &foo{}
 	v, loaded = m.LoadOrStore("foo", newv2)
@@ -170,7 +170,7 @@ func TestMapLoadOrStore_NonNilValue(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if v != newv {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -182,14 +182,14 @@ func TestMapLoadAndStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 	v, loaded = m.Load("foo")
 	if !loaded {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -202,7 +202,7 @@ func TestMapLoadAndStore_NonNilValue(t *testing.T) {
 		t.Error("no value was expected")
 	}
 	if v != v1 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	v2 := 2
 	v, loaded = m.LoadAndStore("foo", v2)
@@ -210,14 +210,14 @@ func TestMapLoadAndStore_NonNilValue(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if v != v1 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	v, loaded = m.Load("foo")
 	if !loaded {
 		t.Error("value was expected")
 	}
 	if v != v2 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -231,7 +231,7 @@ func TestMapRange(t *testing.T) {
 	met := make(map[string]int)
 	m.Range(func(key string, value interface{}) bool {
 		if key != strconv.Itoa(value.(int)) {
-			t.Errorf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
+			t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
 			return false
 		}
 		met[key] += 1
@@ -239,11 +239,11 @@ func TestMapRange(t *testing.T) {
 		return true
 	})
 	if iters != numEntries {
-		t.Errorf("got unexpected number of iterations: %d", iters)
+		t.Fatalf("got unexpected number of iterations: %d", iters)
 	}
 	for i := 0; i < numEntries; i++ {
 		if c := met[strconv.Itoa(i)]; c != 1 {
-			t.Errorf("range did not iterate correctly over %d: %d", i, c)
+			t.Fatalf("range did not iterate correctly over %d: %d", i, c)
 		}
 	}
 }
@@ -259,7 +259,7 @@ func TestMapRange_FalseReturned(t *testing.T) {
 		return iters != 13
 	})
 	if iters != 13 {
-		t.Errorf("got unexpected number of iterations: %d", iters)
+		t.Fatalf("got unexpected number of iterations: %d", iters)
 	}
 }
 
@@ -275,12 +275,12 @@ func TestMapRange_NestedDelete(t *testing.T) {
 	})
 	for i := 0; i < numEntries; i++ {
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value found for %d", i)
+			t.Fatalf("value found for %d", i)
 		}
 	}
 }
 
-func TestMapSerialStore(t *testing.T) {
+func TestMapStore(t *testing.T) {
 	const numEntries = 128
 	m := NewMap()
 	for i := 0; i < numEntries; i++ {
@@ -289,15 +289,15 @@ func TestMapSerialStore(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		v, ok := m.Load(strconv.Itoa(i))
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if vi, ok := v.(int); ok && vi != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestMapSerialLoadOrStore(t *testing.T) {
+func TestMapLoadOrStore(t *testing.T) {
 	const numEntries = 1000
 	m := NewMap()
 	for i := 0; i < numEntries; i++ {
@@ -305,12 +305,12 @@ func TestMapSerialLoadOrStore(t *testing.T) {
 	}
 	for i := 0; i < numEntries; i++ {
 		if _, loaded := m.LoadOrStore(strconv.Itoa(i), i); !loaded {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 	}
 }
 
-func TestMapSerialLoadOrCompute(t *testing.T) {
+func TestMapLoadOrCompute(t *testing.T) {
 	const numEntries = 1000
 	m := NewMap()
 	for i := 0; i < numEntries; i++ {
@@ -318,10 +318,10 @@ func TestMapSerialLoadOrCompute(t *testing.T) {
 			return i
 		})
 		if loaded {
-			t.Errorf("value not computed for %d", i)
+			t.Fatalf("value not computed for %d", i)
 		}
 		if vi, ok := v.(int); ok && vi != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 	for i := 0; i < numEntries; i++ {
@@ -329,15 +329,15 @@ func TestMapSerialLoadOrCompute(t *testing.T) {
 			return i
 		})
 		if !loaded {
-			t.Errorf("value not loaded for %d", i)
+			t.Fatalf("value not loaded for %d", i)
 		}
 		if vi, ok := v.(int); ok && vi != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestMapSerialLoadOrCompute_FunctionCalledOnce(t *testing.T) {
+func TestMapLoadOrCompute_FunctionCalledOnce(t *testing.T) {
 	m := NewMap()
 	for i := 0; i < 100; {
 		m.LoadOrCompute(strconv.Itoa(i), func() (v interface{}) {
@@ -348,13 +348,91 @@ func TestMapSerialLoadOrCompute_FunctionCalledOnce(t *testing.T) {
 
 	m.Range(func(k string, v interface{}) bool {
 		if vi, ok := v.(int); !ok || strconv.Itoa(vi) != k {
-			t.Errorf("%sth key is not equal to value %d", k, v)
+			t.Fatalf("%sth key is not equal to value %d", k, v)
 		}
 		return true
 	})
 }
 
-func TestMapSerialStoreThenDelete(t *testing.T) {
+func TestMapCompute(t *testing.T) {
+	var zeroedV interface{}
+	m := NewMap()
+	// Store a new value.
+	v, ok := m.Compute("foobar", func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
+		if oldValue != zeroedV {
+			t.Fatalf("oldValue should be empty interface{} when computing a new value: %d", oldValue)
+		}
+		if loaded {
+			t.Fatal("loaded should be false when computing a new value")
+		}
+		newValue = 42
+		delete = false
+		return
+	})
+	if v.(int) != 42 {
+		t.Fatalf("v should be 42 when computing a new value: %d", v)
+	}
+	if !ok {
+		t.Fatal("ok should be true when computing a new value")
+	}
+	// Update an existing value.
+	v, ok = m.Compute("foobar", func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
+		if oldValue.(int) != 42 {
+			t.Fatalf("oldValue should be 42 when updating the value: %d", oldValue)
+		}
+		if !loaded {
+			t.Fatal("loaded should be true when updating the value")
+		}
+		newValue = oldValue.(int) + 42
+		delete = false
+		return
+	})
+	if v.(int) != 84 {
+		t.Fatalf("v should be 84 when updating the value: %d", v)
+	}
+	if !ok {
+		t.Fatal("ok should be true when updating the value")
+	}
+	// Delete an existing value.
+	v, ok = m.Compute("foobar", func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
+		if oldValue != 84 {
+			t.Fatalf("oldValue should be 84 when deleting the value: %d", oldValue)
+		}
+		if !loaded {
+			t.Fatal("loaded should be true when deleting the value")
+		}
+		delete = true
+		return
+	})
+	if v.(int) != 84 {
+		t.Fatalf("v should be 84 when deleting the value: %d", v)
+	}
+	if ok {
+		t.Fatal("ok should be false when deleting the value")
+	}
+	// Try to delete a non-existing value. Notice different key.
+	v, ok = m.Compute("barbaz", func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
+		var zeroedV interface{}
+		if oldValue != zeroedV {
+			t.Fatalf("oldValue should be empty interface{} when trying to delete a non-existing value: %d", oldValue)
+		}
+		if loaded {
+			t.Fatal("loaded should be false when trying to delete a non-existing value")
+		}
+		// We're returning a non-zero value, but the map should ignore it.
+		newValue = 42
+		delete = true
+		return
+	})
+	if v != zeroedV {
+		t.Fatalf("v should be empty interface{} when trying to delete a non-existing value: %d", v)
+	}
+	if ok {
+		t.Fatal("ok should be false when trying to delete a non-existing value")
+	}
+}
+
+func TestMapStoreThenDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewMap()
 	for i := 0; i < numEntries; i++ {
@@ -363,23 +441,23 @@ func TestMapSerialStoreThenDelete(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(strconv.Itoa(i))
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value was not expected for %d", i)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestMapSerialStoreThenLoadAndDelete(t *testing.T) {
+func TestMapStoreThenLoadAndDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewMap()
 	for i := 0; i < numEntries; i++ {
 		m.Store(strconv.Itoa(i), i)
 	}
 	for i := 0; i < numEntries; i++ {
-		if _, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded {
-			t.Errorf("value was not found for %d", i)
+		if v, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded || v.(int) != i {
+			t.Fatalf("value was not found or different for %d: %v", i, v)
 		}
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value was not expected for %d", i)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
@@ -398,7 +476,7 @@ func TestMapSize(t *testing.T) {
 	m := NewMap()
 	size := m.Size()
 	if size != 0 {
-		t.Errorf("zero size expected: %d", size)
+		t.Fatalf("zero size expected: %d", size)
 	}
 	expectedSize := 0
 	for i := 0; i < numEntries; i++ {
@@ -406,11 +484,11 @@ func TestMapSize(t *testing.T) {
 		expectedSize++
 		size := m.Size()
 		if size != expectedSize {
-			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+			t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
 		}
 		rsize := sizeBasedOnRange(m)
 		if size != rsize {
-			t.Errorf("size does not match number of entries in Range: %v, %v", size, rsize)
+			t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
 		}
 	}
 	for i := 0; i < numEntries; i++ {
@@ -418,11 +496,11 @@ func TestMapSize(t *testing.T) {
 		expectedSize--
 		size := m.Size()
 		if size != expectedSize {
-			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+			t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
 		}
 		rsize := sizeBasedOnRange(m)
 		if size != rsize {
-			t.Errorf("size does not match number of entries in Range: %v, %v", size, rsize)
+			t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
 		}
 	}
 }
@@ -435,16 +513,16 @@ func TestMapClear(t *testing.T) {
 	}
 	size := m.Size()
 	if size != numEntries {
-		t.Errorf("size of %d was expected, got: %d", numEntries, size)
+		t.Fatalf("size of %d was expected, got: %d", numEntries, size)
 	}
 	m.Clear()
 	size = m.Size()
 	if size != 0 {
-		t.Errorf("zero size was expected, got: %d", size)
+		t.Fatalf("zero size was expected, got: %d", size)
 	}
 	rsize := sizeBasedOnRange(m)
 	if rsize != 0 {
-		t.Errorf("zero number of entries in Range was expected, got: %d", rsize)
+		t.Fatalf("zero number of entries in Range was expected, got: %d", rsize)
 	}
 }
 
@@ -457,20 +535,20 @@ func TestMapResize(t *testing.T) {
 	}
 	stats := CollectMapStats(m)
 	if stats.Size != numEntries {
-		t.Errorf("size was too small: %d", stats.Size)
+		t.Fatalf("size was too small: %d", stats.Size)
 	}
 	expectedCapacity := int(math.RoundToEven(MapLoadFactor+1)) * stats.TableLen * EntriesPerMapBucket
 	if stats.Capacity > expectedCapacity {
-		t.Errorf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
+		t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
 	}
 	if stats.TableLen <= MinMapTableLen {
-		t.Errorf("table was too small: %d", stats.TableLen)
+		t.Fatalf("table was too small: %d", stats.TableLen)
 	}
 	if stats.TotalGrowths == 0 {
-		t.Errorf("non-zero total growths expected: %d", stats.TotalGrowths)
+		t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths)
 	}
 	if stats.TotalShrinks > 0 {
-		t.Errorf("zero total shrinks expected: %d", stats.TotalShrinks)
+		t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks)
 	}
 	// This is useful when debugging table resize and occupancy.
 	stats.Print()
@@ -480,17 +558,17 @@ func TestMapResize(t *testing.T) {
 	}
 	stats = CollectMapStats(m)
 	if stats.Size > 0 {
-		t.Errorf("zero size was expected: %d", stats.Size)
+		t.Fatalf("zero size was expected: %d", stats.Size)
 	}
 	expectedCapacity = stats.TableLen * EntriesPerMapBucket
 	if stats.Capacity != expectedCapacity {
-		t.Errorf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
+		t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
 	}
 	if stats.TableLen != MinMapTableLen {
-		t.Errorf("table was too large: %d", stats.TableLen)
+		t.Fatalf("table was too large: %d", stats.TableLen)
 	}
 	if stats.TotalShrinks == 0 {
-		t.Errorf("non-zero total shrinks expected: %d", stats.TotalShrinks)
+		t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks)
 	}
 	stats.Print()
 }
@@ -504,10 +582,10 @@ func TestMapResize_CounterLenLimit(t *testing.T) {
 	}
 	stats := CollectMapStats(m)
 	if stats.Size != numEntries {
-		t.Errorf("size was too small: %d", stats.Size)
+		t.Fatalf("size was too small: %d", stats.Size)
 	}
 	if stats.CounterLen != MaxMapCounterLen {
-		t.Errorf("number of counter stripes was too large: %d, expected: %d",
+		t.Fatalf("number of counter stripes was too large: %d, expected: %d",
 			stats.CounterLen, MaxMapCounterLen)
 	}
 }
@@ -536,14 +614,14 @@ func TestMapParallelResize_GrowOnly(t *testing.T) {
 	for i := -numEntries + 1; i < numEntries; i++ {
 		v, ok := m.Load(strconv.Itoa(i))
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if vi, ok := v.(int); ok && vi != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 	if s := m.Size(); s != 2*numEntries-1 {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 }
 
@@ -580,16 +658,16 @@ func TestMapParallelResize(t *testing.T) {
 			continue
 		}
 		if vi, ok := v.(int); ok && vi != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 	s := m.Size()
 	if s > numEntries {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 	rs := sizeBasedOnRange(m)
 	if s != rs {
-		t.Errorf("size does not match number of entries in Range: %v, %v", s, rs)
+		t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
 	}
 }
 
@@ -621,11 +699,11 @@ func TestMapParallelClear(t *testing.T) {
 	// Verify map size.
 	s := m.Size()
 	if s > numEntries {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 	rs := sizeBasedOnRange(m)
 	if s != rs {
-		t.Errorf("size does not match number of entries in Range: %v, %v", s, rs)
+		t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
 	}
 }
 
@@ -747,6 +825,44 @@ func TestMapParallelStoresAndDeletes(t *testing.T) {
 	}
 }
 
+func parallelComputer(t *testing.T, m *Map, numIters, numEntries int, cdone chan bool) {
+	for i := 0; i < numIters; i++ {
+		for j := 0; j < numEntries; j++ {
+			m.Compute(strconv.Itoa(j), func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
+				if !loaded {
+					return uint64(1), false
+				}
+				return uint64(oldValue.(uint64) + 1), false
+			})
+		}
+	}
+	cdone <- true
+}
+
+func TestMapParallelComputes(t *testing.T) {
+	const numWorkers = 4 // Also stands for numEntries.
+	const numIters = 10_000
+	m := NewMap()
+	cdone := make(chan bool)
+	for i := 0; i < numWorkers; i++ {
+		go parallelComputer(t, m, numIters, numWorkers, cdone)
+	}
+	// Wait for the goroutines to finish.
+	for i := 0; i < numWorkers; i++ {
+		<-cdone
+	}
+	// Verify map contents.
+	for i := 0; i < numWorkers; i++ {
+		v, ok := m.Load(strconv.Itoa(i))
+		if !ok {
+			t.Fatalf("value not found for %d", i)
+		}
+		if v.(uint64) != numWorkers*numIters {
+			t.Fatalf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
 func TestMapTopHashMutex(t *testing.T) {
 	const (
 		numLockers    = 4
@@ -794,7 +910,7 @@ func testMapTopHashMutex_Store(t *testing.T, topHashes *uint64) {
 	hash := uint64(0b1101_0100_1010_1011_1101 << 44)
 	for i := 0; i < EntriesPerMapBucket; i++ {
 		if TopHashMatch(hash, *topHashes, i) {
-			t.Errorf("top hash match for all zeros for index %d", i)
+			t.Fatalf("top hash match for all zeros for index %d", i)
 		}
 
 		prevOnes := bits.OnesCount64(*topHashes)
@@ -802,12 +918,12 @@ func testMapTopHashMutex_Store(t *testing.T, topHashes *uint64) {
 		newOnes := bits.OnesCount64(*topHashes)
 		expectedInc := bits.OnesCount64(hash) + 1
 		if newOnes != prevOnes+expectedInc {
-			t.Errorf("unexpected bits change after store for index %d: %d, %d, %#b",
+			t.Fatalf("unexpected bits change after store for index %d: %d, %d, %#b",
 				i, newOnes, prevOnes+expectedInc, *topHashes)
 		}
 
 		if !TopHashMatch(hash, *topHashes, i) {
-			t.Errorf("top hash mismatch after store for index %d: %#b", i, *topHashes)
+			t.Fatalf("top hash mismatch after store for index %d: %#b", i, *topHashes)
 		}
 	}
 }
@@ -832,12 +948,12 @@ func testMapTopHashMutex_Erase(t *testing.T, topHashes *uint64) {
 
 		*topHashes = EraseTopHash(*topHashes, i)
 		if TopHashMatch(hash, *topHashes, i) {
-			t.Errorf("top hash match after erase for index %d: %#b", i, *topHashes)
+			t.Fatalf("top hash match after erase for index %d: %#b", i, *topHashes)
 		}
 
 		erasedBits := ones - bits.OnesCount64(*topHashes)
 		if erasedBits != 1 {
-			t.Errorf("more than one bit changed after erase: %d, %d", i, erasedBits)
+			t.Fatalf("more than one bit changed after erase: %d, %d", i, erasedBits)
 		}
 	}
 }
@@ -861,19 +977,19 @@ func testMapTopHashMutex_StoreAfterErase(t *testing.T, topHashes *uint64) {
 
 	*topHashes = StoreTopHash(hashOne, *topHashes, idx)
 	if !TopHashMatch(hashOne, *topHashes, idx) {
-		t.Errorf("top hash mismatch for hash one: %#b, %#b", hashOne, *topHashes)
+		t.Fatalf("top hash mismatch for hash one: %#b, %#b", hashOne, *topHashes)
 	}
 	if TopHashMatch(hashTwo, *topHashes, idx) {
-		t.Errorf("top hash match for hash two: %#b, %#b", hashTwo, *topHashes)
+		t.Fatalf("top hash match for hash two: %#b, %#b", hashTwo, *topHashes)
 	}
 
 	*topHashes = EraseTopHash(*topHashes, idx)
 	*topHashes = StoreTopHash(hashTwo, *topHashes, idx)
 	if TopHashMatch(hashOne, *topHashes, idx) {
-		t.Errorf("top hash match for hash one: %#b, %#b", hashOne, *topHashes)
+		t.Fatalf("top hash match for hash one: %#b, %#b", hashOne, *topHashes)
 	}
 	if !TopHashMatch(hashTwo, *topHashes, idx) {
-		t.Errorf("top hash mismatch for hash two: %#b, %#b", hashTwo, *topHashes)
+		t.Fatalf("top hash mismatch for hash two: %#b, %#b", hashTwo, *topHashes)
 	}
 }
 
diff --git a/mapof_test.go b/mapof_test.go
index 05b4804..e6b5e8b 100644
--- a/mapof_test.go
+++ b/mapof_test.go
@@ -75,13 +75,13 @@ func TestMapOf_MissingEntry(t *testing.T) {
 	m := NewMapOf[string]()
 	v, ok := m.Load("foo")
 	if ok {
-		t.Errorf("value was not expected: %v", v)
+		t.Fatalf("value was not expected: %v", v)
 	}
 	if deleted, loaded := m.LoadAndDelete("foo"); loaded {
-		t.Errorf("value was not expected %v", deleted)
+		t.Fatalf("value was not expected %v", deleted)
 	}
 	if actual, loaded := m.LoadOrStore("foo", "bar"); loaded {
-		t.Errorf("value was not expected %v", actual)
+		t.Fatalf("value was not expected %v", actual)
 	}
 }
 
@@ -93,7 +93,7 @@ func TestMapOf_EmptyStringKey(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if v != "foobar" {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -105,7 +105,7 @@ func TestMapOfStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -117,7 +117,7 @@ func TestMapOfLoadOrStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -130,7 +130,7 @@ func TestMapOfLoadOrStore_NonNilValue(t *testing.T) {
 		t.Error("no value was expected")
 	}
 	if v != newv {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	newv2 := &foo{}
 	v, loaded = m.LoadOrStore("foo", newv2)
@@ -138,7 +138,7 @@ func TestMapOfLoadOrStore_NonNilValue(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if v != newv {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -150,14 +150,14 @@ func TestMapOfLoadAndStore_NilValue(t *testing.T) {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 	v, loaded = m.Load("foo")
 	if !loaded {
 		t.Error("nil value was expected")
 	}
 	if v != nil {
-		t.Errorf("value was not nil: %v", v)
+		t.Fatalf("value was not nil: %v", v)
 	}
 }
 
@@ -169,7 +169,7 @@ func TestMapOfLoadAndStore_NonNilValue(t *testing.T) {
 		t.Error("no value was expected")
 	}
 	if v != v1 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	v2 := 2
 	v, loaded = m.LoadAndStore("foo", v2)
@@ -177,14 +177,14 @@ func TestMapOfLoadAndStore_NonNilValue(t *testing.T) {
 		t.Error("value was expected")
 	}
 	if v != v1 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 	v, loaded = m.Load("foo")
 	if !loaded {
 		t.Error("value was expected")
 	}
 	if v != v2 {
-		t.Errorf("value does not match: %v", v)
+		t.Fatalf("value does not match: %v", v)
 	}
 }
 
@@ -198,7 +198,7 @@ func TestMapOfRange(t *testing.T) {
 	met := make(map[string]int)
 	m.Range(func(key string, value int) bool {
 		if key != strconv.Itoa(value) {
-			t.Errorf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
+			t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
 			return false
 		}
 		met[key] += 1
@@ -206,11 +206,11 @@ func TestMapOfRange(t *testing.T) {
 		return true
 	})
 	if iters != numEntries {
-		t.Errorf("got unexpected number of iterations: %d", iters)
+		t.Fatalf("got unexpected number of iterations: %d", iters)
 	}
 	for i := 0; i < numEntries; i++ {
 		if c := met[strconv.Itoa(i)]; c != 1 {
-			t.Errorf("range did not iterate correctly over %d: %d", i, c)
+			t.Fatalf("range did not iterate correctly over %d: %d", i, c)
 		}
 	}
 }
@@ -226,7 +226,7 @@ func TestMapOfRange_FalseReturned(t *testing.T) {
 		return iters != 13
 	})
 	if iters != 13 {
-		t.Errorf("got unexpected number of iterations: %d", iters)
+		t.Fatalf("got unexpected number of iterations: %d", iters)
 	}
 }
 
@@ -242,12 +242,12 @@ func TestMapOfRange_NestedDelete(t *testing.T) {
 	})
 	for i := 0; i < numEntries; i++ {
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value found for %d", i)
+			t.Fatalf("value found for %d", i)
 		}
 	}
 }
 
-func TestMapOfSerialStore(t *testing.T) {
+func TestMapOfStore(t *testing.T) {
 	const numEntries = 128
 	m := NewMapOf[int]()
 	for i := 0; i < numEntries; i++ {
@@ -256,15 +256,15 @@ func TestMapOfSerialStore(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		v, ok := m.Load(strconv.Itoa(i))
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestIntegerMapOfSerialStore(t *testing.T) {
+func TestIntegerMapOfStore(t *testing.T) {
 	const numEntries = 128
 	m := NewIntegerMapOf[int, int]()
 	for i := 0; i < numEntries; i++ {
@@ -273,70 +273,76 @@ func TestIntegerMapOfSerialStore(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		v, ok := m.Load(i)
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestTypedMapOfSerialStore_StructKeys_IntValues(t *testing.T) {
+func TestTypedMapOfStore_StructKeys_IntValues(t *testing.T) {
 	type foo struct {
-		x int
-		y int
+		x int32
+		y int32
 	}
 	const numEntries = 128
 	m := NewTypedMapOf[foo, int](func(seed maphash.Seed, f foo) uint64 {
 		var h maphash.Hash
 		h.SetSeed(seed)
-		binary.Write(&h, binary.LittleEndian, f)
-		return h.Sum64()
+		binary.Write(&h, binary.LittleEndian, f.x)
+		hash := h.Sum64()
+		h.Reset()
+		binary.Write(&h, binary.LittleEndian, f.y)
+		return 31*hash + h.Sum64()
 	})
 	for i := 0; i < numEntries; i++ {
-		m.Store(foo{i, -i}, i)
+		m.Store(foo{int32(i), -int32(i)}, i)
 	}
 	for i := 0; i < numEntries; i++ {
-		v, ok := m.Load(foo{i, -i})
+		v, ok := m.Load(foo{int32(i), -int32(i)})
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestTypedMapOfSerialStore_StructKeys_StructValues(t *testing.T) {
+func TestTypedMapOfStore_StructKeys_StructValues(t *testing.T) {
 	type foo struct {
-		x int
-		y int
+		x int32
+		y int32
 	}
 	const numEntries = 128
 	m := NewTypedMapOf[foo, foo](func(seed maphash.Seed, f foo) uint64 {
 		var h maphash.Hash
 		h.SetSeed(seed)
-		binary.Write(&h, binary.LittleEndian, f)
-		return h.Sum64()
+		binary.Write(&h, binary.LittleEndian, f.x)
+		hash := h.Sum64()
+		h.Reset()
+		binary.Write(&h, binary.LittleEndian, f.y)
+		return 31*hash + h.Sum64()
 	})
 	for i := 0; i < numEntries; i++ {
-		m.Store(foo{i, -i}, foo{-i, i})
+		m.Store(foo{int32(i), -int32(i)}, foo{-int32(i), int32(i)})
 	}
 	for i := 0; i < numEntries; i++ {
-		v, ok := m.Load(foo{i, -i})
+		v, ok := m.Load(foo{int32(i), -int32(i)})
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
-		if v.x != -i {
-			t.Errorf("x value does not match for %d: %v", i, v)
+		if v.x != -int32(i) {
+			t.Fatalf("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)
+		if v.y != int32(i) {
+			t.Fatalf("y value does not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestTypedMapOfSerialStore_HashCodeCollisions(t *testing.T) {
+func TestTypedMapOfStore_HashCodeCollisions(t *testing.T) {
 	const numEntries = 1000
 	m := NewTypedMapOf[int, int](func(_ maphash.Seed, i int) uint64 {
 		// We intentionally use an awful hash function here to make sure
@@ -349,15 +355,15 @@ func TestTypedMapOfSerialStore_HashCodeCollisions(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		v, ok := m.Load(i)
 		if !ok {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestMapOfSerialLoadOrStore(t *testing.T) {
+func TestMapOfLoadOrStore(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
 	for i := 0; i < numEntries; i++ {
@@ -365,12 +371,12 @@ func TestMapOfSerialLoadOrStore(t *testing.T) {
 	}
 	for i := 0; i < numEntries; i++ {
 		if _, loaded := m.LoadOrStore(strconv.Itoa(i), i); !loaded {
-			t.Errorf("value not found for %d", i)
+			t.Fatalf("value not found for %d", i)
 		}
 	}
 }
 
-func TestMapOfSerialLoadOrCompute(t *testing.T) {
+func TestMapOfLoadOrCompute(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
 	for i := 0; i < numEntries; i++ {
@@ -378,10 +384,10 @@ func TestMapOfSerialLoadOrCompute(t *testing.T) {
 			return i
 		})
 		if loaded {
-			t.Errorf("value not computed for %d", i)
+			t.Fatalf("value not computed for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 	for i := 0; i < numEntries; i++ {
@@ -389,15 +395,15 @@ func TestMapOfSerialLoadOrCompute(t *testing.T) {
 			return i
 		})
 		if !loaded {
-			t.Errorf("value not loaded for %d", i)
+			t.Fatalf("value not loaded for %d", i)
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 }
 
-func TestMapOfSerialLoadOrCompute_FunctionCalledOnce(t *testing.T) {
+func TestMapOfLoadOrCompute_FunctionCalledOnce(t *testing.T) {
 	m := NewIntegerMapOf[int, int]()
 	for i := 0; i < 100; {
 		m.LoadOrCompute(i, func() (v int) {
@@ -408,13 +414,89 @@ func TestMapOfSerialLoadOrCompute_FunctionCalledOnce(t *testing.T) {
 
 	m.Range(func(k, v int) bool {
 		if k != v {
-			t.Errorf("%dth key is not equal to value %d", k, v)
+			t.Fatalf("%dth key is not equal to value %d", k, v)
 		}
 		return true
 	})
 }
 
-func TestMapOfSerialStoreThenDelete(t *testing.T) {
+func TestMapOfCompute(t *testing.T) {
+	m := NewMapOf[int]()
+	// Store a new value.
+	v, ok := m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, delete bool) {
+		if oldValue != 0 {
+			t.Fatalf("oldValue should be 0 when computing a new value: %d", oldValue)
+		}
+		if loaded {
+			t.Fatal("loaded should be false when computing a new value")
+		}
+		newValue = 42
+		delete = false
+		return
+	})
+	if v != 42 {
+		t.Fatalf("v should be 42 when computing a new value: %d", v)
+	}
+	if !ok {
+		t.Fatal("ok should be true when computing a new value")
+	}
+	// Update an existing value.
+	v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, delete bool) {
+		if oldValue != 42 {
+			t.Fatalf("oldValue should be 42 when updating the value: %d", oldValue)
+		}
+		if !loaded {
+			t.Fatal("loaded should be true when updating the value")
+		}
+		newValue = oldValue + 42
+		delete = false
+		return
+	})
+	if v != 84 {
+		t.Fatalf("v should be 84 when updating the value: %d", v)
+	}
+	if !ok {
+		t.Fatal("ok should be true when updating the value")
+	}
+	// Delete an existing value.
+	v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, delete bool) {
+		if oldValue != 84 {
+			t.Fatalf("oldValue should be 84 when deleting the value: %d", oldValue)
+		}
+		if !loaded {
+			t.Fatal("loaded should be true when deleting the value")
+		}
+		delete = true
+		return
+	})
+	if v != 84 {
+		t.Fatalf("v should be 84 when deleting the value: %d", v)
+	}
+	if ok {
+		t.Fatal("ok should be false when deleting the value")
+	}
+	// Try to delete a non-existing value. Notice different key.
+	v, ok = m.Compute("barbaz", func(oldValue int, loaded bool) (newValue int, delete bool) {
+		if oldValue != 0 {
+			t.Fatalf("oldValue should be 0 when trying to delete a non-existing value: %d", oldValue)
+		}
+		if loaded {
+			t.Fatal("loaded should be false when trying to delete a non-existing value")
+		}
+		// We're returning a non-zero value, but the map should ignore it.
+		newValue = 42
+		delete = true
+		return
+	})
+	if v != 0 {
+		t.Fatalf("v should be 0 when trying to delete a non-existing value: %d", v)
+	}
+	if ok {
+		t.Fatal("ok should be false when trying to delete a non-existing value")
+	}
+}
+
+func TestMapOfStoreThenDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
 	for i := 0; i < numEntries; i++ {
@@ -423,12 +505,12 @@ func TestMapOfSerialStoreThenDelete(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(strconv.Itoa(i))
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value was not expected for %d", i)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestIntegerMapOfSerialStoreThenDelete(t *testing.T) {
+func TestIntegerMapOfStoreThenDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewIntegerMapOf[int32, int32]()
 	for i := 0; i < numEntries; i++ {
@@ -437,51 +519,54 @@ func TestIntegerMapOfSerialStoreThenDelete(t *testing.T) {
 	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)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestTypedMapOfSerialStoreThenDelete(t *testing.T) {
+func TestTypedMapOfStoreThenDelete(t *testing.T) {
 	type foo struct {
-		x int
-		y int
+		x int32
+		y int32
 	}
 	const numEntries = 1000
 	m := NewTypedMapOf[foo, string](func(seed maphash.Seed, f foo) uint64 {
 		var h maphash.Hash
 		h.SetSeed(seed)
-		binary.Write(&h, binary.LittleEndian, f)
-		return h.Sum64()
+		binary.Write(&h, binary.LittleEndian, f.x)
+		hash := h.Sum64()
+		h.Reset()
+		binary.Write(&h, binary.LittleEndian, f.y)
+		return 31*hash + h.Sum64()
 	})
 	for i := 0; i < numEntries; i++ {
-		m.Store(foo{i, 42}, strconv.Itoa(i))
+		m.Store(foo{int32(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)
+		m.Delete(foo{int32(i), 42})
+		if _, ok := m.Load(foo{int32(i), 42}); ok {
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
+func TestMapOfStoreThenLoadAndDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewMapOf[int]()
 	for i := 0; i < numEntries; i++ {
 		m.Store(strconv.Itoa(i), i)
 	}
 	for i := 0; i < numEntries; i++ {
-		if _, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded {
-			t.Errorf("value was not found for %d", i)
+		if v, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded || v != i {
+			t.Fatalf("value was not found or different for %d: %v", i, v)
 		}
 		if _, ok := m.Load(strconv.Itoa(i)); ok {
-			t.Errorf("value was not expected for %d", i)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestIntegerMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
+func TestIntegerMapOfStoreThenLoadAndDelete(t *testing.T) {
 	const numEntries = 1000
 	m := NewIntegerMapOf[int, int]()
 	for i := 0; i < numEntries; i++ {
@@ -489,35 +574,38 @@ func TestIntegerMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
 	}
 	for i := 0; i < numEntries; i++ {
 		if _, loaded := m.LoadAndDelete(i); !loaded {
-			t.Errorf("value was not found for %d", i)
+			t.Fatalf("value was not found for %d", i)
 		}
 		if _, ok := m.Load(i); ok {
-			t.Errorf("value was not expected for %d", i)
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
 
-func TestTypedMapOfSerialStoreThenLoadAndDelete(t *testing.T) {
+func TestTypedMapOfStoreThenLoadAndDelete(t *testing.T) {
 	type foo struct {
-		x int
-		y int
+		x int32
+		y int32
 	}
 	const numEntries = 1000
 	m := NewTypedMapOf[foo, int](func(seed maphash.Seed, f foo) uint64 {
 		var h maphash.Hash
 		h.SetSeed(seed)
-		binary.Write(&h, binary.LittleEndian, f)
-		return h.Sum64()
+		binary.Write(&h, binary.LittleEndian, f.x)
+		hash := h.Sum64()
+		h.Reset()
+		binary.Write(&h, binary.LittleEndian, f.y)
+		return 31*hash + h.Sum64()
 	})
 	for i := 0; i < numEntries; i++ {
-		m.Store(foo{42, i}, i)
+		m.Store(foo{42, int32(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 _, loaded := m.LoadAndDelete(foo{42, int32(i)}); !loaded {
+			t.Fatalf("value was not found for %d", i)
 		}
-		if _, ok := m.Load(foo{42, i}); ok {
-			t.Errorf("value was not expected for %d", i)
+		if _, ok := m.Load(foo{42, int32(i)}); ok {
+			t.Fatalf("value was not expected for %d", i)
 		}
 	}
 }
@@ -536,7 +624,7 @@ func TestMapOfSize(t *testing.T) {
 	m := NewMapOf[int]()
 	size := m.Size()
 	if size != 0 {
-		t.Errorf("zero size expected: %d", size)
+		t.Fatalf("zero size expected: %d", size)
 	}
 	expectedSize := 0
 	for i := 0; i < numEntries; i++ {
@@ -544,11 +632,11 @@ func TestMapOfSize(t *testing.T) {
 		expectedSize++
 		size := m.Size()
 		if size != expectedSize {
-			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+			t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
 		}
 		rsize := sizeBasedOnTypedRange(m)
 		if size != rsize {
-			t.Errorf("size does not match number of entries in Range: %v, %v", size, rsize)
+			t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
 		}
 	}
 	for i := 0; i < numEntries; i++ {
@@ -556,11 +644,11 @@ func TestMapOfSize(t *testing.T) {
 		expectedSize--
 		size := m.Size()
 		if size != expectedSize {
-			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+			t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
 		}
 		rsize := sizeBasedOnTypedRange(m)
 		if size != rsize {
-			t.Errorf("size does not match number of entries in Range: %v, %v", size, rsize)
+			t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
 		}
 	}
 }
@@ -573,16 +661,16 @@ func TestMapOfClear(t *testing.T) {
 	}
 	size := m.Size()
 	if size != numEntries {
-		t.Errorf("size of %d was expected, got: %d", numEntries, size)
+		t.Fatalf("size of %d was expected, got: %d", numEntries, size)
 	}
 	m.Clear()
 	size = m.Size()
 	if size != 0 {
-		t.Errorf("zero size was expected, got: %d", size)
+		t.Fatalf("zero size was expected, got: %d", size)
 	}
 	rsize := sizeBasedOnTypedRange(m)
 	if rsize != 0 {
-		t.Errorf("zero number of entries in Range was expected, got: %d", rsize)
+		t.Fatalf("zero number of entries in Range was expected, got: %d", rsize)
 	}
 }
 
@@ -595,20 +683,20 @@ func TestMapOfResize(t *testing.T) {
 	}
 	stats := CollectMapOfStats(m)
 	if stats.Size != numEntries {
-		t.Errorf("size was too small: %d", stats.Size)
+		t.Fatalf("size was too small: %d", stats.Size)
 	}
 	expectedCapacity := int(math.RoundToEven(MapLoadFactor+1)) * stats.TableLen * EntriesPerMapBucket
 	if stats.Capacity > expectedCapacity {
-		t.Errorf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
+		t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
 	}
 	if stats.TableLen <= MinMapTableLen {
-		t.Errorf("table was too small: %d", stats.TableLen)
+		t.Fatalf("table was too small: %d", stats.TableLen)
 	}
 	if stats.TotalGrowths == 0 {
-		t.Errorf("non-zero total growths expected: %d", stats.TotalGrowths)
+		t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths)
 	}
 	if stats.TotalShrinks > 0 {
-		t.Errorf("zero total shrinks expected: %d", stats.TotalShrinks)
+		t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks)
 	}
 	// This is useful when debugging table resize and occupancy.
 	stats.Print()
@@ -618,17 +706,17 @@ func TestMapOfResize(t *testing.T) {
 	}
 	stats = CollectMapOfStats(m)
 	if stats.Size > 0 {
-		t.Errorf("zero size was expected: %d", stats.Size)
+		t.Fatalf("zero size was expected: %d", stats.Size)
 	}
 	expectedCapacity = stats.TableLen * EntriesPerMapBucket
 	if stats.Capacity != expectedCapacity {
-		t.Errorf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
+		t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
 	}
 	if stats.TableLen != MinMapTableLen {
-		t.Errorf("table was too large: %d", stats.TableLen)
+		t.Fatalf("table was too large: %d", stats.TableLen)
 	}
 	if stats.TotalShrinks == 0 {
-		t.Errorf("non-zero total shrinks expected: %d", stats.TotalShrinks)
+		t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks)
 	}
 	stats.Print()
 }
@@ -642,10 +730,10 @@ func TestMapOfResize_CounterLenLimit(t *testing.T) {
 	}
 	stats := CollectMapOfStats(m)
 	if stats.Size != numEntries {
-		t.Errorf("size was too small: %d", stats.Size)
+		t.Fatalf("size was too small: %d", stats.Size)
 	}
 	if stats.CounterLen != MaxMapCounterLen {
-		t.Errorf("number of counter stripes was too large: %d, expected: %d",
+		t.Fatalf("number of counter stripes was too large: %d, expected: %d",
 			stats.CounterLen, MaxMapCounterLen)
 	}
 }
@@ -681,7 +769,7 @@ func TestMapOfParallelResize_GrowOnly(t *testing.T) {
 		}
 	}
 	if s := m.Size(); s != 2*numEntries-1 {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 }
 
@@ -718,16 +806,16 @@ func TestMapOfParallelResize(t *testing.T) {
 			continue
 		}
 		if v != i {
-			t.Errorf("values do not match for %d: %v", i, v)
+			t.Fatalf("values do not match for %d: %v", i, v)
 		}
 	}
 	s := m.Size()
 	if s > numEntries {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 	rs := sizeBasedOnTypedRange(m)
 	if s != rs {
-		t.Errorf("size does not match number of entries in Range: %v, %v", s, rs)
+		t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
 	}
 }
 
@@ -759,11 +847,11 @@ func TestMapOfParallelClear(t *testing.T) {
 	// Verify map size.
 	s := m.Size()
 	if s > numEntries {
-		t.Errorf("unexpected size: %v", s)
+		t.Fatalf("unexpected size: %v", s)
 	}
 	rs := sizeBasedOnTypedRange(m)
 	if s != rs {
-		t.Errorf("size does not match number of entries in Range: %v, %v", s, rs)
+		t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
 	}
 }
 
@@ -885,6 +973,41 @@ func TestMapOfParallelStoresAndDeletes(t *testing.T) {
 	}
 }
 
+func parallelTypedComputer(t *testing.T, m *MapOf[uint64, uint64], numIters, numEntries int, cdone chan bool) {
+	for i := 0; i < numIters; i++ {
+		for j := 0; j < numEntries; j++ {
+			m.Compute(uint64(j), func(oldValue uint64, loaded bool) (newValue uint64, delete bool) {
+				return oldValue + 1, false
+			})
+		}
+	}
+	cdone <- true
+}
+
+func TestMapOfParallelComputes(t *testing.T) {
+	const numWorkers = 4 // Also stands for numEntries.
+	const numIters = 10_000
+	m := NewIntegerMapOf[uint64, uint64]()
+	cdone := make(chan bool)
+	for i := 0; i < numWorkers; i++ {
+		go parallelTypedComputer(t, m, numIters, numWorkers, cdone)
+	}
+	// Wait for the goroutines to finish.
+	for i := 0; i < numWorkers; i++ {
+		<-cdone
+	}
+	// Verify map contents.
+	for i := 0; i < numWorkers; i++ {
+		v, ok := m.Load(uint64(i))
+		if !ok {
+			t.Fatalf("value not found for %d", i)
+		}
+		if v != numWorkers*numIters {
+			t.Fatalf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
 func BenchmarkMapOf_NoWarmUp(b *testing.B) {
 	for _, bc := range benchmarkCases {
 		if bc.readPercentage == 100 {
diff --git a/mpmcqueue_test.go b/mpmcqueue_test.go
index e9b9c97..10c416f 100644
--- a/mpmcqueue_test.go
+++ b/mpmcqueue_test.go
@@ -27,7 +27,7 @@ func TestQueueEnqueueDequeue(t *testing.T) {
 	}
 	for i := 0; i < 10; i++ {
 		if got := q.Dequeue(); got != i {
-			t.Errorf("got %v, want %d", got, i)
+			t.Fatalf("got %v, want %d", got, i)
 		}
 	}
 }
@@ -47,7 +47,7 @@ func TestQueueEnqueueBlocksOnFull(t *testing.T) {
 	time.Sleep(50 * time.Millisecond)
 	atomic.StoreInt32(&flag, 1)
 	if got := q.Dequeue(); got != "foo" {
-		t.Errorf("got %v, want foo", got)
+		t.Fatalf("got %v, want foo", got)
 	}
 	<-cdone
 }
@@ -73,12 +73,12 @@ func TestQueueTryEnqueueDequeue(t *testing.T) {
 	q := NewMPMCQueue(10)
 	for i := 0; i < 10; i++ {
 		if !q.TryEnqueue(i) {
-			t.Errorf("failed to enqueue for %d", i)
+			t.Fatalf("failed to enqueue for %d", i)
 		}
 	}
 	for i := 0; i < 10; i++ {
 		if got, ok := q.TryDequeue(); !ok || got != i {
-			t.Errorf("got %v, want %d, for status %v", got, i, ok)
+			t.Fatalf("got %v, want %d, for status %v", got, i, ok)
 		}
 	}
 }
@@ -137,7 +137,7 @@ func hammerQueueBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int)
 	// Assert the total sum.
 	expectedSum := numOps * (numOps - 1) / 2
 	if sum != expectedSum {
-		t.Errorf("sums don't match for %d num ops, %d num threads: got %d, want %d",
+		t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d",
 			numOps, numThreads, sum, expectedSum)
 	}
 }
@@ -204,7 +204,7 @@ func hammerQueueNonBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads in
 	// Assert the total sum.
 	expectedSum := numOps * (numOps - 1) / 2
 	if sum != expectedSum {
-		t.Errorf("sums don't match for %d num ops, %d num threads: got %d, want %d",
+		t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d",
 			numOps, numThreads, sum, expectedSum)
 	}
 }