Repository: puzpuzpuz/xsync
Pull request #34 (2022-02-13 21:54:34)

Add type parameter for map values
Resolves https://github.com/puzpuzpuz/xsync/issues/33.

This PR updates `Map` implementation to use type parameter for values, keys are left as strings. 

As a draft (proof of concept) it changes `Map` to `MapOf[V any]`. If the approach is correct and adding new implementation makes sense, `Map` would be reverted to its original state and `MapOf` would be added as a new identifier.

In-place change is temporary for sake of easier review of changes.

I benchmarked both implementations with 
```
go version devel go1.18-16b1893600 Sun Feb 13 18:51:07 2022 +0000 darwin/amd64
```
```
gotip test -bench ^BenchmarkMap_ -count 5 -benchmem -run ^$
```
but unfortunately could not reliably see performance improvement (measured with noisy intel macbook).

```
name                       old time/op    new time/op    delta
Map_NoWarmUp/99%-reads-12    37.5ns ± 7%    37.0ns ± 3%     ~     (p=1.000 n=5+5)
Map_NoWarmUp/90%-reads-12    52.8ns ± 2%    54.7ns ±17%     ~     (p=1.000 n=5+5)
Map_NoWarmUp/75%-reads-12    66.1ns ±12%    59.9ns ± 2%     ~     (p=0.556 n=5+4)
Map_NoWarmUp/50%-reads-12    84.2ns ±21%    77.2ns ±12%     ~     (p=0.222 n=5+5)
Map_NoWarmUp/0%-reads-12      100ns ±20%      93ns ±14%     ~     (p=0.310 n=5+5)
Map_WarmUp/100%-reads-12     81.2ns ± 0%    81.3ns ± 0%     ~     (p=1.000 n=4+5)
Map_WarmUp/99%-reads-12      79.4ns ± 1%    78.2ns ± 4%     ~     (p=0.151 n=5+5)
Map_WarmUp/90%-reads-12      66.2ns ± 1%    42.9ns ± 1%  -35.21%  (p=0.016 n=5+4)
Map_WarmUp/75%-reads-12      57.5ns ± 2%    57.3ns ± 1%     ~     (p=0.841 n=5+5)
Map_WarmUp/50%-reads-12      58.0ns ± 1%    58.4ns ± 0%   +0.81%  (p=0.008 n=5+5)
Map_WarmUp/0%-reads-12       66.3ns ± 1%    65.7ns ± 1%   -0.92%  (p=0.024 n=5+5)

name                       old alloc/op   new alloc/op   delta
Map_NoWarmUp/99%-reads-12     1.00B ± 0%     1.00B ± 0%     ~     (all equal)
Map_NoWarmUp/90%-reads-12     11.4B ±14%     14.0B ±64%     ~     (p=0.437 n=5+5)
Map_NoWarmUp/75%-reads-12     27.0B ±33%     22.6B ±50%     ~     (p=0.921 n=5+5)
Map_NoWarmUp/50%-reads-12     45.4B ±45%     35.6B ±30%     ~     (p=0.143 n=5+5)
Map_NoWarmUp/0%-reads-12      63.2B ±35%     55.0B ±36%     ~     (p=0.508 n=5+5)
Map_WarmUp/100%-reads-12      0.00B          0.00B          ~     (all equal)
Map_WarmUp/99%-reads-12       0.00B          0.00B          ~     (all equal)
Map_WarmUp/90%-reads-12       2.00B ± 0%     2.00B ± 0%     ~     (all equal)
Map_WarmUp/75%-reads-12       5.00B ± 0%     5.00B ± 0%     ~     (all equal)
Map_WarmUp/50%-reads-12       10.0B ± 0%      9.6B ± 6%     ~     (p=0.333 n=4+5)
Map_WarmUp/0%-reads-12        20.0B ± 0%     20.0B ± 0%     ~     (all equal)

name                       old allocs/op  new allocs/op  delta
Map_NoWarmUp/99%-reads-12      0.00           0.00          ~     (all equal)
Map_NoWarmUp/90%-reads-12      0.00           0.00          ~     (all equal)
Map_NoWarmUp/75%-reads-12      0.00           0.00          ~     (all equal)
Map_NoWarmUp/50%-reads-12      0.00           0.00          ~     (all equal)
Map_NoWarmUp/0%-reads-12       1.00 ± 0%      1.00 ± 0%     ~     (all equal)
Map_WarmUp/100%-reads-12       0.00           0.00          ~     (all equal)
Map_WarmUp/99%-reads-12        0.00           0.00          ~     (all equal)
Map_WarmUp/90%-reads-12        0.00           0.00          ~     (all equal)
Map_WarmUp/75%-reads-12        0.00           0.00          ~     (all equal)
Map_WarmUp/50%-reads-12        0.00           0.00          ~     (all equal)
Map_WarmUp/0%-reads-12         1.00 ± 0%      1.00 ± 0%     ~     (all equal)
```

<details>
<summary>interface.txt</summary>

```
goos: darwin
goarch: amd64
pkg: github.com/puzpuzpuz/xsync
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkMap_NoWarmUp/99%-reads-12         	48962466	        36.49 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	47330576	        37.41 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	47011944	        40.01 ns/op	       3 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	43511911	        37.43 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	45625410	        36.39 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	26827248	        53.85 ns/op	      12 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	27721191	        52.07 ns/op	      11 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	22768507	        51.61 ns/op	      13 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	27333925	        53.67 ns/op	      11 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	30015016	        52.87 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18419127	        72.92 ns/op	      34 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18533600	        59.35 ns/op	      19 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	19422375	        57.96 ns/op	      18 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	19942258	        69.41 ns/op	      31 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18577172	        70.73 ns/op	      33 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	14960580	        85.94 ns/op	      45 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	12971814	        87.42 ns/op	      51 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	11677726	        90.44 ns/op	      55 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	12825160	        90.70 ns/op	      51 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	17156444	        66.37 ns/op	      25 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	 8295542	       120.8 ns/op	      84 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	12492330	        83.34 ns/op	      41 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	14663932	        94.18 ns/op	      56 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	10136504	       108.1 ns/op	      72 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	12357886	        95.51 ns/op	      63 B/op	       1 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19745044	        81.27 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	17525035	        82.54 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19255826	        81.25 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19465071	        81.08 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19471263	        81.29 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19552345	        80.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19766455	        79.51 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19384752	        79.17 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19312754	        78.29 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19431264	        79.44 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19555131	        65.86 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19221114	        66.76 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19346696	        66.11 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19609820	        66.43 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19181444	        66.00 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	19124010	        58.69 ns/op	       4 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18854839	        57.22 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18768171	        57.40 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	19039752	        56.97 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18920331	        57.19 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	20528736	        57.59 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	20534898	        57.93 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	18273267	        58.09 ns/op	       9 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17896005	        58.11 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17724134	        58.18 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15905893	        66.04 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15595194	        66.39 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15850712	        66.15 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	16000840	        66.82 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15789391	        65.92 ns/op	      20 B/op	       1 allocs/op
PASS
ok  	github.com/puzpuzpuz/xsync	203.218s

```
</details>


<details>
<summary>generic.txt</summary>

```
goos: darwin
goarch: amd64
pkg: github.com/puzpuzpuz/xsync
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkMap_NoWarmUp/99%-reads-12         	45228591	        37.74 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	46462082	        37.63 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	46670072	        35.82 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	46281225	        36.25 ns/op	       1 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/99%-reads-12         	47246422	        37.47 ns/op	       3 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	27384402	        53.08 ns/op	      11 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	23195971	        53.35 ns/op	      13 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	28019607	        52.59 ns/op	      11 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	25035237	        50.59 ns/op	      12 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/90%-reads-12         	24977448	        63.69 ns/op	      23 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	16451547	        60.79 ns/op	      21 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	17741193	        59.41 ns/op	      20 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18839890	        58.72 ns/op	      19 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18815866	        60.78 ns/op	      19 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/75%-reads-12         	18102775	        75.92 ns/op	      34 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	14985184	        86.85 ns/op	      45 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	15930730	        69.97 ns/op	      26 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	17425974	        80.84 ns/op	      40 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	17401228	        67.71 ns/op	      25 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/50%-reads-12         	16593295	        80.75 ns/op	      42 B/op	       0 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	14395152	        80.01 ns/op	      38 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	11224239	       102.0 ns/op	      67 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	14195145	        92.44 ns/op	      57 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	14154236	        82.95 ns/op	      38 B/op	       1 allocs/op
BenchmarkMap_NoWarmUp/0%-reads-12          	 9758628	       105.3 ns/op	      75 B/op	       1 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19474294	        81.12 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19599298	        81.24 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	17259147	        81.54 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19449316	        81.04 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/100%-reads-12          	19230926	        81.62 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19425238	        78.26 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	20386447	        81.59 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19611324	        76.85 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19222250	        77.40 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/99%-reads-12           	19358870	        76.94 ns/op	       0 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	26985049	        42.62 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	24723876	        42.98 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	24406772	        43.16 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	24713092	        42.90 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/90%-reads-12           	19498702	        64.99 ns/op	       2 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18889095	        56.81 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	17939518	        57.46 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	17723084	        57.91 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18845682	        57.28 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/75%-reads-12           	18738049	        56.94 ns/op	       5 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17805460	        58.51 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17693041	        58.31 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17675780	        58.57 ns/op	      10 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17662780	        58.33 ns/op	       9 B/op	       0 allocs/op
BenchmarkMap_WarmUp/50%-reads-12           	17713188	        58.52 ns/op	       9 B/op	       0 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	16398828	        65.44 ns/op	      19 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15915253	        65.64 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15707212	        66.04 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15839439	        65.50 ns/op	      20 B/op	       1 allocs/op
BenchmarkMap_WarmUp/0%-reads-12            	15680858	        65.64 ns/op	      20 B/op	       1 allocs/op
PASS
ok  	github.com/puzpuzpuz/xsync	207.723s

```
</details>


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6494e44..d77073f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,13 +6,14 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go-version: [1.16.x, 1.17.x]
+        go-version: [1.16.x, 1.17.x, 1.18.0-rc1]
     name: Build with Go ${{ matrix.go-version }}
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Install Go
         uses: actions/setup-go@v2
         with:
+          stable: false
           go-version: ${{ matrix.go-version }}
 
       - name: Install golint
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 9a7af88..bb2f0b0 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -6,13 +6,14 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go-version: [1.16.x]
+        go-version: [1.18.0-rc1]
     name: Build with Go ${{ matrix.go-version }}
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Install Go
         uses: actions/setup-go@v2
         with:
+          stable: false
           go-version: ${{ matrix.go-version }}
 
       - name: Run coverage
diff --git a/README.md b/README.md
index 2d99422..7e11c60 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,14 @@ CLHT is built around idea to organize the hash table in cache-line-sized buckets
 
 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.
 
+`MapOf[V]` is an implementation with parametrized value type. It is available for Go 1.18 or later.
+
+```go
+m := xsync.NewMapOf[string]()
+m.Store("foo", "bar")
+v, ok := m.Load("foo")
+```
+
 ## MPMCQueue
 
 A `MPMCQeueue` is a bounded multi-producer multi-consumer concurrent queue.
diff --git a/go.mod b/go.mod
index 804880c..262ba39 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
 module github.com/puzpuzpuz/xsync
 
-go 1.16
+go 1.18
diff --git a/mapof.go b/mapof.go
new file mode 100644
index 0000000..6a5a459
--- /dev/null
+++ b/mapof.go
@@ -0,0 +1,368 @@
+//go:build go1.18
+// +build go1.18
+
+package xsync
+
+import (
+	"fmt"
+	"math"
+	"sync"
+	"sync/atomic"
+	"unsafe"
+)
+
+// MapOf is like a Go map[string]V but is safe for concurrent
+// use by multiple goroutines without additional locking or
+// coordination. It follows the interface of sync.Map.
+//
+// A MapOf must not be copied after first use.
+//
+// MapOf 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 at most one cache-line transfer.
+// Also, Get operations involve no write to memory, as well as no
+// mutexes or any other sort of locks. Due to this design, in all
+// considered scenarios MapOf 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.
+//
+// Also note that, unlike in sync.Map, the underlying hash table used
+// by MapOf never shrinks and only grows on demand. However, this
+// should not be an issue in many cases since updates happen in-place
+// leaving no tombstone entries.
+type MapOf[V any] struct {
+	table        unsafe.Pointer // *mapTable
+	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)
+	totalGrowths int64
+	totalShrinks int64
+}
+
+// NewMapOf creates a new MapOf instance.
+func NewMapOf[V any]() *MapOf[V] {
+	m := &MapOf[V]{}
+	m.resizeCond = *sync.NewCond(&m.resizeMu)
+	table := newMapTable(minMapTableLen)
+	atomic.StorePointer(&m.table, unsafe.Pointer(table))
+	return m
+}
+
+// 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)
+	table := (*mapTable)(atomic.LoadPointer(&m.table))
+	bidx := bucketIdx(table, hash)
+	b := &table.buckets[bidx]
+	topHashes := atomic.LoadUint64(&b.topHashes)
+	for i := 0; i < entriesPerMapBucket; i++ {
+		if !topHashMatch(hash, topHashes, i) {
+			continue
+		}
+	atomic_snapshot:
+		// Start atomic snapshot.
+		vp := atomic.LoadPointer(&b.values[i])
+		kp := atomic.LoadPointer(&b.keys[i])
+		if kp != nil && vp != nil {
+			if key == derefKey(kp) {
+				if uintptr(vp) == uintptr(atomic.LoadPointer(&b.values[i])) {
+					// Atomic snapshot succeeded.
+					return derefTypedValue[V](vp), true
+				}
+				// Concurrent update/remove. Go for another spin.
+				goto atomic_snapshot
+			}
+		}
+	}
+	return value, false
+}
+
+// Store sets the value for a key.
+func (m *MapOf[V]) Store(key string, 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) {
+	return m.doStore(key, value, true)
+}
+
+func (m *MapOf[V]) doStore(key string, value V, loadIfExists bool) (actual V, loaded bool) {
+	// Read-only path.
+	if loadIfExists {
+		if v, ok := m.Load(key); ok {
+			return v, true
+		}
+	}
+	// Write path.
+	hash := maphash64(key)
+	for {
+		var (
+			emptykp, emptyvp *unsafe.Pointer
+			emptyidx         int
+		)
+		table := (*mapTable)(atomic.LoadPointer(&m.table))
+		bidx := bucketIdx(table, hash)
+		b := &table.buckets[bidx]
+		b.mu.Lock()
+		if m.newerTableExists(table) {
+			// Someone resized the table. Go for another attempt.
+			b.mu.Unlock()
+			continue
+		}
+		if m.resizeInProgress() {
+			// Resize is in progress. Wait, then go for another attempt.
+			b.mu.Unlock()
+			m.waitForResize()
+			continue
+		}
+		for i := 0; i < entriesPerMapBucket; i++ {
+			if b.keys[i] == nil {
+				if emptykp == nil {
+					emptykp = &b.keys[i]
+					emptyvp = &b.values[i]
+					emptyidx = i
+				}
+				continue
+			}
+			if !topHashMatch(hash, b.topHashes, i) {
+				continue
+			}
+			if key == derefKey(b.keys[i]) {
+				if loadIfExists {
+					vp := b.values[i]
+					b.mu.Unlock()
+					return derefTypedValue[V](vp), true
+				}
+				// 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.
+				atomic.StorePointer(&b.values[i], unsafe.Pointer(&value))
+				b.mu.Unlock()
+				return
+			}
+		}
+		if emptykp != nil {
+			// Insertion case. First we update the value, then the key.
+			// This is important for atomic snapshot states.
+			atomic.StoreUint64(&b.topHashes, storeTopHash(hash, b.topHashes, emptyidx))
+			atomic.StorePointer(emptyvp, unsafe.Pointer(&value))
+			atomic.StorePointer(emptykp, unsafe.Pointer(&key))
+			b.mu.Unlock()
+			addSize(table, bidx, 1)
+			return
+		}
+		// Need to grow the table. Then go for another attempt.
+		b.mu.Unlock()
+		m.resize(table, mapGrowHint)
+	}
+}
+
+func (m *MapOf[V]) newerTableExists(table *mapTable) bool {
+	curTablePtr := atomic.LoadPointer(&m.table)
+	return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table))
+}
+
+func (m *MapOf[V]) resizeInProgress() bool {
+	return atomic.LoadInt64(&m.resizing) == 1
+}
+
+func (m *MapOf[V]) waitForResize() {
+	m.resizeMu.Lock()
+	for m.resizeInProgress() {
+		m.resizeCond.Wait()
+	}
+	m.resizeMu.Unlock()
+}
+
+func (m *MapOf[V]) resize(table *mapTable, hint mapResizeHint) {
+	var shrinkThreshold int64
+	tableLen := len(table.buckets)
+	// Fast path for shrink attempts.
+	if hint == mapShrinkHint {
+		shrinkThreshold = int64((tableLen * entriesPerMapBucket) / mapShrinkFraction)
+		if tableLen == minMapTableLen || sumSize(table) > shrinkThreshold {
+			return
+		}
+	}
+	// Slow path.
+	if !atomic.CompareAndSwapInt64(&m.resizing, 0, 1) {
+		// Someone else started resize. Wait for it to finish.
+		m.waitForResize()
+		return
+	}
+	var newTable *mapTable
+	switch hint {
+	case mapGrowHint:
+		// Grow the table with factor of 2.
+		atomic.AddInt64(&m.totalGrowths, 1)
+		newTable = newMapTable(tableLen << 1)
+	case mapShrinkHint:
+		if sumSize(table) <= shrinkThreshold {
+			// Shrink the table with factor of 2.
+			atomic.AddInt64(&m.totalShrinks, 1)
+			newTable = newMapTable(tableLen >> 1)
+		} else {
+			// No need to shrink. Wake up all waiters and give up.
+			m.resizeMu.Lock()
+			atomic.StoreInt64(&m.resizing, 0)
+			m.resizeCond.Broadcast()
+			m.resizeMu.Unlock()
+			return
+		}
+	default:
+		panic(fmt.Sprintf("unexpected resize hint: %d", hint))
+	}
+copy:
+	for i := 0; i < tableLen; i++ {
+		copied, ok := copyBucket(&table.buckets[i], newTable)
+		if !ok {
+			// Table size is insufficient, need to grow it.
+			newTable = newMapTable(len(newTable.buckets) << 1)
+			goto copy
+		}
+		addSizeNonAtomic(newTable, uint64(i), copied)
+	}
+	// Publish the new table and wake up all waiters.
+	atomic.StorePointer(&m.table, unsafe.Pointer(newTable))
+	m.resizeMu.Lock()
+	atomic.StoreInt64(&m.resizing, 0)
+	m.resizeCond.Broadcast()
+	m.resizeMu.Unlock()
+}
+
+// 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)
+delete_attempt:
+	hintNonEmpty := 0
+	table := (*mapTable)(atomic.LoadPointer(&m.table))
+	bidx := bucketIdx(table, hash)
+	b := &table.buckets[bidx]
+	b.mu.Lock()
+	if m.newerTableExists(table) {
+		// Someone resized the table. Go for another attempt.
+		b.mu.Unlock()
+		goto delete_attempt
+	}
+	if m.resizeInProgress() {
+		// Resize is in progress. Wait, then go for another attempt.
+		b.mu.Unlock()
+		m.waitForResize()
+		goto delete_attempt
+	}
+	for i := 0; i < entriesPerMapBucket; i++ {
+		kp := b.keys[i]
+		if kp == nil || !topHashMatch(hash, b.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.topHashes, eraseTopHash(b.topHashes, i))
+			atomic.StorePointer(&b.values[i], nil)
+			atomic.StorePointer(&b.keys[i], nil)
+			leftEmpty := false
+			if hintNonEmpty == 0 {
+				leftEmpty = isEmpty(b)
+			}
+			b.mu.Unlock()
+			addSize(table, bidx, -1)
+			// Might need to shrink the table.
+			if leftEmpty {
+				m.resize(table, mapShrinkHint)
+			}
+			return derefTypedValue[V](vp), true
+		}
+		hintNonEmpty++
+	}
+	b.mu.Unlock()
+	return value, false
+}
+
+// Delete deletes the value for a key.
+func (m *MapOf[V]) Delete(key string) {
+	m.LoadAndDelete(key)
+}
+
+// Range calls f sequentially for each key and value present in the
+// map. If f returns false, range stops the iteration.
+//
+// Range does not necessarily correspond to any consistent snapshot
+// of the Map's contents: no key will be visited more than once, but
+// if the value for any key is stored or deleted concurrently, Range
+// may reflect any mapping for that key from any point during the
+// Range call.
+//
+// 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) {
+	tablep := atomic.LoadPointer(&m.table)
+	table := *(*mapTable)(tablep)
+	bentries := make([]rangeEntry, 0, entriesPerMapBucket)
+	for i := range table.buckets {
+		copyRangeEntries(&table.buckets[i], &bentries)
+		for j := range bentries {
+			k := derefKey(bentries[j].key)
+			v := derefTypedValue[V](bentries[j].value)
+			if !f(k, v) {
+				return
+			}
+		}
+	}
+}
+
+// used in tests to verify the table counter
+func (m *MapOf[V]) size() int {
+	table := (*mapTable)(atomic.LoadPointer(&m.table))
+	return int(sumSize(table))
+}
+
+func derefTypedValue[V any](valuePtr unsafe.Pointer) (val V) {
+	return *(*V)(valuePtr)
+}
+
+// O(N) operation; use for debug purposes only
+func (m *MapOf[V]) stats() mapStats {
+	stats := mapStats{
+		TotalGrowths: atomic.LoadInt64(&m.totalGrowths),
+		TotalShrinks: atomic.LoadInt64(&m.totalShrinks),
+		MinEntries:   math.MaxInt32,
+	}
+	table := (*mapTable)(atomic.LoadPointer(&m.table))
+	stats.TableLen = len(table.buckets)
+	stats.Counter = int(sumSize(table))
+	stats.CounterLen = len(table.size)
+	for i := range table.buckets {
+		numEntries := 0
+		stats.Capacity += entriesPerMapBucket
+		b := &table.buckets[i]
+		for i := 0; i < entriesPerMapBucket; i++ {
+			if atomic.LoadPointer(&b.keys[i]) != nil {
+				stats.Size++
+				numEntries++
+			}
+		}
+		if numEntries < stats.MinEntries {
+			stats.MinEntries = numEntries
+		}
+		if numEntries > stats.MaxEntries {
+			stats.MaxEntries = numEntries
+		}
+	}
+	return stats
+}


Test patch:
diff --git a/export_mapof_test.go b/export_mapof_test.go
new file mode 100644
index 0000000..a0c5827
--- /dev/null
+++ b/export_mapof_test.go
@@ -0,0 +1,12 @@
+//go:build go1.18
+// +build go1.18
+
+package xsync
+
+func CollectMapOfStats[V any](m *MapOf[V]) MapStats {
+	return MapStats{m.stats()}
+}
+
+func MapOfSize[V any](m *MapOf[V]) int {
+	return m.size()
+}
diff --git a/mapof_test.go b/mapof_test.go
new file mode 100644
index 0000000..17d4b6c
--- /dev/null
+++ b/mapof_test.go
@@ -0,0 +1,471 @@
+//go:build go1.18
+// +build go1.18
+
+package xsync_test
+
+import (
+	. "github.com/puzpuzpuz/xsync"
+	"math/rand"
+	"strconv"
+	"testing"
+	"time"
+)
+
+func TestMapOf_MissingEntry(t *testing.T) {
+	m := NewMapOf[string]()
+	v, ok := m.Load("foo")
+	if ok {
+		t.Errorf("value was not expected: %v", v)
+	}
+	if deleted, loaded := m.LoadAndDelete("foo"); loaded {
+		t.Errorf("value was not expected %v", deleted)
+	}
+	if actual, loaded := m.LoadOrStore("foo", "bar"); loaded {
+		t.Errorf("value was not expected %v", actual)
+	}
+}
+
+func TestMapOf_EmptyStringKey(t *testing.T) {
+	m := NewMapOf[string]()
+	m.Store("", "foobar")
+	v, ok := m.Load("")
+	if !ok {
+		t.Error("value was expected")
+	}
+	if v != "foobar" {
+		t.Errorf("value does not match: %v", v)
+	}
+}
+
+func TestMapOfStore_NilValue(t *testing.T) {
+	m := NewMapOf[*struct{}]()
+	m.Store("foo", nil)
+	v, ok := m.Load("foo")
+	if !ok {
+		t.Error("nil value was expected")
+	}
+	if v != nil {
+		t.Errorf("value was not nil: %v", v)
+	}
+}
+
+func TestMapOfLoadOrStore_NilValue(t *testing.T) {
+	m := NewMapOf[*struct{}]()
+	m.LoadOrStore("foo", nil)
+	v, loaded := m.LoadOrStore("foo", nil)
+	if !loaded {
+		t.Error("nil value was expected")
+	}
+	if v != nil {
+		t.Errorf("value was not nil: %v", v)
+	}
+}
+
+func TestMapOfRange(t *testing.T) {
+	const numEntries = 1000
+	m := NewMapOf[int]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	iters := 0
+	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)
+			return false
+		}
+		met[key] += 1
+		iters++
+		return true
+	})
+	if iters != numEntries {
+		t.Errorf("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)
+		}
+	}
+}
+
+func TestMapOfRange_FalseReturned(t *testing.T) {
+	m := NewMapOf[int]()
+	for i := 0; i < 100; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	iters := 0
+	m.Range(func(key string, value int) bool {
+		iters++
+		return iters != 13
+	})
+	if iters != 13 {
+		t.Errorf("got unexpected number of iterations: %d", iters)
+	}
+}
+
+func TestMapOfRange_NestedDelete(t *testing.T) {
+	const numEntries = 256
+	m := NewMapOf[int]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	m.Range(func(key string, value int) bool {
+		m.Delete(key)
+		return true
+	})
+	for i := 0; i < numEntries; i++ {
+		if _, ok := m.Load(strconv.Itoa(i)); ok {
+			t.Errorf("value found for %d", i)
+		}
+	}
+}
+
+func TestMapOfSerialStore(t *testing.T) {
+	const numEntries = 128
+	m := NewMapOf[int]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(strconv.Itoa(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]()
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	for i := 0; i < numEntries; i++ {
+		if _, loaded := m.LoadOrStore(strconv.Itoa(i), i); !loaded {
+			t.Errorf("value not found for %d", i)
+		}
+	}
+}
+
+func TestMapOfSerialStoreThenDelete(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++ {
+		m.Delete(strconv.Itoa(i))
+		if _, ok := m.Load(strconv.Itoa(i)); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
+func TestMapOfSerialStoreThenLoadAndDelete(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 _, ok := m.Load(strconv.Itoa(i)); ok {
+			t.Errorf("value was not expected for %d", i)
+		}
+	}
+}
+
+func TestMapOfSize(t *testing.T) {
+	const numEntries = 1000
+	m := NewMapOf[int]()
+	size := MapOfSize(m)
+	if size != 0 {
+		t.Errorf("zero size expected: %d", size)
+	}
+	expectedSize := 0
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+		expectedSize++
+		size := MapOfSize(m)
+		if size != expectedSize {
+			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+		}
+	}
+	for i := 0; i < numEntries; i++ {
+		m.Delete(strconv.Itoa(i))
+		expectedSize--
+		size := MapOfSize(m)
+		if size != expectedSize {
+			t.Errorf("size of %d was expected, got: %d", expectedSize, size)
+		}
+	}
+}
+
+func TestMapOfResize(t *testing.T) {
+	const numEntries = 100_000
+	m := NewMapOf[int]()
+
+	for i := 0; i < numEntries; i++ {
+		m.Store(strconv.Itoa(i), i)
+	}
+	stats := CollectMapOfStats(m)
+	if stats.Size != numEntries {
+		t.Errorf("size was too small: %d", stats.Size)
+	}
+	expectedCapacity := stats.TableLen * EntriesPerMapBucket
+	if stats.Capacity > expectedCapacity {
+		t.Errorf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
+	}
+	if stats.TableLen <= MinMapTableLen {
+		t.Errorf("table was too small: %d", stats.TableLen)
+	}
+	if stats.TotalGrowths == 0 {
+		t.Errorf("non-zero total growths expected: %d", stats.TotalGrowths)
+	}
+	if stats.TotalShrinks > 0 {
+		t.Errorf("zero total shrinks expected: %d", stats.TotalShrinks)
+	}
+	// This is useful when debugging table resize and occupancy.
+	stats.Print()
+
+	for i := 0; i < numEntries; i++ {
+		m.Delete(strconv.Itoa(i))
+	}
+	stats = CollectMapOfStats(m)
+	if stats.Size > 0 {
+		t.Errorf("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)
+	}
+	if stats.TableLen != MinMapTableLen {
+		t.Errorf("table was too large: %d", stats.TableLen)
+	}
+	if stats.TotalShrinks == 0 {
+		t.Errorf("non-zero total shrinks expected: %d", stats.TotalShrinks)
+	}
+	stats.Print()
+}
+
+func TestMapOfResize_CounterLenLimit(t *testing.T) {
+	const numEntries = 1_000_000
+	m := NewMapOf[string]()
+
+	for i := 0; i < numEntries; i++ {
+		m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i))
+	}
+	stats := CollectMapOfStats(m)
+	if stats.Size != numEntries {
+		t.Errorf("size was too small: %d", stats.Size)
+	}
+	if stats.CounterLen != MaxMapCounterLen {
+		t.Errorf("number of counter stripes was too large: %d, expected: %d",
+			stats.CounterLen, MaxMapCounterLen)
+	}
+}
+
+func parallelSeqTypedStorer(t *testing.T, m *MapOf[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 {
+				m.Store(strconv.Itoa(j), j)
+				// Due to atomic snapshots we must see a "<j>"/j pair.
+				v, ok := m.Load(strconv.Itoa(j))
+				if !ok {
+					t.Errorf("value was not found for %d", j)
+					break
+				}
+				if v != j {
+					t.Errorf("value was not expected for %d: %d", j, v)
+					break
+				}
+			}
+		}
+	}
+	cdone <- true
+}
+
+func TestMapOfParallelStores(t *testing.T) {
+	const numStorers = 4
+	const numIters = 10_000
+	const numEntries = 100
+	m := NewMapOf[int]()
+	cdone := make(chan bool)
+	for i := 0; i < numStorers; i++ {
+		go parallelSeqTypedStorer(t, m, i, numIters, numEntries, cdone)
+	}
+	// Wait for the goroutines to finish.
+	for i := 0; i < numStorers; i++ {
+		<-cdone
+	}
+	// Verify map contents.
+	for i := 0; i < numEntries; i++ {
+		v, ok := m.Load(strconv.Itoa(i))
+		if !ok {
+			t.Fatalf("value not found for %d", i)
+		}
+		if v != i {
+			t.Fatalf("values do not match for %d: %v", i, v)
+		}
+	}
+}
+
+func parallelRandTypedStorer(t *testing.T, m *MapOf[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)
+		if v, loaded := m.LoadOrStore(strconv.Itoa(j), j); loaded {
+			if v != j {
+				t.Errorf("value was not expected for %d: %d", j, v)
+			}
+		}
+	}
+	cdone <- true
+}
+
+func parallelRandTypedDeleter(t *testing.T, m *MapOf[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)
+		if v, loaded := m.LoadAndDelete(strconv.Itoa(j)); loaded {
+			if v != j {
+				t.Errorf("value was not expected for %d: %d", j, v)
+			}
+		}
+	}
+	cdone <- true
+}
+
+func parallelTypedLoader(t *testing.T, m *MapOf[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.
+			if v, ok := m.Load(strconv.Itoa(j)); ok {
+				if v != j {
+					t.Errorf("value was not expected for %d: %d", j, v)
+				}
+			}
+		}
+	}
+	cdone <- true
+}
+
+func TestMapOfAtomicSnapshot(t *testing.T) {
+	const numIters = 100_000
+	const numEntries = 100
+	m := NewMapOf[int]()
+	cdone := make(chan bool)
+	// Update or delete random entry in parallel with loads.
+	go parallelRandTypedStorer(t, m, numIters, numEntries, cdone)
+	go parallelRandTypedDeleter(t, m, numIters, numEntries, cdone)
+	go parallelTypedLoader(t, m, numIters, numEntries, cdone)
+	// Wait for the goroutines to finish.
+	for i := 0; i < 3; i++ {
+		<-cdone
+	}
+}
+
+func TestMapOfParallelStoresAndDeletes(t *testing.T) {
+	const numWorkers = 2
+	const numIters = 100_000
+	const numEntries = 1000
+	m := NewMapOf[int]()
+	cdone := make(chan bool)
+	// Update random entry in parallel with deletes.
+	for i := 0; i < numWorkers; i++ {
+		go parallelRandTypedStorer(t, m, numIters, numEntries, cdone)
+		go parallelRandTypedDeleter(t, m, numIters, numEntries, cdone)
+	}
+	// Wait for the goroutines to finish.
+	for i := 0; i < 2*numWorkers; i++ {
+		<-cdone
+	}
+}
+
+func BenchmarkMapOf_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 := NewMapOf[int]()
+			benchmarkMapInt(b, func(k string) (int, bool) {
+				return m.Load(k)
+			}, func(k string, v int) {
+				m.Store(k, v)
+			}, func(k string) {
+				m.Delete(k)
+			}, bc.readPercentage)
+		})
+	}
+}
+
+func BenchmarkMapOf_WarmUp(b *testing.B) {
+	for _, bc := range benchmarkCases {
+		b.Run(bc.name, func(b *testing.B) {
+			m := NewMapOf[int]()
+			for i := 0; i < benchmarkNumEntries; i++ {
+				m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i)
+			}
+			benchmarkMapInt(b, func(k string) (int, bool) {
+				return m.Load(k)
+			}, func(k string, v int) {
+				m.Store(k, v)
+			}, func(k string) {
+				m.Delete(k)
+			}, bc.readPercentage)
+		})
+	}
+}
+
+func benchmarkMapInt(
+	b *testing.B,
+	loadFn func(k string) (int, bool),
+	storeFn func(k string, v int),
+	deleteFn func(k string),
+	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(benchmarkKeys[i])
+			} else if op >= storeThreshold {
+				storeFn(benchmarkKeys[i], i)
+			} else {
+				loadFn(benchmarkKeys[i])
+			}
+		}
+	})
+}
+
+func BenchmarkMapOfRange(b *testing.B) {
+	m := NewMapOf[int]()
+	for i := 0; i < benchmarkNumEntries; i++ {
+		m.Store(benchmarkKeys[i], i)
+	}
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		foo := 0
+		for pb.Next() {
+			m.Range(func(key string, value int) bool {
+				foo++
+				return true
+			})
+			_ = foo
+		}
+	})
+}