From e49f9b9ea43385d7c4cfec93f2d45202111c8bc1 Mon Sep 17 00:00:00 2001 From: Fufu Date: Mon, 8 Jul 2024 10:23:32 +0800 Subject: [PATCH] refactor: move xsync package to github.com/fufuok/xsync@dev --- README.md | 10 +- sched/examples/main.go | 8 +- xsync/BENCHMARKS.md | 131 ---- xsync/DOC.md | 779 ------------------- xsync/LICENSE | 21 - xsync/README.md | 149 ---- xsync/counter.go | 99 --- xsync/counter_test.go | 123 --- xsync/example_test.go | 74 -- xsync/export_mapof_test.go | 23 - xsync/export_test.go | 65 -- xsync/map.go | 843 -------------------- xsync/map_test.go | 1362 --------------------------------- xsync/mapof.go | 687 ----------------- xsync/mapof_helper.go | 72 -- xsync/mapof_test.go | 1320 -------------------------------- xsync/mpmcqueue.go | 137 ---- xsync/mpmcqueue_test.go | 327 -------- xsync/mpmcqueueof.go | 150 ---- xsync/mpmcqueueof_test.go | 326 -------- xsync/rbmutex.go | 145 ---- xsync/rbmutex_test.go | 219 ------ xsync/util.go | 46 -- xsync/util_hash.go | 56 -- xsync/util_hash_mapof.go | 30 - xsync/util_hash_mapof_test.go | 186 ----- xsync/util_hash_test.go | 33 - xsync/util_test.go | 53 -- 28 files changed, 10 insertions(+), 7464 deletions(-) delete mode 100644 xsync/BENCHMARKS.md delete mode 100644 xsync/DOC.md delete mode 100644 xsync/LICENSE delete mode 100644 xsync/README.md delete mode 100644 xsync/counter.go delete mode 100644 xsync/counter_test.go delete mode 100644 xsync/example_test.go delete mode 100644 xsync/export_mapof_test.go delete mode 100644 xsync/export_test.go delete mode 100644 xsync/map.go delete mode 100644 xsync/map_test.go delete mode 100644 xsync/mapof.go delete mode 100644 xsync/mapof_helper.go delete mode 100644 xsync/mapof_test.go delete mode 100644 xsync/mpmcqueue.go delete mode 100644 xsync/mpmcqueue_test.go delete mode 100644 xsync/mpmcqueueof.go delete mode 100644 xsync/mpmcqueueof_test.go delete mode 100644 xsync/rbmutex.go delete mode 100644 xsync/rbmutex_test.go delete mode 100644 xsync/util.go delete mode 100644 xsync/util_hash.go delete mode 100644 xsync/util_hash_mapof.go delete mode 100644 xsync/util_hash_mapof_test.go delete mode 100644 xsync/util_hash_test.go delete mode 100644 xsync/util_test.go diff --git a/README.md b/README.md index 5eeedaa..f1de58c 100644 --- a/README.md +++ b/README.md @@ -673,6 +673,8 @@ type Daemon struct{ ... } **官方版本: `v3.0.0` 已统一了调用方法并内置了 hasher 生成器, 不再需要之前的改动, 直接使用官方原版就好** +**如果要在 go1.18 以下使用, 可以使用: github.com/fufuok/xsync@v1.3.1** +
DOC @@ -1001,6 +1003,7 @@ package main import ( "encoding/json" "fmt" + "sync/atomic" "time" "github.com/fufuok/utils" @@ -1014,7 +1017,6 @@ import ( "github.com/fufuok/utils/xhash" "github.com/fufuok/utils/xid" "github.com/fufuok/utils/xjson/jsongen" - "github.com/fufuok/utils/xsync" ) func main() { @@ -1186,16 +1188,16 @@ func main() { now = utils.WaitNextSecondWithTime() fmt.Println("hour:minute:second.00*ms", now) - count := xsync.NewCounter() + count := int64(0) bus := sched.New() // 默认并发数: runtime.NumCPU() for i := 0; i < 30; i++ { bus.Add(1) bus.RunWithArgs(func(n ...interface{}) { - count.Add(int64(n[0].(int))) + atomic.AddInt64(&count, int64(n[0].(int))) }, i) } bus.Wait() - fmt.Println("count:", count.Value()) // count: 435 + fmt.Println("count:", atomic.LoadInt64(&count)) // count: 435 // 继续下一批任务 bus.Add(1) diff --git a/sched/examples/main.go b/sched/examples/main.go index f9a1a67..c3ca453 100644 --- a/sched/examples/main.go +++ b/sched/examples/main.go @@ -2,23 +2,23 @@ package main import ( "fmt" + "sync/atomic" "time" "github.com/fufuok/utils/sched" - "github.com/fufuok/utils/xsync" ) func main() { - count := xsync.NewCounter() + count := int64(0) bus := sched.New() // 默认并发数: runtime.NumCPU() for i := 0; i < 30; i++ { bus.Add(1) bus.RunWithArgs(func(n ...interface{}) { - count.Add(int64(n[0].(int))) + atomic.AddInt64(&count, int64(n[0].(int))) }, i) } bus.Wait() - fmt.Println("count:", count.Value()) // count: 435 + fmt.Println("count:", atomic.LoadInt64(&count)) // count: 435 // 继续下一批任务 bus.Add(1) diff --git a/xsync/BENCHMARKS.md b/xsync/BENCHMARKS.md deleted file mode 100644 index af72721..0000000 --- a/xsync/BENCHMARKS.md +++ /dev/null @@ -1,131 +0,0 @@ -# xsync benchmarks - -If you're interested in `MapOf` comparison with some of the popular concurrent hash maps written in Go, check [this](https://github.com/cornelk/hashmap/pull/70) and [this](https://github.com/alphadose/haxmap/pull/22) PRs. - -The below results were obtained for xsync v2.3.1 on a c6g.metal EC2 instance (64 CPU, 128GB RAM) running Linux and Go 1.19.3. I'd like to thank [@felixge](https://github.com/felixge) who kindly ran the benchmarks. - -The following commands were used to run the benchmarks: -```bash -$ go test -run='^$' -cpu=1,2,4,8,16,32,64 -bench . -count=30 -timeout=0 | tee bench.txt -$ benchstat bench.txt | tee benchstat.txt -``` - -The below sections contain some of the results. Refer to [this gist](https://gist.github.com/puzpuzpuz/e62e38e06feadecfdc823c0f941ece0b) for the complete output. - -### Counter vs. atomic int64 - -``` -name time/op -Counter 27.3ns ± 1% -Counter-2 27.2ns ±11% -Counter-4 15.3ns ± 8% -Counter-8 7.43ns ± 7% -Counter-16 3.70ns ±10% -Counter-32 1.77ns ± 3% -Counter-64 0.96ns ±10% -AtomicInt64 7.60ns ± 0% -AtomicInt64-2 12.6ns ±13% -AtomicInt64-4 13.5ns ±14% -AtomicInt64-8 12.7ns ± 9% -AtomicInt64-16 12.8ns ± 8% -AtomicInt64-32 13.0ns ± 6% -AtomicInt64-64 12.9ns ± 7% -``` - -Here `time/op` stands for average time spent on operation. If you divide `10^9` by the result in nanoseconds per operation, you'd get the throughput in operations per second. Thus, the ideal theoretical scalability of a concurrent data structure implies that the reported `time/op` decreases proportionally with the increased number of CPU cores. On the contrary, if the measured time per operation increases when run on more cores, it means performance degradation. - -### MapOf vs. sync.Map - -1,000 `[int, int]` entries with a warm-up, 100% Loads: -``` -IntegerMapOf_WarmUp/reads=100% 24.0ns ± 0% -IntegerMapOf_WarmUp/reads=100%-2 12.0ns ± 0% -IntegerMapOf_WarmUp/reads=100%-4 6.02ns ± 0% -IntegerMapOf_WarmUp/reads=100%-8 3.01ns ± 0% -IntegerMapOf_WarmUp/reads=100%-16 1.50ns ± 0% -IntegerMapOf_WarmUp/reads=100%-32 0.75ns ± 0% -IntegerMapOf_WarmUp/reads=100%-64 0.38ns ± 0% -IntegerMapStandard_WarmUp/reads=100% 55.3ns ± 0% -IntegerMapStandard_WarmUp/reads=100%-2 27.6ns ± 0% -IntegerMapStandard_WarmUp/reads=100%-4 16.1ns ± 3% -IntegerMapStandard_WarmUp/reads=100%-8 8.35ns ± 7% -IntegerMapStandard_WarmUp/reads=100%-16 4.24ns ± 7% -IntegerMapStandard_WarmUp/reads=100%-32 2.18ns ± 6% -IntegerMapStandard_WarmUp/reads=100%-64 1.11ns ± 3% -``` - -1,000 `[int, int]` entries with a warm-up, 99% Loads, 0.5% Stores, 0.5% Deletes: -``` -IntegerMapOf_WarmUp/reads=99% 31.0ns ± 0% -IntegerMapOf_WarmUp/reads=99%-2 16.4ns ± 1% -IntegerMapOf_WarmUp/reads=99%-4 8.42ns ± 0% -IntegerMapOf_WarmUp/reads=99%-8 4.41ns ± 0% -IntegerMapOf_WarmUp/reads=99%-16 2.38ns ± 2% -IntegerMapOf_WarmUp/reads=99%-32 1.37ns ± 4% -IntegerMapOf_WarmUp/reads=99%-64 0.85ns ± 2% -IntegerMapStandard_WarmUp/reads=99% 121ns ± 1% -IntegerMapStandard_WarmUp/reads=99%-2 109ns ± 3% -IntegerMapStandard_WarmUp/reads=99%-4 115ns ± 4% -IntegerMapStandard_WarmUp/reads=99%-8 114ns ± 2% -IntegerMapStandard_WarmUp/reads=99%-16 105ns ± 2% -IntegerMapStandard_WarmUp/reads=99%-32 97.0ns ± 3% -IntegerMapStandard_WarmUp/reads=99%-64 98.0ns ± 2% -``` - -1,000 `[int, int]` entries with a warm-up, 75% Loads, 12.5% Stores, 12.5% Deletes: -``` -IntegerMapOf_WarmUp/reads=75%-reads 46.2ns ± 1% -IntegerMapOf_WarmUp/reads=75%-reads-2 36.7ns ± 2% -IntegerMapOf_WarmUp/reads=75%-reads-4 22.0ns ± 1% -IntegerMapOf_WarmUp/reads=75%-reads-8 12.8ns ± 2% -IntegerMapOf_WarmUp/reads=75%-reads-16 7.69ns ± 1% -IntegerMapOf_WarmUp/reads=75%-reads-32 5.16ns ± 1% -IntegerMapOf_WarmUp/reads=75%-reads-64 4.91ns ± 1% -IntegerMapStandard_WarmUp/reads=75%-reads 156ns ± 0% -IntegerMapStandard_WarmUp/reads=75%-reads-2 177ns ± 1% -IntegerMapStandard_WarmUp/reads=75%-reads-4 197ns ± 1% -IntegerMapStandard_WarmUp/reads=75%-reads-8 221ns ± 2% -IntegerMapStandard_WarmUp/reads=75%-reads-16 242ns ± 1% -IntegerMapStandard_WarmUp/reads=75%-reads-32 258ns ± 1% -IntegerMapStandard_WarmUp/reads=75%-reads-64 264ns ± 1% -``` - -### MPMCQueue vs. Go channels - -Concurrent producers and consumers (1:1), queue/channel size 1,000, some work done by both producers and consumers: -``` -QueueProdConsWork100 252ns ± 0% -QueueProdConsWork100-2 206ns ± 5% -QueueProdConsWork100-4 136ns ±12% -QueueProdConsWork100-8 110ns ± 6% -QueueProdConsWork100-16 108ns ± 2% -QueueProdConsWork100-32 102ns ± 2% -QueueProdConsWork100-64 101ns ± 0% -ChanProdConsWork100 283ns ± 0% -ChanProdConsWork100-2 406ns ±21% -ChanProdConsWork100-4 549ns ± 7% -ChanProdConsWork100-8 754ns ± 7% -ChanProdConsWork100-16 828ns ± 7% -ChanProdConsWork100-32 810ns ± 8% -ChanProdConsWork100-64 832ns ± 4% -``` - -### RBMutex vs. sync.RWMutex - -The writer locks on each 100,000 iteration with some work in the critical section for both readers and the writer: -``` -RBMutexWorkWrite100000 146ns ± 0% -RBMutexWorkWrite100000-2 73.3ns ± 0% -RBMutexWorkWrite100000-4 36.7ns ± 0% -RBMutexWorkWrite100000-8 18.6ns ± 0% -RBMutexWorkWrite100000-16 9.83ns ± 3% -RBMutexWorkWrite100000-32 5.53ns ± 0% -RBMutexWorkWrite100000-64 4.04ns ± 3% -RWMutexWorkWrite100000 121ns ± 0% -RWMutexWorkWrite100000-2 128ns ± 1% -RWMutexWorkWrite100000-4 124ns ± 2% -RWMutexWorkWrite100000-8 101ns ± 1% -RWMutexWorkWrite100000-16 92.9ns ± 1% -RWMutexWorkWrite100000-32 89.9ns ± 1% -RWMutexWorkWrite100000-64 88.4ns ± 1% -``` diff --git a/xsync/DOC.md b/xsync/DOC.md deleted file mode 100644 index 3fcc8a2..0000000 --- a/xsync/DOC.md +++ /dev/null @@ -1,779 +0,0 @@ - - -# xsync - -```go -import "github.com/fufuok/utils/xsync" -``` - -## Index - -- [type Counter](<#Counter>) - - [func NewCounter\(\) \*Counter](<#NewCounter>) - - [func \(c \*Counter\) Add\(delta int64\)](<#Counter.Add>) - - [func \(c \*Counter\) Dec\(\)](<#Counter.Dec>) - - [func \(c \*Counter\) Inc\(\)](<#Counter.Inc>) - - [func \(c \*Counter\) Reset\(\)](<#Counter.Reset>) - - [func \(c \*Counter\) Value\(\) int64](<#Counter.Value>) -- [type HashMapOf](<#HashMapOf>) -- [type MPMCQueue](<#MPMCQueue>) - - [func NewMPMCQueue\(capacity int\) \*MPMCQueue](<#NewMPMCQueue>) - - [func \(q \*MPMCQueue\) Dequeue\(\) interface\{\}](<#MPMCQueue.Dequeue>) - - [func \(q \*MPMCQueue\) Enqueue\(item interface\{\}\)](<#MPMCQueue.Enqueue>) - - [func \(q \*MPMCQueue\) TryDequeue\(\) \(item interface\{\}, ok bool\)](<#MPMCQueue.TryDequeue>) - - [func \(q \*MPMCQueue\) TryEnqueue\(item interface\{\}\) bool](<#MPMCQueue.TryEnqueue>) -- [type MPMCQueueOf](<#MPMCQueueOf>) - - [func NewMPMCQueueOf\[I any\]\(capacity int\) \*MPMCQueueOf\[I\]](<#NewMPMCQueueOf>) - - [func \(q \*MPMCQueueOf\[I\]\) Dequeue\(\) I](<#MPMCQueueOf[I].Dequeue>) - - [func \(q \*MPMCQueueOf\[I\]\) Enqueue\(item I\)](<#MPMCQueueOf[I].Enqueue>) - - [func \(q \*MPMCQueueOf\[I\]\) TryDequeue\(\) \(item I, ok bool\)](<#MPMCQueueOf[I].TryDequeue>) - - [func \(q \*MPMCQueueOf\[I\]\) TryEnqueue\(item I\) bool](<#MPMCQueueOf[I].TryEnqueue>) -- [type Map](<#Map>) - - [func NewMap\(\) \*Map](<#NewMap>) - - [func NewMapPresized\(sizeHint int\) \*Map](<#NewMapPresized>) - - [func \(m \*Map\) Clear\(\)](<#Map.Clear>) - - [func \(m \*Map\) Compute\(key string, valueFn func\(oldValue interface\{\}, loaded bool\) \(newValue interface\{\}, delete bool\)\) \(actual interface\{\}, ok bool\)](<#Map.Compute>) - - [func \(m \*Map\) Delete\(key string\)](<#Map.Delete>) - - [func \(m \*Map\) Load\(key string\) \(value interface\{\}, ok bool\)](<#Map.Load>) - - [func \(m \*Map\) LoadAndDelete\(key string\) \(value interface\{\}, loaded bool\)](<#Map.LoadAndDelete>) - - [func \(m \*Map\) LoadAndStore\(key string, value interface\{\}\) \(actual interface\{\}, loaded bool\)](<#Map.LoadAndStore>) - - [func \(m \*Map\) LoadOrCompute\(key string, valueFn func\(\) interface\{\}\) \(actual interface\{\}, loaded bool\)](<#Map.LoadOrCompute>) - - [func \(m \*Map\) LoadOrStore\(key string, value interface\{\}\) \(actual interface\{\}, loaded bool\)](<#Map.LoadOrStore>) - - [func \(m \*Map\) Range\(f func\(key string, value interface\{\}\) bool\)](<#Map.Range>) - - [func \(m \*Map\) Size\(\) int](<#Map.Size>) - - [func \(m \*Map\) Store\(key string, value interface\{\}\)](<#Map.Store>) -- [type MapOf](<#MapOf>) - - [func NewMapOf\[K comparable, V any\]\(\) \*MapOf\[K, V\]](<#NewMapOf>) - - [func NewMapOfPresized\[K comparable, V any\]\(sizeHint int\) \*MapOf\[K, V\]](<#NewMapOfPresized>) - - [func \(m \*MapOf\[K, V\]\) Clear\(\)](<#MapOf[K, V].Clear>) - - [func \(m \*MapOf\[K, V\]\) Compute\(key K, valueFn func\(oldValue V, loaded bool\) \(newValue V, delete bool\)\) \(actual V, ok bool\)](<#MapOf[K, V].Compute>) - - [func \(m \*MapOf\[K, V\]\) Delete\(key K\)](<#MapOf[K, V].Delete>) - - [func \(m \*MapOf\[K, V\]\) Load\(key K\) \(value V, ok bool\)](<#MapOf[K, V].Load>) - - [func \(m \*MapOf\[K, V\]\) LoadAndDelete\(key K\) \(value V, loaded bool\)](<#MapOf[K, V].LoadAndDelete>) - - [func \(m \*MapOf\[K, V\]\) LoadAndStore\(key K, value V\) \(actual V, loaded bool\)](<#MapOf[K, V].LoadAndStore>) - - [func \(m \*MapOf\[K, V\]\) LoadOrCompute\(key K, valueFn func\(\) V\) \(actual V, loaded bool\)](<#MapOf[K, V].LoadOrCompute>) - - [func \(m \*MapOf\[K, V\]\) LoadOrStore\(key K, value V\) \(actual V, loaded bool\)](<#MapOf[K, V].LoadOrStore>) - - [func \(m \*MapOf\[K, V\]\) Range\(f func\(key K, value V\) bool\)](<#MapOf[K, V].Range>) - - [func \(m \*MapOf\[K, V\]\) Size\(\) int](<#MapOf[K, V].Size>) - - [func \(m \*MapOf\[K, V\]\) Store\(key K, value V\)](<#MapOf[K, V].Store>) -- [type RBMutex](<#RBMutex>) - - [func NewRBMutex\(\) \*RBMutex](<#NewRBMutex>) - - [func \(mu \*RBMutex\) Lock\(\)](<#RBMutex.Lock>) - - [func \(mu \*RBMutex\) RLock\(\) \*RToken](<#RBMutex.RLock>) - - [func \(mu \*RBMutex\) RUnlock\(t \*RToken\)](<#RBMutex.RUnlock>) - - [func \(mu \*RBMutex\) Unlock\(\)](<#RBMutex.Unlock>) -- [type RToken](<#RToken>) - - - -## type Counter - -A Counter is a striped int64 counter. - -Should be preferred over a single atomically updated int64 counter in high contention scenarios. - -A Counter must not be copied after first use. - -```go -type Counter struct { - // contains filtered or unexported fields -} -``` - - -### func NewCounter - -```go -func NewCounter() *Counter -``` - -NewCounter creates a new Counter instance. - - -### func \(\*Counter\) Add - -```go -func (c *Counter) Add(delta int64) -``` - -Add adds the delta to the counter. - - -### func \(\*Counter\) Dec - -```go -func (c *Counter) Dec() -``` - -Dec decrements the counter by 1. - - -### func \(\*Counter\) Inc - -```go -func (c *Counter) Inc() -``` - -Inc increments the counter by 1. - - -### func \(\*Counter\) Reset - -```go -func (c *Counter) Reset() -``` - -Reset resets the counter to zero. This method should only be used when it is known that there are no concurrent modifications of the counter. - - -### func \(\*Counter\) Value - -```go -func (c *Counter) Value() int64 -``` - -Value returns the current counter value. The returned value may not include all of the latest operations in presence of concurrent modifications of the counter. - - -## type HashMapOf - - - -```go -type HashMapOf[K comparable, V any] interface { - // 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. - Load(key K) (value V, ok bool) - - // Store sets the value for a key. - Store(key K, value V) - - // 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. - LoadOrStore(key K, value V) (actual V, loaded bool) - - // LoadAndStore returns the existing value for the key if present, - // while setting the new value for the key. - // It stores the new value and returns the existing one, if present. - // The loaded result is true if the existing value was loaded, - // false otherwise. - LoadAndStore(key K, value V) (actual V, loaded bool) - - // LoadOrCompute returns the existing value for the key if present. - // Otherwise, it computes the value using the provided function and - // returns the computed value. The loaded result is true if the value - // was loaded, false if stored. - LoadOrCompute(key K, valueFn func() V) (actual V, loaded 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. - Compute( - key K, - valueFn func(oldValue V, loaded bool) (newValue V, delete bool), - ) (actual V, ok bool) - - // LoadAndDelete deletes the value for a key, returning the previous - // value if any. The loaded result reports whether the key was - // present. - LoadAndDelete(key K) (value V, loaded bool) - - // Delete deletes the value for a key. - Delete(key K) - - // 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. - Range(f func(key K, value V) bool) - - // Clear deletes all keys and values currently stored in the map. - Clear() - - // Size returns current size of the map. - Size() int -} -``` - - -## type MPMCQueue - -A MPMCQueue is a bounded multi\-producer multi\-consumer concurrent queue. - -MPMCQueue instances must be created with NewMPMCQueue function. A MPMCQueue must not be copied after first use. - -Based on the data structure from the following C\+\+ library: https://github.com/rigtorp/MPMCQueue - -```go -type MPMCQueue struct { - // contains filtered or unexported fields -} -``` - - -### func NewMPMCQueue - -```go -func NewMPMCQueue(capacity int) *MPMCQueue -``` - -NewMPMCQueue creates a new MPMCQueue instance with the given capacity. - - -### func \(\*MPMCQueue\) Dequeue - -```go -func (q *MPMCQueue) Dequeue() interface{} -``` - -Dequeue retrieves and removes the item from the head of the queue. Blocks, if the queue is empty. - - -### func \(\*MPMCQueue\) Enqueue - -```go -func (q *MPMCQueue) Enqueue(item interface{}) -``` - -Enqueue inserts the given item into the queue. Blocks, if the queue is full. - - -### func \(\*MPMCQueue\) TryDequeue - -```go -func (q *MPMCQueue) TryDequeue() (item interface{}, ok bool) -``` - -TryDequeue retrieves and removes the item from the head of the queue. Does not block and returns immediately. The ok result indicates that the queue isn't empty and an item was retrieved. - - -### func \(\*MPMCQueue\) TryEnqueue - -```go -func (q *MPMCQueue) TryEnqueue(item interface{}) bool -``` - -TryEnqueue inserts the given item into the queue. Does not block and returns immediately. The result indicates that the queue isn't full and the item was inserted. - - -## type MPMCQueueOf - -A MPMCQueueOf is a bounded multi\-producer multi\-consumer concurrent queue. It's a generic version of MPMCQueue. - -MPMCQueue instances must be created with NewMPMCQueueOf function. A MPMCQueueOf must not be copied after first use. - -Based on the data structure from the following C\+\+ library: https://github.com/rigtorp/MPMCQueue - -```go -type MPMCQueueOf[I any] struct { - // contains filtered or unexported fields -} -``` - - -### func NewMPMCQueueOf - -```go -func NewMPMCQueueOf[I any](capacity int) *MPMCQueueOf[I] -``` - -NewMPMCQueueOf creates a new MPMCQueueOf instance with the given capacity. - - -### func \(\*MPMCQueueOf\[I\]\) Dequeue - -```go -func (q *MPMCQueueOf[I]) Dequeue() I -``` - -Dequeue retrieves and removes the item from the head of the queue. Blocks, if the queue is empty. - - -### func \(\*MPMCQueueOf\[I\]\) Enqueue - -```go -func (q *MPMCQueueOf[I]) Enqueue(item I) -``` - -Enqueue inserts the given item into the queue. Blocks, if the queue is full. - - -### func \(\*MPMCQueueOf\[I\]\) TryDequeue - -```go -func (q *MPMCQueueOf[I]) TryDequeue() (item I, ok bool) -``` - -TryDequeue retrieves and removes the item from the head of the queue. Does not block and returns immediately. The ok result indicates that the queue isn't empty and an item was retrieved. - - -### func \(\*MPMCQueueOf\[I\]\) TryEnqueue - -```go -func (q *MPMCQueueOf[I]) TryEnqueue(item I) bool -``` - -TryEnqueue inserts the given item into the queue. Does not block and returns immediately. The result indicates that the queue isn't full and the item was inserted. - - -## type Map - -Map is like a Go map\[string\]interface\{\} but is safe for concurrent use by multiple goroutines without additional locking or coordination. It follows the interface of sync.Map with a number of valuable extensions like Compute or Size. - -A Map must not be copied after first use. - -Map uses a modified version of Cache\-Line Hash Table \(CLHT\) data structure: https://github.com/LPD-EPFL/CLHT - -CLHT is built around idea to organize the hash table in cache\-line\-sized buckets, so that on all modern CPUs update operations complete with 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 Map outperforms sync.Map. - -One important difference with sync.Map is that only string keys are supported. That's because Golang standard library does not expose the built\-in hash functions for interface\{\} values. - -```go -type Map struct { - // contains filtered or unexported fields -} -``` - - -### func NewMap - -```go -func NewMap() *Map -``` - -NewMap creates a new Map instance. - - -### func NewMapPresized - -```go -func NewMapPresized(sizeHint int) *Map -``` - -NewMapPresized creates a new Map instance with capacity enough to hold sizeHint entries. The capacity is treated as the minimal capacity meaning that the underlying hash table will never shrink to a smaller capacity. If sizeHint is zero or negative, the value is ignored. - - -### func \(\*Map\) Clear - -```go -func (m *Map) Clear() -``` - -Clear deletes all keys and values currently stored in the map. - - -### func \(\*Map\) Compute - -```go -func (m *Map) Compute(key string, valueFn func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool)) (actual interface{}, ok 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. - -This call locks a hash table bucket while the compute function is executed. It means that modifications on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function includes long\-running operations. - - -### func \(\*Map\) Delete - -```go -func (m *Map) Delete(key string) -``` - -Delete deletes the value for a key. - - -### func \(\*Map\) Load - -```go -func (m *Map) Load(key string) (value interface{}, ok bool) -``` - -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 \(\*Map\) LoadAndDelete - -```go -func (m *Map) LoadAndDelete(key string) (value interface{}, loaded bool) -``` - -LoadAndDelete deletes the value for a key, returning the previous value if any. The loaded result reports whether the key was present. - - -### func \(\*Map\) LoadAndStore - -```go -func (m *Map) LoadAndStore(key string, value interface{}) (actual interface{}, loaded bool) -``` - -LoadAndStore returns the existing value for the key if present, while setting the new value for the key. It stores the new value and returns the existing one, if present. The loaded result is true if the existing value was loaded, false otherwise. - - -### func \(\*Map\) LoadOrCompute - -```go -func (m *Map) LoadOrCompute(key string, valueFn func() interface{}) (actual interface{}, loaded bool) -``` - -LoadOrCompute returns the existing value for the key if present. Otherwise, it computes the value using the provided function and returns the computed value. The loaded result is true if the value was loaded, false if stored. - -This call locks a hash table bucket while the compute function is executed. It means that modifications on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function includes long\-running operations. - - -### func \(\*Map\) LoadOrStore - -```go -func (m *Map) LoadOrStore(key string, value interface{}) (actual interface{}, loaded bool) -``` - -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 \(\*Map\) Range - -```go -func (m *Map) Range(f func(key string, value interface{}) bool) -``` - -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, including entry creation, modification and deletion. However, the concurrent modification rule apply, i.e. the changes may be not reflected in the subsequently iterated entries. - - -### func \(\*Map\) Size - -```go -func (m *Map) Size() int -``` - -Size returns current size of the map. - - -### func \(\*Map\) Store - -```go -func (m *Map) Store(key string, value interface{}) -``` - -Store sets the value for a key. - - -## type MapOf - -MapOf is like a Go map\[K\]V but is safe for concurrent use by multiple goroutines without additional locking or coordination. It follows the interface of sync.Map with a number of valuable extensions like Compute or Size. - -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. - -```go -type MapOf[K comparable, V any] struct { - // contains filtered or unexported fields -} -``` - - -### func NewMapOf - -```go -func NewMapOf[K comparable, V any]() *MapOf[K, V] -``` - -NewMapOf creates a new MapOf instance. - - -### func NewMapOfPresized - -```go -func NewMapOfPresized[K comparable, V any](sizeHint int) *MapOf[K, V] -``` - -NewMapOfPresized creates a new MapOf instance with capacity enough to hold sizeHint entries. The capacity is treated as the minimal capacity meaning that the underlying hash table will never shrink to a smaller capacity. If sizeHint is zero or negative, the value is ignored. - - -### func \(\*MapOf\[K, V\]\) Clear - -```go -func (m *MapOf[K, V]) Clear() -``` - -Clear deletes all keys and values currently stored in the map. - - -### func \(\*MapOf\[K, V\]\) Compute - -```go -func (m *MapOf[K, V]) Compute(key K, valueFn func(oldValue V, loaded bool) (newValue V, delete bool)) (actual V, ok 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. - -This call locks a hash table bucket while the compute function is executed. It means that modifications on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function includes long\-running operations. - -
Example -

- - - -```go -package main - -import ( - "errors" - "fmt" - - "github.com/fufuok/utils/xsync" -) - -func main() { - counts := xsync.NewMapOf[int, int]() - - // Store a new value. - v, ok := counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is false here. - newValue = 42 - delete = false - return - }) - // v: 42, ok: true - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // Update an existing value. - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is true here. - newValue = oldValue + 42 - delete = false - return - }) - // v: 84, ok: true - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // 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) { - oldVal = oldValue - if !loaded || oldValue < minVal { - newValue = minVal - delete = false - return - } - newValue = oldValue - delete = false - return - }) - // v: 84, ok: true, oldVal: 84 - fmt.Printf("v: %v, ok: %v, oldVal: %v\n", v, ok, oldVal) - - // Delete an existing value. - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is true here. - delete = true - return - }) - // v: 84, ok: false - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // Propagate an error from the compute function to the outer scope. - var err error - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - if oldValue == 42 { - err = errors.New("something went wrong") - return 0, true // no need to create a key/value pair - } - newValue = 0 - delete = false - return - }) - fmt.Printf("err: %v\n", err) -} -``` - -

-
- - -### func \(\*MapOf\[K, V\]\) Delete - -```go -func (m *MapOf[K, V]) Delete(key K) -``` - -Delete deletes the value for a key. - - -### func \(\*MapOf\[K, V\]\) Load - -```go -func (m *MapOf[K, V]) Load(key K) (value V, ok bool) -``` - -Load returns the value stored in the map for a key, or zero value of type V if no value is present. The ok result indicates whether value was found in the map. - - -### func \(\*MapOf\[K, V\]\) LoadAndDelete - -```go -func (m *MapOf[K, V]) LoadAndDelete(key K) (value V, loaded bool) -``` - -LoadAndDelete deletes the value for a key, returning the previous value if any. The loaded result reports whether the key was present. - - -### func \(\*MapOf\[K, V\]\) LoadAndStore - -```go -func (m *MapOf[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) -``` - -LoadAndStore returns the existing value for the key if present, while setting the new value for the key. It stores the new value and returns the existing one, if present. The loaded result is true if the existing value was loaded, false otherwise. - - -### func \(\*MapOf\[K, V\]\) LoadOrCompute - -```go -func (m *MapOf[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded bool) -``` - -LoadOrCompute returns the existing value for the key if present. Otherwise, it computes the value using the provided function and returns the computed value. The loaded result is true if the value was loaded, false if stored. - -This call locks a hash table bucket while the compute function is executed. It means that modifications on other entries in the bucket will be blocked until the valueFn executes. Consider this when the function includes long\-running operations. - - -### func \(\*MapOf\[K, V\]\) LoadOrStore - -```go -func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) -``` - -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 \(\*MapOf\[K, V\]\) Range - -```go -func (m *MapOf[K, V]) Range(f func(key K, value V) bool) -``` - -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, including entry creation, modification and deletion. However, the concurrent modification rule apply, i.e. the changes may be not reflected in the subsequently iterated entries. - - -### func \(\*MapOf\[K, V\]\) Size - -```go -func (m *MapOf[K, V]) Size() int -``` - -Size returns current size of the map. - - -### func \(\*MapOf\[K, V\]\) Store - -```go -func (m *MapOf[K, V]) Store(key K, value V) -``` - -Store sets the value for a key. - - -## type RBMutex - -A RBMutex is a reader biased reader/writer mutual exclusion lock. The lock can be held by an many readers or a single writer. The zero value for a RBMutex is an unlocked mutex. - -A RBMutex must not be copied after first use. - -RBMutex is based on a modified version of BRAVO \(Biased Locking for Reader\-Writer Locks\) algorithm: https://arxiv.org/pdf/1810.01553.pdf - -RBMutex is a specialized mutex for scenarios, such as caches, where the vast majority of locks are acquired by readers and write lock acquire attempts are infrequent. In such scenarios, RBMutex performs better than sync.RWMutex on large multicore machines. - -RBMutex extends sync.RWMutex internally and uses it as the "reader bias disabled" fallback, so the same semantics apply. The only noticeable difference is in reader tokens returned from the RLock/RUnlock methods. - -```go -type RBMutex struct { - // contains filtered or unexported fields -} -``` - - -### func NewRBMutex - -```go -func NewRBMutex() *RBMutex -``` - -NewRBMutex creates a new RBMutex instance. - - -### func \(\*RBMutex\) Lock - -```go -func (mu *RBMutex) Lock() -``` - -Lock locks m for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available. - - -### func \(\*RBMutex\) RLock - -```go -func (mu *RBMutex) RLock() *RToken -``` - -RLock locks m for reading and returns a reader token. The token must be used in the later RUnlock call. - -Should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. - - -### func \(\*RBMutex\) RUnlock - -```go -func (mu *RBMutex) RUnlock(t *RToken) -``` - -RUnlock undoes a single RLock call. A reader token obtained from the RLock call must be provided. RUnlock does not affect other simultaneous readers. A panic is raised if m is not locked for reading on entry to RUnlock. - - -### func \(\*RBMutex\) Unlock - -```go -func (mu *RBMutex) Unlock() -``` - -Unlock unlocks m for writing. A panic is raised if m is not locked for writing on entry to Unlock. - -As with RWMutex, a locked RBMutex is not associated with a particular goroutine. One goroutine may RLock \(Lock\) a RBMutex and then arrange for another goroutine to RUnlock \(Unlock\) it. - - -## type RToken - -RToken is a reader lock token. - -```go -type RToken struct { - // contains filtered or unexported fields -} -``` - -Generated by [gomarkdoc]() diff --git a/xsync/LICENSE b/xsync/LICENSE deleted file mode 100644 index 8376971..0000000 --- a/xsync/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Andrey Pechkurov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/xsync/README.md b/xsync/README.md deleted file mode 100644 index 7af99bc..0000000 --- a/xsync/README.md +++ /dev/null @@ -1,149 +0,0 @@ -# 标准库 `sync` 扩展包 - -*forked from puzpuzpuz/xsync v20240622 v3.2.0* - -## 改动: - -- ~~增加 `func NewHashMapOf[K comparable, V any](hasher ...func(K) uint64) HashMapOf[K, V]` 实现统一调用方法, 根据键类型使用 xxHash~~ -- 保留了对 go1.18 以下的支持 - -**官方版本: `v3.0.0` 已统一了调用方法并内置了 hasher 生成器, 不再需要上面的改动, 可以直接使用官方原版就好** - -[![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/puzpuzpuz/xsync/v3) -[![GoReport](https://goreportcard.com/badge/github.com/puzpuzpuz/xsync/v3)](https://goreportcard.com/report/github.com/puzpuzpuz/xsync/v3) -[![codecov](https://codecov.io/gh/puzpuzpuz/xsync/branch/main/graph/badge.svg)](https://codecov.io/gh/puzpuzpuz/xsync) - -# xsync - -Concurrent data structures for Go. Aims to provide more scalable alternatives for some of the data structures from the standard `sync` package, but not only. - -Covered with tests following the approach described [here](https://puzpuzpuz.dev/testing-concurrent-code-for-fun-and-profit). - -## Benchmarks - -Benchmark results may be found [here](BENCHMARKS.md). I'd like to thank [@felixge](https://github.com/felixge) who kindly ran the benchmarks on a beefy multicore machine. - -Also, a non-scientific, unfair benchmark comparing Java's [j.u.c.ConcurrentHashMap](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ConcurrentHashMap.html) and `xsync.MapOf` is available [here](https://puzpuzpuz.dev/concurrent-map-in-go-vs-java-yet-another-meaningless-benchmark). - -## Usage - -The latest xsync major version is v3, so `/v3` suffix should be used when importing the library: - -```go -import ( - "github.com/fufuok/utils/xsync" -) -``` - -*Note for v1 and v2 users*: v1 and v2 support is discontinued, so please upgrade to v3. While the API has some breaking changes, the migration should be trivial. - -### Counter - -A `Counter` is a striped `int64` counter inspired by the `j.u.c.a.LongAdder` class from the Java standard library. - -```go -c := xsync.NewCounter() -// increment and decrement the counter -c.Inc() -c.Dec() -// read the current value -v := c.Value() -``` - -Works better in comparison with a single atomically updated `int64` counter in high contention scenarios. - -### Map - -A `Map` is like a concurrent hash table-based map. It follows the interface of `sync.Map` with a number of valuable extensions like `Compute` or `Size`. - -```go -m := xsync.NewMap() -m.Store("foo", "bar") -v, ok := m.Load("foo") -s := m.Size() -``` - -`Map` uses a modified version of Cache-Line Hash Table (CLHT) data structure: https://github.com/LPD-EPFL/CLHT - -CLHT is built around the idea of organizing the hash table in cache-line-sized buckets, so that on all modern CPUs update operations complete with minimal cache-line transfer. Also, `Get` operations are obstruction-free and involve no writes to shared memory, hence no mutexes or any other sort of locks. Due to this design, in all considered scenarios `Map` outperforms `sync.Map`. - -One important difference with `sync.Map` is that only string keys are supported. That's because Golang standard library does not expose the built-in hash functions for `interface{}` values. - -`MapOf[K, V]` is an implementation with parametrized key and value types. While it's still a CLHT-inspired hash map, `MapOf`'s design is quite different from `Map`. As a result, less GC pressure and fewer atomic operations on reads. - -```go -m := xsync.NewMapOf[string, string]() -m.Store("foo", "bar") -v, ok := m.Load("foo") -``` - -One important difference with `Map` is that `MapOf` supports arbitrary `comparable` key types: - -```go -type Point struct { - x int32 - y int32 -} -m := NewMapOf[Point, int]() -m.Store(Point{42, 42}, 42) -v, ok := m.Load(point{42, 42}) -``` - -### MPMCQueue - -A `MPMCQueue` is a bounded multi-producer multi-consumer concurrent queue. - -```go -q := xsync.NewMPMCQueue(1024) -// producer inserts an item into the queue -q.Enqueue("foo") -// optimistic insertion attempt; doesn't block -inserted := q.TryEnqueue("bar") -// consumer obtains an item from the queue -item := q.Dequeue() // interface{} pointing to a string -// optimistic obtain attempt; doesn't block -item, ok := q.TryDequeue() -``` - -`MPMCQueueOf[I]` is an implementation with parametrized item type. It is available for Go 1.19 or later. - -```go -q := xsync.NewMPMCQueueOf[string](1024) -q.Enqueue("foo") -item := q.Dequeue() // string -``` - -The queue is based on the algorithm from the [MPMCQueue](https://github.com/rigtorp/MPMCQueue) C++ library which in its turn references D.Vyukov's [MPMC queue](https://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue). According to the following [classification](https://www.1024cores.net/home/lock-free-algorithms/queues), the queue is array-based, fails on overflow, provides causal FIFO, has blocking producers and consumers. - -The idea of the algorithm is to allow parallelism for concurrent producers and consumers by introducing the notion of tickets, i.e. values of two counters, one per producers/consumers. An atomic increment of one of those counters is the only noticeable contention point in queue operations. The rest of the operation avoids contention on writes thanks to the turn-based read/write access for each of the queue items. - -In essence, `MPMCQueue` is a specialized queue for scenarios where there are multiple concurrent producers and consumers of a single queue running on a large multicore machine. - -To get the optimal performance, you may want to set the queue size to be large enough, say, an order of magnitude greater than the number of producers/consumers, to allow producers and consumers to progress with their queue operations in parallel most of the time. - -### RBMutex - -A `RBMutex` is a reader-biased reader/writer mutual exclusion lock. The lock can be held by many readers or a single writer. - -```go -mu := xsync.NewRBMutex() -// reader lock calls return a token -t := mu.RLock() -// the token must be later used to unlock the mutex -mu.RUnlock(t) -// writer locks are the same as in sync.RWMutex -mu.Lock() -mu.Unlock() -``` - -`RBMutex` is based on a modified version of BRAVO (Biased Locking for Reader-Writer Locks) algorithm: https://arxiv.org/pdf/1810.01553.pdf - -The idea of the algorithm is to build on top of an existing reader-writer mutex and introduce a fast path for readers. On the fast path, reader lock attempts are sharded over an internal array based on the reader identity (a token in the case of Golang). This means that readers do not contend over a single atomic counter like it's done in, say, `sync.RWMutex` allowing for better scalability in terms of cores. - -Hence, by the design `RBMutex` is a specialized mutex for scenarios, such as caches, where the vast majority of locks are acquired by readers and write lock acquire attempts are infrequent. In such scenarios, `RBMutex` should perform better than the `sync.RWMutex` on large multicore machines. - -`RBMutex` extends `sync.RWMutex` internally and uses it as the "reader bias disabled" fallback, so the same semantics apply. The only noticeable difference is in the reader tokens returned from the `RLock`/`RUnlock` methods. - -## License - -Licensed under MIT. diff --git a/xsync/counter.go b/xsync/counter.go deleted file mode 100644 index 4d4dc87..0000000 --- a/xsync/counter.go +++ /dev/null @@ -1,99 +0,0 @@ -package xsync - -import ( - "sync" - "sync/atomic" -) - -// pool for P tokens -var ptokenPool sync.Pool - -// a P token is used to point at the current OS thread (P) -// on which the goroutine is run; exact identity of the thread, -// as well as P migration tolerance, is not important since -// it's used to as a best effort mechanism for assigning -// concurrent operations (goroutines) to different stripes of -// the counter -type ptoken struct { - idx uint32 - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - 4]byte -} - -// A Counter is a striped int64 counter. -// -// Should be preferred over a single atomically updated int64 -// counter in high contention scenarios. -// -// A Counter must not be copied after first use. -type Counter struct { - stripes []cstripe - mask uint32 -} - -type cstripe struct { - c int64 - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - 8]byte -} - -// NewCounter creates a new Counter instance. -func NewCounter() *Counter { - nstripes := nextPowOf2(parallelism()) - c := Counter{ - stripes: make([]cstripe, nstripes), - mask: nstripes - 1, - } - return &c -} - -// Inc increments the counter by 1. -func (c *Counter) Inc() { - c.Add(1) -} - -// Dec decrements the counter by 1. -func (c *Counter) Dec() { - c.Add(-1) -} - -// Add adds the delta to the counter. -func (c *Counter) Add(delta int64) { - t, ok := ptokenPool.Get().(*ptoken) - if !ok { - t = new(ptoken) - t.idx = runtime_fastrand() - } - for { - stripe := &c.stripes[t.idx&c.mask] - cnt := atomic.LoadInt64(&stripe.c) - if atomic.CompareAndSwapInt64(&stripe.c, cnt, cnt+delta) { - break - } - // Give a try with another randomly selected stripe. - t.idx = runtime_fastrand() - } - ptokenPool.Put(t) -} - -// Value returns the current counter value. -// The returned value may not include all of the latest operations in -// presence of concurrent modifications of the counter. -func (c *Counter) Value() int64 { - v := int64(0) - for i := 0; i < len(c.stripes); i++ { - stripe := &c.stripes[i] - v += atomic.LoadInt64(&stripe.c) - } - return v -} - -// Reset resets the counter to zero. -// This method should only be used when it is known that there are -// no concurrent modifications of the counter. -func (c *Counter) Reset() { - for i := 0; i < len(c.stripes); i++ { - stripe := &c.stripes[i] - atomic.StoreInt64(&stripe.c, 0) - } -} diff --git a/xsync/counter_test.go b/xsync/counter_test.go deleted file mode 100644 index cd18c80..0000000 --- a/xsync/counter_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package xsync_test - -import ( - "runtime" - "sync/atomic" - "testing" - - . "github.com/fufuok/utils/xsync" -) - -func TestCounterInc(t *testing.T) { - c := NewCounter() - for i := 0; i < 100; i++ { - if v := c.Value(); v != int64(i) { - t.Fatalf("got %v, want %d", v, i) - } - c.Inc() - } -} - -func TestCounterDec(t *testing.T) { - c := NewCounter() - for i := 0; i < 100; i++ { - if v := c.Value(); v != int64(-i) { - t.Fatalf("got %v, want %d", v, -i) - } - c.Dec() - } -} - -func TestCounterAdd(t *testing.T) { - c := NewCounter() - for i := 0; i < 100; i++ { - if v := c.Value(); v != int64(i*42) { - t.Fatalf("got %v, want %d", v, i*42) - } - c.Add(42) - } -} - -func TestCounterReset(t *testing.T) { - c := NewCounter() - c.Add(42) - if v := c.Value(); v != 42 { - t.Fatalf("got %v, want %d", v, 42) - } - c.Reset() - if v := c.Value(); v != 0 { - t.Fatalf("got %v, want %d", v, 0) - } -} - -func parallelIncrementor(c *Counter, numIncs int, cdone chan bool) { - for i := 0; i < numIncs; i++ { - c.Inc() - } - cdone <- true -} - -func doTestParallelIncrementors(t *testing.T, numModifiers, gomaxprocs int) { - runtime.GOMAXPROCS(gomaxprocs) - c := NewCounter() - cdone := make(chan bool) - numIncs := 10_000 - for i := 0; i < numModifiers; i++ { - go parallelIncrementor(c, numIncs, cdone) - } - // Wait for the goroutines to finish. - for i := 0; i < numModifiers; i++ { - <-cdone - } - expected := int64(numModifiers * numIncs) - if v := c.Value(); v != expected { - t.Fatalf("got %d, want %d", v, expected) - } -} - -func TestCounterParallelIncrementors(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) - doTestParallelIncrementors(t, 4, 2) - doTestParallelIncrementors(t, 16, 4) - doTestParallelIncrementors(t, 64, 8) -} - -func benchmarkCounter(b *testing.B, writeRatio int) { - c := NewCounter() - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - foo++ - if writeRatio > 0 && foo%writeRatio == 0 { - c.Value() - } else { - c.Inc() - } - } - _ = foo - }) -} - -func BenchmarkCounter(b *testing.B) { - benchmarkCounter(b, 10000) -} - -func benchmarkAtomicInt64(b *testing.B, writeRatio int) { - var c int64 - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - foo++ - if writeRatio > 0 && foo%writeRatio == 0 { - atomic.LoadInt64(&c) - } else { - atomic.AddInt64(&c, 1) - } - } - _ = foo - }) -} - -func BenchmarkAtomicInt64(b *testing.B) { - benchmarkAtomicInt64(b, 10000) -} diff --git a/xsync/example_test.go b/xsync/example_test.go deleted file mode 100644 index 193ae28..0000000 --- a/xsync/example_test.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync_test - -import ( - "errors" - "fmt" - - "github.com/fufuok/utils/xsync" -) - -func ExampleMapOf_Compute() { - counts := xsync.NewMapOf[int, int]() - - // Store a new value. - v, ok := counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is false here. - newValue = 42 - delete = false - return - }) - // v: 42, ok: true - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // Update an existing value. - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is true here. - newValue = oldValue + 42 - delete = false - return - }) - // v: 84, ok: true - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // 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) { - oldVal = oldValue - if !loaded || oldValue < minVal { - newValue = minVal - delete = false - return - } - newValue = oldValue - delete = false - return - }) - // v: 84, ok: true, oldVal: 84 - fmt.Printf("v: %v, ok: %v, oldVal: %v\n", v, ok, oldVal) - - // Delete an existing value. - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - // loaded is true here. - delete = true - return - }) - // v: 84, ok: false - fmt.Printf("v: %v, ok: %v\n", v, ok) - - // Propagate an error from the compute function to the outer scope. - var err error - v, ok = counts.Compute(42, func(oldValue int, loaded bool) (newValue int, delete bool) { - if oldValue == 42 { - err = errors.New("something went wrong") - return 0, true // no need to create a key/value pair - } - newValue = 0 - delete = false - return - }) - fmt.Printf("err: %v\n", err) -} diff --git a/xsync/export_mapof_test.go b/xsync/export_mapof_test.go deleted file mode 100644 index 0ede5bf..0000000 --- a/xsync/export_mapof_test.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync - -type ( - BucketOfPadded = bucketOfPadded -) - -func MakeHasher[T comparable]() func(T, uint64) uint64 { - return makeHasher[T]() -} - -func CollectMapOfStats[K comparable, V any](m *MapOf[K, V]) MapStats { - return MapStats{m.stats()} -} - -func NewMapOfWithHasher[K comparable, V any]( - hasher func(K, uint64) uint64, - options ...func(*MapConfig), -) *MapOf[K, V] { - return newMapOf[K, V](hasher, options...) -} diff --git a/xsync/export_test.go b/xsync/export_test.go deleted file mode 100644 index 23f517d..0000000 --- a/xsync/export_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package xsync - -const ( - EntriesPerMapBucket = entriesPerMapBucket - MapLoadFactor = mapLoadFactor - DefaultMinMapTableLen = defaultMinMapTableLen - DefaultMinMapTableCap = defaultMinMapTableLen * entriesPerMapBucket - MaxMapCounterLen = maxMapCounterLen -) - -type ( - BucketPadded = bucketPadded -) - -type MapStats struct { - mapStats -} - -func CollectMapStats(m *Map) MapStats { - return MapStats{m.stats()} -} - -func LockBucket(mu *uint64) { - lockBucket(mu) -} - -func UnlockBucket(mu *uint64) { - unlockBucket(mu) -} - -func TopHashMatch(hash, topHashes uint64, idx int) bool { - return topHashMatch(hash, topHashes, idx) -} - -func StoreTopHash(hash, topHashes uint64, idx int) uint64 { - return storeTopHash(hash, topHashes, idx) -} - -func EraseTopHash(topHashes uint64, idx int) uint64 { - return eraseTopHash(topHashes, idx) -} - -func EnableAssertions() { - assertionsEnabled = true -} - -func DisableAssertions() { - assertionsEnabled = false -} - -func Fastrand() uint32 { - return runtime_fastrand() -} - -func NextPowOf2(v uint32) uint32 { - return nextPowOf2(v) -} - -func MakeSeed() uint64 { - return makeSeed() -} - -func HashString(s string, seed uint64) uint64 { - return hashString(s, seed) -} diff --git a/xsync/map.go b/xsync/map.go deleted file mode 100644 index 92d73ac..0000000 --- a/xsync/map.go +++ /dev/null @@ -1,843 +0,0 @@ -package xsync - -import ( - "fmt" - "math" - "runtime" - "strings" - "sync" - "sync/atomic" - "unsafe" -) - -type mapResizeHint int - -const ( - mapGrowHint mapResizeHint = 0 - mapShrinkHint mapResizeHint = 1 - mapClearHint mapResizeHint = 2 -) - -const ( - // number of entries per bucket; 3 entries lead to size of 64B - // (one cache line) on 64-bit machines - entriesPerMapBucket = 3 - // threshold fraction of table occupation to start a table shrinking - // when deleting the last entry in a bucket chain - mapShrinkFraction = 128 - // map load factor to trigger a table resize during insertion; - // a map holds up to mapLoadFactor*entriesPerMapBucket*mapTableLen - // key-value pairs (this is a soft limit) - mapLoadFactor = 0.75 - // minimal table size, i.e. number of buckets; thus, minimal map - // capacity can be calculated as entriesPerMapBucket*defaultMinMapTableLen - defaultMinMapTableLen = 32 - // minimum counter stripes to use - minMapCounterLen = 8 - // maximum counter stripes to use; stands for around 4KB of memory - maxMapCounterLen = 32 -) - -var ( - topHashMask = uint64((1<<20)-1) << 44 - topHashEntryMasks = [3]uint64{ - topHashMask, - topHashMask >> 20, - topHashMask >> 40, - } -) - -// Map is like a Go map[string]interface{} but is safe for concurrent -// use by multiple goroutines without additional locking or -// coordination. It follows the interface of sync.Map with -// a number of valuable extensions like Compute or Size. -// -// A Map must not be copied after first use. -// -// Map uses a modified version of Cache-Line Hash Table (CLHT) -// data structure: https://github.com/LPD-EPFL/CLHT -// -// CLHT is built around idea to organize the hash table in -// cache-line-sized buckets, so that on all modern CPUs update -// operations complete with 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 Map outperforms sync.Map. -// -// One important difference with sync.Map is that only string keys -// are supported. That's because Golang standard library does not -// expose the built-in hash functions for interface{} values. -type Map struct { - totalGrowths int64 - totalShrinks int64 - resizing int64 // resize in progress flag; updated atomically - resizeMu sync.Mutex // only used along with resizeCond - resizeCond sync.Cond // used to wake up resize waiters (concurrent modifications) - table unsafe.Pointer // *mapTable - minTableLen int - growOnly bool -} - -type mapTable struct { - buckets []bucketPadded - // striped counter for number of table entries; - // used to determine if a table shrinking is needed - // occupies min(buckets_memory/1024, 64KB) of memory - size []counterStripe - seed uint64 -} - -type counterStripe struct { - c int64 - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - 8]byte -} - -type bucketPadded struct { - //lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs - pad [cacheLineSize - unsafe.Sizeof(bucket{})]byte - bucket -} - -type bucket struct { - next unsafe.Pointer // *bucketPadded - keys [entriesPerMapBucket]unsafe.Pointer - values [entriesPerMapBucket]unsafe.Pointer - // topHashMutex is a 2-in-1 value. - // - // It contains packed top 20 bits (20 MSBs) of hash codes for keys - // stored in the bucket: - // | key 0's top hash | key 1's top hash | key 2's top hash | bitmap for keys | mutex | - // | 20 bits | 20 bits | 20 bits | 3 bits | 1 bit | - // - // The least significant bit is used for the mutex (TTAS spinlock). - topHashMutex uint64 -} - -type rangeEntry struct { - key unsafe.Pointer - value unsafe.Pointer -} - -// MapConfig defines configurable Map/MapOf options. -type MapConfig struct { - sizeHint int - growOnly bool -} - -// WithPresize configures new Map/MapOf instance with capacity enough -// to hold sizeHint entries. The capacity is treated as the minimal -// capacity meaning that the underlying hash table will never shrink -// to a smaller capacity. If sizeHint is zero or negative, the value -// is ignored. -func WithPresize(sizeHint int) func(*MapConfig) { - return func(c *MapConfig) { - c.sizeHint = sizeHint - } -} - -// WithGrowOnly configures new Map/MapOf instance to be grow-only. -// This means that the underlying hash table grows in capacity when -// new keys are added, but does not shrink when keys are deleted. -// The only exception to this rule is the Clear method which -// shrinks the hash table back to the initial capacity. -func WithGrowOnly() func(*MapConfig) { - return func(c *MapConfig) { - c.growOnly = true - } -} - -// NewMap creates a new Map instance configured with the given -// options. -func NewMap(options ...func(*MapConfig)) *Map { - c := &MapConfig{ - sizeHint: defaultMinMapTableLen * entriesPerMapBucket, - } - for _, o := range options { - o(c) - } - - m := &Map{} - m.resizeCond = *sync.NewCond(&m.resizeMu) - var table *mapTable - if c.sizeHint <= defaultMinMapTableLen*entriesPerMapBucket { - table = newMapTable(defaultMinMapTableLen) - } else { - tableLen := nextPowOf2(uint32(c.sizeHint / entriesPerMapBucket)) - table = newMapTable(int(tableLen)) - } - m.minTableLen = len(table.buckets) - m.growOnly = c.growOnly - atomic.StorePointer(&m.table, unsafe.Pointer(table)) - return m -} - -// NewMapPresized creates a new Map instance with capacity enough to hold -// sizeHint entries. The capacity is treated as the minimal capacity -// meaning that the underlying hash table will never shrink to -// a smaller capacity. If sizeHint is zero or negative, the value -// is ignored. -// -// Deprecated: use NewMap in combination with WithPresize. -func NewMapPresized(sizeHint int) *Map { - return NewMap(WithPresize(sizeHint)) -} - -func newMapTable(minTableLen int) *mapTable { - buckets := make([]bucketPadded, minTableLen) - counterLen := minTableLen >> 10 - if counterLen < minMapCounterLen { - counterLen = minMapCounterLen - } else if counterLen > maxMapCounterLen { - counterLen = maxMapCounterLen - } - counter := make([]counterStripe, counterLen) - t := &mapTable{ - buckets: buckets, - size: counter, - seed: makeSeed(), - } - return t -} - -// 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 *Map) Load(key string) (value interface{}, ok bool) { - table := (*mapTable)(atomic.LoadPointer(&m.table)) - hash := hashString(key, table.seed) - bidx := uint64(len(table.buckets)-1) & hash - b := &table.buckets[bidx] - for { - topHashes := atomic.LoadUint64(&b.topHashMutex) - 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 derefValue(vp), true - } - // Concurrent update/remove. Go for another spin. - goto atomic_snapshot - } - } - } - bptr := atomic.LoadPointer(&b.next) - if bptr == nil { - return - } - b = (*bucketPadded)(bptr) - } -} - -// Store sets the value for a key. -func (m *Map) Store(key string, value interface{}) { - m.doCompute( - key, - func(interface{}, bool) (interface{}, bool) { - return value, false - }, - false, - 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 *Map) LoadOrStore(key string, value interface{}) (actual interface{}, loaded bool) { - return m.doCompute( - key, - func(interface{}, bool) (interface{}, bool) { - return value, false - }, - true, - false, - ) -} - -// LoadAndStore returns the existing value for the key if present, -// while setting the new value for the key. -// It stores the new value and returns the existing one, if present. -// 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.doCompute( - key, - func(interface{}, bool) (interface{}, bool) { - return value, false - }, - false, - false, - ) -} - -// LoadOrCompute returns the existing value for the key if present. -// Otherwise, it computes the value using the provided function and -// returns the computed value. The loaded result is true if the value -// was loaded, false if stored. -// -// This call locks a hash table bucket while the compute function -// is executed. It means that modifications on other entries in -// the bucket will be blocked until the valueFn executes. Consider -// this when the function includes long-running operations. -func (m *Map) LoadOrCompute(key string, valueFn func() interface{}) (actual interface{}, loaded bool) { - 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. -// -// This call locks a hash table bucket while the compute function -// is executed. It means that modifications on other entries in -// the bucket will be blocked until the valueFn executes. Consider -// this when the function includes long-running operations. -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) 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, !computeOnly - } - } - // Write path. - for { - compute_attempt: - var ( - emptyb *bucketPadded - emptyidx int - hintNonEmpty int - ) - table := (*mapTable)(atomic.LoadPointer(&m.table)) - tableLen := len(table.buckets) - hash := hashString(key, table.seed) - bidx := uint64(len(table.buckets)-1) & hash - rootb := &table.buckets[bidx] - lockBucket(&rootb.topHashMutex) - // The following two checks must go in reverse to what's - // in the resize method. - if m.resizeInProgress() { - // Resize is in progress. Wait, then go for another attempt. - unlockBucket(&rootb.topHashMutex) - m.waitForResize() - goto compute_attempt - } - if m.newerTableExists(table) { - // Someone resized the table. Go for another attempt. - unlockBucket(&rootb.topHashMutex) - goto compute_attempt - } - b := rootb - for { - topHashes := atomic.LoadUint64(&b.topHashMutex) - for i := 0; i < entriesPerMapBucket; i++ { - if b.keys[i] == nil { - if emptyb == nil { - emptyb = b - emptyidx = i - } - continue - } - if !topHashMatch(hash, topHashes, i) { - hintNonEmpty++ - continue - } - if key == derefKey(b.keys[i]) { - vp := b.values[i] - if loadIfExists { - unlockBucket(&rootb.topHashMutex) - 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 = isEmptyBucket(b) - } - unlockBucket(&rootb.topHashMutex) - table.addSize(bidx, -1) - // Might need to shrink the table. - if leftEmpty { - m.resize(table, mapShrinkHint) - } - return oldValue, !computeOnly - } - 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 oldValue, true - } - hintNonEmpty++ - } - if b.next == nil { - if emptyb != nil { - // 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)) - atomic.StorePointer(&emptyb.values[emptyidx], unsafe.Pointer(&newValue)) - atomic.StorePointer(&emptyb.keys[emptyidx], unsafe.Pointer(&key)) - unlockBucket(&rootb.topHashMutex) - table.addSize(bidx, 1) - 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 compute_attempt - } - // 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) - 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 newValue, computeOnly - } - b = (*bucketPadded)(b.next) - } - } -} - -func (m *Map) newerTableExists(table *mapTable) bool { - curTablePtr := atomic.LoadPointer(&m.table) - return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table)) -} - -func (m *Map) resizeInProgress() bool { - return atomic.LoadInt64(&m.resizing) == 1 -} - -func (m *Map) waitForResize() { - m.resizeMu.Lock() - for m.resizeInProgress() { - m.resizeCond.Wait() - } - m.resizeMu.Unlock() -} - -func (m *Map) resize(knownTable *mapTable, hint mapResizeHint) { - knownTableLen := len(knownTable.buckets) - // Fast path for shrink attempts. - if hint == mapShrinkHint { - if m.growOnly || - m.minTableLen == knownTableLen || - knownTable.sumSize() > int64((knownTableLen*entriesPerMapBucket)/mapShrinkFraction) { - 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 - table := (*mapTable)(atomic.LoadPointer(&m.table)) - tableLen := len(table.buckets) - switch hint { - case mapGrowHint: - // Grow the table with factor of 2. - atomic.AddInt64(&m.totalGrowths, 1) - newTable = newMapTable(tableLen << 1) - case mapShrinkHint: - shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction) - if tableLen > m.minTableLen && table.sumSize() <= 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 - } - case mapClearHint: - newTable = newMapTable(m.minTableLen) - default: - panic(fmt.Sprintf("unexpected resize hint: %d", hint)) - } - // Copy the data only if we're not clearing the map. - if hint != mapClearHint { - for i := 0; i < tableLen; i++ { - copied := copyBucket(&table.buckets[i], newTable) - newTable.addSizePlain(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() -} - -func copyBucket(b *bucketPadded, destTable *mapTable) (copied int) { - rootb := b - lockBucket(&rootb.topHashMutex) - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.keys[i] != nil { - k := derefKey(b.keys[i]) - hash := hashString(k, destTable.seed) - bidx := uint64(len(destTable.buckets)-1) & hash - destb := &destTable.buckets[bidx] - appendToBucket(hash, b.keys[i], b.values[i], destb) - copied++ - } - } - if b.next == nil { - unlockBucket(&rootb.topHashMutex) - return - } - b = (*bucketPadded)(b.next) - } -} - -func appendToBucket(hash uint64, keyPtr, valPtr unsafe.Pointer, b *bucketPadded) { - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.keys[i] == nil { - b.keys[i] = keyPtr - b.values[i] = valPtr - b.topHashMutex = storeTopHash(hash, b.topHashMutex, i) - return - } - } - if b.next == nil { - newb := new(bucketPadded) - newb.keys[0] = keyPtr - newb.values[0] = valPtr - newb.topHashMutex = storeTopHash(hash, newb.topHashMutex, 0) - b.next = unsafe.Pointer(newb) - return - } - b = (*bucketPadded)(b.next) - } -} - -func isEmptyBucket(rootb *bucketPadded) bool { - b := rootb - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.keys[i] != nil { - return false - } - } - if b.next == nil { - return true - } - b = (*bucketPadded)(b.next) - } -} - -// 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, including entry -// creation, modification and deletion. However, the concurrent -// modification rule apply, i.e. the changes may be not reflected -// in the subsequently iterated entries. -func (m *Map) Range(f func(key string, value interface{}) bool) { - var zeroEntry rangeEntry - // Pre-allocate array big enough to fit entries for most hash tables. - bentries := make([]rangeEntry, 0, 16*entriesPerMapBucket) - tablep := atomic.LoadPointer(&m.table) - table := *(*mapTable)(tablep) - for i := range table.buckets { - rootb := &table.buckets[i] - b := rootb - // Prevent concurrent modifications and copy all entries into - // the intermediate slice. - lockBucket(&rootb.topHashMutex) - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.keys[i] != nil { - bentries = append(bentries, rangeEntry{ - key: b.keys[i], - value: b.values[i], - }) - } - } - if b.next == nil { - unlockBucket(&rootb.topHashMutex) - break - } - b = (*bucketPadded)(b.next) - } - // Call the function for all copied entries. - for j := range bentries { - k := derefKey(bentries[j].key) - v := derefValue(bentries[j].value) - if !f(k, v) { - return - } - // Remove the reference to avoid preventing the copied - // entries from being GCed until this method finishes. - bentries[j] = zeroEntry - } - bentries = bentries[:0] - } -} - -// Clear deletes all keys and values currently stored in the map. -func (m *Map) Clear() { - table := (*mapTable)(atomic.LoadPointer(&m.table)) - m.resize(table, mapClearHint) -} - -// Size returns current size of the map. -func (m *Map) Size() int { - table := (*mapTable)(atomic.LoadPointer(&m.table)) - return int(table.sumSize()) -} - -func derefKey(keyPtr unsafe.Pointer) string { - return *(*string)(keyPtr) -} - -func derefValue(valuePtr unsafe.Pointer) interface{} { - return *(*interface{})(valuePtr) -} - -func lockBucket(mu *uint64) { - for { - var v uint64 - for { - v = atomic.LoadUint64(mu) - if v&1 != 1 { - break - } - runtime.Gosched() - } - if atomic.CompareAndSwapUint64(mu, v, v|1) { - return - } - runtime.Gosched() - } -} - -func unlockBucket(mu *uint64) { - v := atomic.LoadUint64(mu) - atomic.StoreUint64(mu, v&^1) -} - -func topHashMatch(hash, topHashes uint64, idx int) bool { - if topHashes&(1<<(idx+1)) == 0 { - // Entry is not present. - return false - } - hash = hash & topHashMask - topHashes = (topHashes & topHashEntryMasks[idx]) << (20 * idx) - return hash == topHashes -} - -func storeTopHash(hash, topHashes uint64, idx int) uint64 { - // Zero out top hash at idx. - topHashes = topHashes &^ topHashEntryMasks[idx] - // Chop top 20 MSBs of the given hash and position them at idx. - hash = (hash & topHashMask) >> (20 * idx) - // Store the MSBs. - topHashes = topHashes | hash - // Mark the entry as present. - return topHashes | (1 << (idx + 1)) -} - -func eraseTopHash(topHashes uint64, idx int) uint64 { - return topHashes &^ (1 << (idx + 1)) -} - -func (table *mapTable) addSize(bucketIdx uint64, delta int) { - cidx := uint64(len(table.size)-1) & bucketIdx - atomic.AddInt64(&table.size[cidx].c, int64(delta)) -} - -func (table *mapTable) addSizePlain(bucketIdx uint64, delta int) { - cidx := uint64(len(table.size)-1) & bucketIdx - table.size[cidx].c += int64(delta) -} - -func (table *mapTable) sumSize() int64 { - sum := int64(0) - for i := range table.size { - sum += atomic.LoadInt64(&table.size[i].c) - } - return sum -} - -type mapStats struct { - RootBuckets int - TotalBuckets int - EmptyBuckets int - Capacity int - Size int // calculated number of entries - Counter int // number of entries according to table counter - CounterLen int // number of counter stripes - MinEntries int // min entries per chain of buckets - MaxEntries int // max entries per chain of buckets - TotalGrowths int64 - TotalShrinks int64 -} - -func (s *mapStats) ToString() string { - var sb strings.Builder - sb.WriteString("\n---\n") - sb.WriteString(fmt.Sprintf("RootBuckets: %d\n", s.RootBuckets)) - sb.WriteString(fmt.Sprintf("TotalBuckets: %d\n", s.TotalBuckets)) - sb.WriteString(fmt.Sprintf("EmptyBuckets: %d\n", s.EmptyBuckets)) - sb.WriteString(fmt.Sprintf("Capacity: %d\n", s.Capacity)) - sb.WriteString(fmt.Sprintf("Size: %d\n", s.Size)) - sb.WriteString(fmt.Sprintf("Counter: %d\n", s.Counter)) - sb.WriteString(fmt.Sprintf("CounterLen: %d\n", s.CounterLen)) - sb.WriteString(fmt.Sprintf("MinEntries: %d\n", s.MinEntries)) - sb.WriteString(fmt.Sprintf("MaxEntries: %d\n", s.MaxEntries)) - sb.WriteString(fmt.Sprintf("TotalGrowths: %d\n", s.TotalGrowths)) - sb.WriteString(fmt.Sprintf("TotalShrinks: %d\n", s.TotalShrinks)) - sb.WriteString("---\n") - return sb.String() -} - -// O(N) operation; use for debug purposes only -func (m *Map) stats() mapStats { - stats := mapStats{ - TotalGrowths: atomic.LoadInt64(&m.totalGrowths), - TotalShrinks: atomic.LoadInt64(&m.totalShrinks), - MinEntries: math.MaxInt32, - } - table := (*mapTable)(atomic.LoadPointer(&m.table)) - stats.RootBuckets = len(table.buckets) - stats.Counter = int(table.sumSize()) - stats.CounterLen = len(table.size) - for i := range table.buckets { - nentries := 0 - b := &table.buckets[i] - stats.TotalBuckets++ - for { - nentriesLocal := 0 - stats.Capacity += entriesPerMapBucket - for i := 0; i < entriesPerMapBucket; i++ { - if atomic.LoadPointer(&b.keys[i]) != nil { - stats.Size++ - nentriesLocal++ - } - } - nentries += nentriesLocal - if nentriesLocal == 0 { - stats.EmptyBuckets++ - } - if b.next == nil { - break - } - b = (*bucketPadded)(b.next) - stats.TotalBuckets++ - } - if nentries < stats.MinEntries { - stats.MinEntries = nentries - } - if nentries > stats.MaxEntries { - stats.MaxEntries = nentries - } - } - return stats -} diff --git a/xsync/map_test.go b/xsync/map_test.go deleted file mode 100644 index a8fdea9..0000000 --- a/xsync/map_test.go +++ /dev/null @@ -1,1362 +0,0 @@ -package xsync_test - -import ( - "fmt" - "math" - "math/bits" - "math/rand" - "strconv" - "sync" - "sync/atomic" - "testing" - "time" - "unsafe" - - . "github.com/fufuok/utils/xsync" -) - -const ( - // number of entries to use in benchmarks - benchmarkNumEntries = 1_000 - // key prefix used in benchmarks - benchmarkKeyPrefix = "what_a_looooooooooooooooooooooong_key_prefix_" -) - -var benchmarkCases = []struct { - name string - readPercentage int -}{ - {"reads=100%", 100}, // 100% loads, 0% stores, 0% deletes - {"reads=99%", 99}, // 99% loads, 0.5% stores, 0.5% deletes - {"reads=90%", 90}, // 90% loads, 5% stores, 5% deletes - {"reads=75%", 75}, // 75% loads, 12.5% stores, 12.5% deletes -} - -var benchmarkKeys []string - -type point struct { - x int32 - y int32 -} - -func init() { - benchmarkKeys = make([]string, benchmarkNumEntries) - for i := 0; i < benchmarkNumEntries; i++ { - benchmarkKeys[i] = benchmarkKeyPrefix + strconv.Itoa(i) - } -} - -func runParallel(b *testing.B, benchFn func(pb *testing.PB)) { - b.ResetTimer() - start := time.Now() - b.RunParallel(benchFn) - opsPerSec := float64(b.N) / float64(time.Since(start).Seconds()) - b.ReportMetric(opsPerSec, "ops/s") -} - -func TestMap_BucketStructSize(t *testing.T) { - size := unsafe.Sizeof(BucketPadded{}) - if size != 64 { - t.Fatalf("size of 64B (one cache line) is expected, got: %d", size) - } -} - -func TestMap_UniqueValuePointers_Int(t *testing.T) { - EnableAssertions() - m := NewMap() - v := 42 - m.Store("foo", v) - m.Store("foo", v) - DisableAssertions() -} - -func TestMap_UniqueValuePointers_Struct(t *testing.T) { - type foo struct{} - EnableAssertions() - m := NewMap() - v := foo{} - m.Store("foo", v) - m.Store("foo", v) - DisableAssertions() -} - -func TestMap_UniqueValuePointers_Pointer(t *testing.T) { - type foo struct{} - EnableAssertions() - m := NewMap() - v := &foo{} - m.Store("foo", v) - m.Store("foo", v) - DisableAssertions() -} - -func TestMap_UniqueValuePointers_Slice(t *testing.T) { - EnableAssertions() - m := NewMap() - v := make([]int, 13) - m.Store("foo", v) - m.Store("foo", v) - DisableAssertions() -} - -func TestMap_UniqueValuePointers_String(t *testing.T) { - EnableAssertions() - m := NewMap() - v := "bar" - m.Store("foo", v) - m.Store("foo", v) - DisableAssertions() -} - -func TestMap_UniqueValuePointers_Nil(t *testing.T) { - EnableAssertions() - m := NewMap() - m.Store("foo", nil) - m.Store("foo", nil) - DisableAssertions() -} - -func TestMap_MissingEntry(t *testing.T) { - m := NewMap() - v, ok := m.Load("foo") - if ok { - t.Fatalf("value was not expected: %v", v) - } - if deleted, loaded := m.LoadAndDelete("foo"); loaded { - t.Fatalf("value was not expected %v", deleted) - } - if actual, loaded := m.LoadOrStore("foo", "bar"); loaded { - t.Fatalf("value was not expected %v", actual) - } -} - -func TestMap_EmptyStringKey(t *testing.T) { - m := NewMap() - m.Store("", "foobar") - v, ok := m.Load("") - if !ok { - t.Fatal("value was expected") - } - if vs, ok := v.(string); ok && vs != "foobar" { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapStore_NilValue(t *testing.T) { - m := NewMap() - m.Store("foo", nil) - v, ok := m.Load("foo") - if !ok { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapLoadOrStore_NilValue(t *testing.T) { - m := NewMap() - m.LoadOrStore("foo", nil) - v, loaded := m.LoadOrStore("foo", nil) - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapLoadOrStore_NonNilValue(t *testing.T) { - type foo struct{} - m := NewMap() - newv := &foo{} - v, loaded := m.LoadOrStore("foo", newv) - if loaded { - t.Fatal("no value was expected") - } - if v != newv { - t.Fatalf("value does not match: %v", v) - } - newv2 := &foo{} - v, loaded = m.LoadOrStore("foo", newv2) - if !loaded { - t.Fatal("value was expected") - } - if v != newv { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapLoadAndStore_NilValue(t *testing.T) { - m := NewMap() - m.LoadAndStore("foo", nil) - v, loaded := m.LoadAndStore("foo", nil) - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } - v, loaded = m.Load("foo") - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapLoadAndStore_NonNilValue(t *testing.T) { - type foo struct{} - m := NewMap() - v1 := &foo{} - v, loaded := m.LoadAndStore("foo", v1) - if loaded { - t.Fatal("no value was expected") - } - if v != v1 { - t.Fatalf("value does not match: %v", v) - } - v2 := 2 - v, loaded = m.LoadAndStore("foo", v2) - if !loaded { - t.Fatal("value was expected") - } - if v != v1 { - t.Fatalf("value does not match: %v", v) - } - v, loaded = m.Load("foo") - if !loaded { - t.Fatal("value was expected") - } - if v != v2 { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapRange(t *testing.T) { - const numEntries = 1000 - m := NewMap() - 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 interface{}) bool { - if key != strconv.Itoa(value.(int)) { - t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value) - return false - } - met[key] += 1 - iters++ - return true - }) - if iters != numEntries { - t.Fatalf("got unexpected number of iterations: %d", iters) - } - for i := 0; i < numEntries; i++ { - if c := met[strconv.Itoa(i)]; c != 1 { - t.Fatalf("met key %d multiple times: %d", i, c) - } - } -} - -func TestMapRange_FalseReturned(t *testing.T) { - m := NewMap() - for i := 0; i < 100; i++ { - m.Store(strconv.Itoa(i), i) - } - iters := 0 - m.Range(func(key string, value interface{}) bool { - iters++ - return iters != 13 - }) - if iters != 13 { - t.Fatalf("got unexpected number of iterations: %d", iters) - } -} - -func TestMapRange_NestedDelete(t *testing.T) { - const numEntries = 256 - m := NewMap() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - m.Range(func(key string, value interface{}) bool { - m.Delete(key) - return true - }) - for i := 0; i < numEntries; i++ { - if _, ok := m.Load(strconv.Itoa(i)); ok { - t.Fatalf("value found for %d", i) - } - } -} - -func TestMapStore(t *testing.T) { - const numEntries = 128 - m := NewMap() - 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.Fatalf("value not found for %d", i) - } - if vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapLoadOrStore(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.LoadOrStore(strconv.Itoa(i), i); !loaded { - t.Fatalf("value not found for %d", i) - } - } -} - -func TestMapLoadOrCompute(t *testing.T) { - const numEntries = 1000 - m := NewMap() - for i := 0; i < numEntries; i++ { - v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() interface{} { - return i - }) - if loaded { - t.Fatalf("value not computed for %d", i) - } - if vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - for i := 0; i < numEntries; i++ { - v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() interface{} { - return i - }) - if !loaded { - t.Fatalf("value not loaded for %d", i) - } - if vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapLoadOrCompute_FunctionCalledOnce(t *testing.T) { - m := NewMap() - for i := 0; i < 100; { - m.LoadOrCompute(strconv.Itoa(i), func() (v interface{}) { - v, i = i, i+1 - return v - }) - } - - m.Range(func(k string, v interface{}) bool { - if vi, ok := v.(int); !ok || strconv.Itoa(vi) != k { - t.Fatalf("%sth key is not equal to value %d", k, v) - } - return true - }) -} - -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++ { - 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.Fatalf("value was not expected for %d", i) - } - } -} - -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 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.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) { - const numEntries = 1000 - m := NewMap() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - - cdone := make(chan bool) - go func() { - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(int(i))) - } - cdone <- true - }() - go func() { - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(int(i))) - } - cdone <- true - }() - - // Wait for the goroutines to finish. - <-cdone - <-cdone - - stats := CollectMapStats(m) - if stats.RootBuckets != DefaultMinMapTableLen { - t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) - } -} - -func sizeBasedOnRange(m *Map) int { - size := 0 - m.Range(func(key string, value interface{}) bool { - size++ - return true - }) - return size -} - -func TestMapSize(t *testing.T) { - const numEntries = 1000 - m := NewMap() - size := m.Size() - if size != 0 { - t.Fatalf("zero size expected: %d", size) - } - expectedSize := 0 - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - expectedSize++ - size := m.Size() - if size != expectedSize { - t.Fatalf("size of %d was expected, got: %d", expectedSize, size) - } - rsize := sizeBasedOnRange(m) - if size != rsize { - t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) - } - } - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(i)) - expectedSize-- - size := m.Size() - if size != expectedSize { - t.Fatalf("size of %d was expected, got: %d", expectedSize, size) - } - rsize := sizeBasedOnRange(m) - if size != rsize { - t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) - } - } -} - -func TestMapClear(t *testing.T) { - const numEntries = 1000 - m := NewMap() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - size := m.Size() - if size != numEntries { - t.Fatalf("size of %d was expected, got: %d", numEntries, size) - } - m.Clear() - size = m.Size() - if size != 0 { - t.Fatalf("zero size was expected, got: %d", size) - } - rsize := sizeBasedOnRange(m) - if rsize != 0 { - t.Fatalf("zero number of entries in Range was expected, got: %d", rsize) - } -} - -func assertMapCapacity(t *testing.T, m *Map, expectedCap int) { - stats := CollectMapStats(m) - if stats.Capacity != expectedCap { - t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity) - } -} - -func TestNewMapPresized(t *testing.T) { - assertMapCapacity(t, NewMap(), DefaultMinMapTableCap) - assertMapCapacity(t, NewMapPresized(1000), 1536) - assertMapCapacity(t, NewMap(WithPresize(1000)), 1536) - assertMapCapacity(t, NewMapPresized(0), DefaultMinMapTableCap) - assertMapCapacity(t, NewMap(WithPresize(0)), DefaultMinMapTableCap) - assertMapCapacity(t, NewMapPresized(-1), DefaultMinMapTableCap) - assertMapCapacity(t, NewMap(WithPresize(-1)), DefaultMinMapTableCap) -} - -func TestNewMapPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) { - const minTableLen = 1024 - const numEntries = minTableLen * EntriesPerMapBucket - m := NewMap(WithPresize(numEntries)) - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - - stats := CollectMapStats(m) - if stats.RootBuckets <= minTableLen { - t.Fatalf("table did not grow: %d", stats.RootBuckets) - } - - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(int(i))) - } - - stats = CollectMapStats(m) - if stats.RootBuckets != minTableLen { - t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) - } -} - -func TestNewMapGrowOnly_OnlyShrinksOnClear(t *testing.T) { - const minTableLen = 128 - const numEntries = minTableLen * EntriesPerMapBucket - m := NewMap(WithPresize(numEntries), WithGrowOnly()) - - stats := CollectMapStats(m) - initialTableLen := stats.RootBuckets - - for i := 0; i < 2*numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - stats = CollectMapStats(m) - maxTableLen := stats.RootBuckets - if maxTableLen <= minTableLen { - t.Fatalf("table did not grow: %d", maxTableLen) - } - - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(int(i))) - } - stats = CollectMapStats(m) - if stats.RootBuckets != maxTableLen { - t.Fatalf("table length was different from the expected: %d", stats.RootBuckets) - } - - m.Clear() - stats = CollectMapStats(m) - if stats.RootBuckets != initialTableLen { - t.Fatalf("table length was different from the initial: %d", stats.RootBuckets) - } -} - -func TestMapResize(t *testing.T) { - const numEntries = 100_000 - m := NewMap() - - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - stats := CollectMapStats(m) - if stats.Size != numEntries { - t.Fatalf("size was too small: %d", stats.Size) - } - expectedCapacity := int(math.RoundToEven(MapLoadFactor+1)) * stats.RootBuckets * EntriesPerMapBucket - if stats.Capacity > expectedCapacity { - t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) - } - if stats.RootBuckets <= DefaultMinMapTableLen { - t.Fatalf("table was too small: %d", stats.RootBuckets) - } - if stats.TotalGrowths == 0 { - t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths) - } - if stats.TotalShrinks > 0 { - t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks) - } - // This is useful when debugging table resize and occupancy. - // Use -v flag to see the output. - t.Log(stats.ToString()) - - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(i)) - } - stats = CollectMapStats(m) - if stats.Size > 0 { - t.Fatalf("zero size was expected: %d", stats.Size) - } - expectedCapacity = stats.RootBuckets * EntriesPerMapBucket - if stats.Capacity != expectedCapacity { - t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) - } - if stats.RootBuckets != DefaultMinMapTableLen { - t.Fatalf("table was too large: %d", stats.RootBuckets) - } - if stats.TotalShrinks == 0 { - t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks) - } - t.Log(stats.ToString()) -} - -func TestMapResize_CounterLenLimit(t *testing.T) { - const numEntries = 1_000_000 - m := NewMap() - - for i := 0; i < numEntries; i++ { - m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i)) - } - stats := CollectMapStats(m) - if stats.Size != numEntries { - t.Fatalf("size was too small: %d", stats.Size) - } - if stats.CounterLen != MaxMapCounterLen { - t.Fatalf("number of counter stripes was too large: %d, expected: %d", - stats.CounterLen, MaxMapCounterLen) - } -} - -func parallelSeqResizer(t *testing.T, m *Map, numEntries int, positive bool, cdone chan bool) { - for i := 0; i < numEntries; i++ { - if positive { - m.Store(strconv.Itoa(i), i) - } else { - m.Store(strconv.Itoa(-i), -i) - } - } - cdone <- true -} - -func TestMapParallelResize_GrowOnly(t *testing.T) { - const numEntries = 100_000 - m := NewMap() - cdone := make(chan bool) - go parallelSeqResizer(t, m, numEntries, true, cdone) - go parallelSeqResizer(t, m, numEntries, false, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - for i := -numEntries + 1; i < numEntries; i++ { - v, ok := m.Load(strconv.Itoa(i)) - if !ok { - t.Fatalf("value not found for %d", i) - } - if vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - if s := m.Size(); s != 2*numEntries-1 { - t.Fatalf("unexpected size: %v", s) - } -} - -func parallelRandResizer(t *testing.T, m *Map, numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - coin := r.Int63n(2) - for j := 0; j < numEntries; j++ { - if coin == 1 { - m.Store(strconv.Itoa(j), j) - } else { - m.Delete(strconv.Itoa(j)) - } - } - } - cdone <- true -} - -func TestMapParallelResize(t *testing.T) { - const numIters = 1_000 - const numEntries = 2 * EntriesPerMapBucket * DefaultMinMapTableLen - m := NewMap() - cdone := make(chan bool) - go parallelRandResizer(t, m, numIters, numEntries, cdone) - go parallelRandResizer(t, m, numIters, numEntries, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - for i := 0; i < numEntries; i++ { - v, ok := m.Load(strconv.Itoa(i)) - if !ok { - // The entry may be deleted and that's ok. - continue - } - if vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - s := m.Size() - if s > numEntries { - t.Fatalf("unexpected size: %v", s) - } - rs := sizeBasedOnRange(m) - if s != rs { - t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) - } -} - -func parallelRandClearer(t *testing.T, m *Map, numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - coin := r.Int63n(2) - for j := 0; j < numEntries; j++ { - if coin == 1 { - m.Store(strconv.Itoa(j), j) - } else { - m.Clear() - } - } - } - cdone <- true -} - -func TestMapParallelClear(t *testing.T) { - const numIters = 100 - const numEntries = 1_000 - m := NewMap() - cdone := make(chan bool) - go parallelRandClearer(t, m, numIters, numEntries, cdone) - go parallelRandClearer(t, m, numIters, numEntries, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map size. - s := m.Size() - if s > numEntries { - t.Fatalf("unexpected size: %v", s) - } - rs := sizeBasedOnRange(m) - if s != rs { - t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) - } -} - -func parallelSeqStorer(t *testing.T, m *Map, 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 pair. - v, ok := m.Load(strconv.Itoa(j)) - if !ok { - t.Errorf("value was not found for %d", j) - break - } - if vi, ok := v.(int); !ok || vi != j { - t.Errorf("value was not expected for %d: %d", j, vi) - break - } - } - } - } - cdone <- true -} - -func TestMapParallelStores(t *testing.T) { - const numStorers = 4 - const numIters = 10_000 - const numEntries = 100 - m := NewMap() - cdone := make(chan bool) - for i := 0; i < numStorers; i++ { - go parallelSeqStorer(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 vi, ok := v.(int); ok && vi != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func parallelRandStorer(t *testing.T, m *Map, 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 vi, ok := v.(int); !ok || vi != j { - t.Errorf("value was not expected for %d: %d", j, vi) - } - } - } - cdone <- true -} - -func parallelRandDeleter(t *testing.T, m *Map, 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 vi, ok := v.(int); !ok || vi != j { - t.Errorf("value was not expected for %d: %d", j, vi) - } - } - } - cdone <- true -} - -func parallelLoader(t *testing.T, m *Map, 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 pair. - if v, ok := m.Load(strconv.Itoa(j)); ok { - if vi, ok := v.(int); !ok || vi != j { - t.Errorf("value was not expected for %d: %d", j, vi) - } - } - } - } - cdone <- true -} - -func TestMapAtomicSnapshot(t *testing.T) { - const numIters = 100_000 - const numEntries = 100 - m := NewMap() - cdone := make(chan bool) - // Update or delete random entry in parallel with loads. - go parallelRandStorer(t, m, numIters, numEntries, cdone) - go parallelRandDeleter(t, m, numIters, numEntries, cdone) - go parallelLoader(t, m, numIters, numEntries, cdone) - // Wait for the goroutines to finish. - for i := 0; i < 3; i++ { - <-cdone - } -} - -func TestMapParallelStoresAndDeletes(t *testing.T) { - const numWorkers = 2 - const numIters = 100_000 - const numEntries = 1000 - m := NewMap() - cdone := make(chan bool) - // Update random entry in parallel with deletes. - for i := 0; i < numWorkers; i++ { - go parallelRandStorer(t, m, numIters, numEntries, cdone) - go parallelRandDeleter(t, m, numIters, numEntries, cdone) - } - // Wait for the goroutines to finish. - for i := 0; i < 2*numWorkers; i++ { - <-cdone - } -} - -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 parallelRangeStorer(t *testing.T, m *Map, numEntries int, stopFlag *int64, cdone chan bool) { - for { - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - if atomic.LoadInt64(stopFlag) != 0 { - break - } - } - cdone <- true -} - -func parallelRangeDeleter(t *testing.T, m *Map, numEntries int, stopFlag *int64, cdone chan bool) { - for { - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(i)) - } - if atomic.LoadInt64(stopFlag) != 0 { - break - } - } - cdone <- true -} - -func TestMapParallelRange(t *testing.T) { - const numEntries = 10_000 - m := NewMap() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - // Start goroutines that would be storing and deleting items in parallel. - cdone := make(chan bool) - stopFlag := int64(0) - go parallelRangeStorer(t, m, numEntries, &stopFlag, cdone) - go parallelRangeDeleter(t, m, numEntries, &stopFlag, cdone) - // Iterate the map and verify that no duplicate keys were met. - met := make(map[string]int) - m.Range(func(key string, value interface{}) bool { - if key != strconv.Itoa(value.(int)) { - t.Fatalf("got unexpected value for key %s: %v", key, value) - return false - } - met[key] += 1 - return true - }) - if len(met) == 0 { - t.Fatal("no entries were met when iterating") - } - for k, c := range met { - if c != 1 { - t.Fatalf("met key %s multiple times: %d", k, c) - } - } - // Make sure that both goroutines finish. - atomic.StoreInt64(&stopFlag, 1) - <-cdone - <-cdone -} - -func parallelShrinker(t *testing.T, m *Map, numIters, numEntries int, stopFlag *int64, cdone chan bool) { - for i := 0; i < numIters; i++ { - for j := 0; j < numEntries; j++ { - if p, loaded := m.LoadOrStore(strconv.Itoa(j), &point{int32(j), int32(j)}); loaded { - t.Errorf("value was present for %d: %v", j, p) - } - } - for j := 0; j < numEntries; j++ { - m.Delete(strconv.Itoa(j)) - } - } - atomic.StoreInt64(stopFlag, 1) - cdone <- true -} - -func parallelUpdater(t *testing.T, m *Map, idx int, stopFlag *int64, cdone chan bool) { - for atomic.LoadInt64(stopFlag) != 1 { - sleepUs := int(Fastrand() % 10) - if p, loaded := m.LoadOrStore(strconv.Itoa(idx), &point{int32(idx), int32(idx)}); loaded { - t.Errorf("value was present for %d: %v", idx, p) - } - time.Sleep(time.Duration(sleepUs) * time.Microsecond) - if _, ok := m.Load(strconv.Itoa(idx)); !ok { - t.Errorf("value was not found for %d", idx) - } - m.Delete(strconv.Itoa(idx)) - } - cdone <- true -} - -func TestMapDoesNotLoseEntriesOnResize(t *testing.T) { - const numIters = 10_000 - const numEntries = 128 - m := NewMap() - cdone := make(chan bool) - stopFlag := int64(0) - go parallelShrinker(t, m, numIters, numEntries, &stopFlag, cdone) - go parallelUpdater(t, m, numEntries, &stopFlag, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - if s := m.Size(); s != 0 { - t.Fatalf("map is not empty: %d", s) - } -} - -func TestMapTopHashMutex(t *testing.T) { - const ( - numLockers = 4 - numIterations = 1000 - ) - var ( - activity int32 - mu uint64 - ) - cdone := make(chan bool) - for i := 0; i < numLockers; i++ { - go func() { - for i := 0; i < numIterations; i++ { - LockBucket(&mu) - n := atomic.AddInt32(&activity, 1) - if n != 1 { - UnlockBucket(&mu) - panic(fmt.Sprintf("lock(%d)\n", n)) - } - atomic.AddInt32(&activity, -1) - UnlockBucket(&mu) - } - cdone <- true - }() - } - // Wait for all lockers to finish. - for i := 0; i < numLockers; i++ { - <-cdone - } -} - -func TestMapTopHashMutex_Store_NoLock(t *testing.T) { - mu := uint64(0) - testMapTopHashMutex_Store(t, &mu) -} - -func TestMapTopHashMutex_Store_WhileLocked(t *testing.T) { - mu := uint64(0) - LockBucket(&mu) - defer UnlockBucket(&mu) - testMapTopHashMutex_Store(t, &mu) -} - -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.Fatalf("top hash match for all zeros for index %d", i) - } - - prevOnes := bits.OnesCount64(*topHashes) - *topHashes = StoreTopHash(hash, *topHashes, i) - newOnes := bits.OnesCount64(*topHashes) - expectedInc := bits.OnesCount64(hash) + 1 - if newOnes != prevOnes+expectedInc { - t.Fatalf("unexpected bits change after store for index %d: %d, %d, %#b", - i, newOnes, prevOnes+expectedInc, *topHashes) - } - - if !TopHashMatch(hash, *topHashes, i) { - t.Fatalf("top hash mismatch after store for index %d: %#b", i, *topHashes) - } - } -} - -func TestMapTopHashMutex_Erase_NoLock(t *testing.T) { - mu := uint64(0) - testMapTopHashMutex_Erase(t, &mu) -} - -func TestMapTopHashMutex_Erase_WhileLocked(t *testing.T) { - mu := uint64(0) - LockBucket(&mu) - defer UnlockBucket(&mu) - testMapTopHashMutex_Erase(t, &mu) -} - -func testMapTopHashMutex_Erase(t *testing.T, topHashes *uint64) { - hash := uint64(0xababaaaaaaaaaaaa) // top hash is 1010_1011_1010_1011_1010 - for i := 0; i < EntriesPerMapBucket; i++ { - *topHashes = StoreTopHash(hash, *topHashes, i) - ones := bits.OnesCount64(*topHashes) - - *topHashes = EraseTopHash(*topHashes, i) - if TopHashMatch(hash, *topHashes, i) { - t.Fatalf("top hash match after erase for index %d: %#b", i, *topHashes) - } - - erasedBits := ones - bits.OnesCount64(*topHashes) - if erasedBits != 1 { - t.Fatalf("more than one bit changed after erase: %d, %d", i, erasedBits) - } - } -} - -func TestMapTopHashMutex_StoreAfterErase_NoLock(t *testing.T) { - mu := uint64(0) - testMapTopHashMutex_StoreAfterErase(t, &mu) -} - -func TestMapTopHashMutex_StoreAfterErase_WhileLocked(t *testing.T) { - mu := uint64(0) - LockBucket(&mu) - defer UnlockBucket(&mu) - testMapTopHashMutex_StoreAfterErase(t, &mu) -} - -func testMapTopHashMutex_StoreAfterErase(t *testing.T, topHashes *uint64) { - hashOne := uint64(0b1101_0100_1101_0100_1101_1111 << 40) // top hash is 1101_0100_1101_0100_1101 - hashTwo := uint64(0b1010_1011_1010_1011_1010_1111 << 40) // top hash is 1010_1011_1010_1011_1010 - idx := 2 - - *topHashes = StoreTopHash(hashOne, *topHashes, idx) - if !TopHashMatch(hashOne, *topHashes, idx) { - t.Fatalf("top hash mismatch for hash one: %#b, %#b", hashOne, *topHashes) - } - if TopHashMatch(hashTwo, *topHashes, idx) { - 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.Fatalf("top hash match for hash one: %#b, %#b", hashOne, *topHashes) - } - if !TopHashMatch(hashTwo, *topHashes, idx) { - t.Fatalf("top hash mismatch for hash two: %#b, %#b", hashTwo, *topHashes) - } -} - -func BenchmarkMap_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 := NewMap() - benchmarkMap(b, func(k string) (interface{}, bool) { - return m.Load(k) - }, func(k string, v interface{}) { - m.Store(k, v) - }, func(k string) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func BenchmarkMapStandard_NoWarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - if bc.readPercentage == 100 { - // This benchmark doesn't make sense without a warm-up. - continue - } - b.Run(bc.name, func(b *testing.B) { - var m sync.Map - benchmarkMap(b, func(k string) (interface{}, bool) { - return m.Load(k) - }, func(k string, v interface{}) { - m.Store(k, v) - }, func(k string) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func BenchmarkMap_WarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - b.Run(bc.name, func(b *testing.B) { - m := NewMap(WithPresize(benchmarkNumEntries)) - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i) - } - b.ResetTimer() - benchmarkMap(b, func(k string) (interface{}, bool) { - return m.Load(k) - }, func(k string, v interface{}) { - m.Store(k, v) - }, func(k string) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -// This is a nice scenario for sync.Map since a lot of updates -// will hit the readOnly part of the map. -func BenchmarkMapStandard_WarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - b.Run(bc.name, func(b *testing.B) { - var m sync.Map - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i) - } - b.ResetTimer() - benchmarkMap(b, func(k string) (interface{}, bool) { - return m.Load(k) - }, func(k string, v interface{}) { - m.Store(k, v) - }, func(k string) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func benchmarkMap( - b *testing.B, - loadFn func(k string) (interface{}, bool), - storeFn func(k string, v interface{}), - deleteFn func(k string), - readPercentage int, -) { - runParallel(b, func(pb *testing.PB) { - // convert percent to permille to support 99% case - storeThreshold := 10 * readPercentage - deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2) - for pb.Next() { - op := int(Fastrand() % 1000) - i := int(Fastrand() % benchmarkNumEntries) - if op >= deleteThreshold { - deleteFn(benchmarkKeys[i]) - } else if op >= storeThreshold { - storeFn(benchmarkKeys[i], i) - } else { - loadFn(benchmarkKeys[i]) - } - } - }) -} - -func BenchmarkMapRange(b *testing.B) { - m := NewMap() - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeys[i], i) - } - b.ResetTimer() - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - m.Range(func(key string, value interface{}) bool { - // Dereference the value to have an apple-to-apple - // comparison with MapOf.Range. - _ = value.(int) - foo++ - return true - }) - _ = foo - } - }) -} - -func BenchmarkMapRangeStandard(b *testing.B) { - var m sync.Map - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeys[i], i) - } - b.ResetTimer() - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - m.Range(func(key interface{}, value interface{}) bool { - // Dereference the key and the value to have an apple-to-apple - // comparison with MapOf.Range. - _, _ = key.(string), value.(int) - foo++ - return true - }) - _ = foo - } - }) -} diff --git a/xsync/mapof.go b/xsync/mapof.go deleted file mode 100644 index 222370c..0000000 --- a/xsync/mapof.go +++ /dev/null @@ -1,687 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync - -import ( - "fmt" - "math" - "sync" - "sync/atomic" - "unsafe" -) - -// MapOf is like a Go map[K]V but is safe for concurrent -// use by multiple goroutines without additional locking or -// coordination. It follows the interface of sync.Map with -// a number of valuable extensions like Compute or Size. -// -// 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. -type MapOf[K comparable, V any] struct { - totalGrowths int64 - totalShrinks int64 - resizing int64 // resize in progress flag; updated atomically - resizeMu sync.Mutex // only used along with resizeCond - resizeCond sync.Cond // used to wake up resize waiters (concurrent modifications) - table unsafe.Pointer // *mapOfTable - hasher func(K, uint64) uint64 - minTableLen int - growOnly bool -} - -type mapOfTable[K comparable, V any] struct { - buckets []bucketOfPadded - // striped counter for number of table entries; - // used to determine if a table shrinking is needed - // occupies min(buckets_memory/1024, 64KB) of memory - size []counterStripe - seed uint64 -} - -// bucketOfPadded is a CL-sized map bucket holding up to -// entriesPerMapBucket entries. -type bucketOfPadded struct { - //lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs - pad [cacheLineSize - unsafe.Sizeof(bucketOf{})]byte - bucketOf -} - -type bucketOf struct { - hashes [entriesPerMapBucket]uint64 - entries [entriesPerMapBucket]unsafe.Pointer // *entryOf - next unsafe.Pointer // *bucketOfPadded - mu sync.Mutex -} - -// entryOf is an immutable map entry. -type entryOf[K comparable, V any] struct { - key K - value V -} - -// NewMapOf creates a new MapOf instance configured with the given -// options. -func NewMapOf[K comparable, V any](options ...func(*MapConfig)) *MapOf[K, V] { - return newMapOf[K, V](makeHasher[K](), options...) -} - -// NewMapOfPresized creates a new MapOf instance with capacity enough -// to hold sizeHint entries. The capacity is treated as the minimal capacity -// meaning that the underlying hash table will never shrink to -// a smaller capacity. If sizeHint is zero or negative, the value -// is ignored. -// -// Deprecated: use NewMapOf in combination with WithPresize. -func NewMapOfPresized[K comparable, V any](sizeHint int) *MapOf[K, V] { - return NewMapOf[K, V](WithPresize(sizeHint)) -} - -func newMapOf[K comparable, V any]( - hasher func(K, uint64) uint64, - options ...func(*MapConfig), -) *MapOf[K, V] { - c := &MapConfig{ - sizeHint: defaultMinMapTableLen * entriesPerMapBucket, - } - for _, o := range options { - o(c) - } - - m := &MapOf[K, V]{} - m.resizeCond = *sync.NewCond(&m.resizeMu) - m.hasher = hasher - var table *mapOfTable[K, V] - if c.sizeHint <= defaultMinMapTableLen*entriesPerMapBucket { - table = newMapOfTable[K, V](defaultMinMapTableLen) - } else { - tableLen := nextPowOf2(uint32(c.sizeHint / entriesPerMapBucket)) - table = newMapOfTable[K, V](int(tableLen)) - } - m.minTableLen = len(table.buckets) - m.growOnly = c.growOnly - atomic.StorePointer(&m.table, unsafe.Pointer(table)) - return m -} - -func newMapOfTable[K comparable, V any](minTableLen int) *mapOfTable[K, V] { - buckets := make([]bucketOfPadded, minTableLen) - counterLen := minTableLen >> 10 - if counterLen < minMapCounterLen { - counterLen = minMapCounterLen - } else if counterLen > maxMapCounterLen { - counterLen = maxMapCounterLen - } - counter := make([]counterStripe, counterLen) - t := &mapOfTable[K, V]{ - buckets: buckets, - size: counter, - seed: makeSeed(), - } - return t -} - -// Load returns the value stored in the map for a key, or zero value -// of type V if no value is present. -// The ok result indicates whether value was found in the map. -func (m *MapOf[K, V]) Load(key K) (value V, ok bool) { - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - hash := shiftHash(m.hasher(key, table.seed)) - bidx := uint64(len(table.buckets)-1) & hash - b := &table.buckets[bidx] - for { - for i := 0; i < entriesPerMapBucket; i++ { - // We treat the hash code only as a hint, so there is no - // need to get an atomic snapshot. - h := atomic.LoadUint64(&b.hashes[i]) - if h == uint64(0) || h != hash { - continue - } - eptr := atomic.LoadPointer(&b.entries[i]) - if eptr == nil { - continue - } - e := (*entryOf[K, V])(eptr) - if e.key == key { - return e.value, true - } - } - bptr := atomic.LoadPointer(&b.next) - if bptr == nil { - return - } - b = (*bucketOfPadded)(bptr) - } -} - -// Store sets the value for a key. -func (m *MapOf[K, V]) Store(key K, value V) { - m.doCompute( - key, - func(V, bool) (V, bool) { - return value, false - }, - false, - 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[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { - return m.doCompute( - key, - func(V, bool) (V, bool) { - return value, false - }, - true, - false, - ) -} - -// LoadAndStore returns the existing value for the key if present, -// while setting the new value for the key. -// It stores the new value and returns the existing one, if present. -// 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.doCompute( - key, - func(V, bool) (V, bool) { - return value, false - }, - false, - false, - ) -} - -// LoadOrCompute returns the existing value for the key if present. -// Otherwise, it computes the value using the provided function and -// returns the computed value. The loaded result is true if the value -// was loaded, false if stored. -// -// This call locks a hash table bucket while the compute function -// is executed. It means that modifications on other entries in -// the bucket will be blocked until the valueFn executes. Consider -// this when the function includes long-running operations. -func (m *MapOf[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded bool) { - return m.doCompute( - key, - func(V, bool) (V, 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. -// -// This call locks a hash table bucket while the compute function -// is executed. It means that modifications on other entries in -// the bucket will be blocked until the valueFn executes. Consider -// this when the function includes long-running operations. -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, !computeOnly - } - } - // Write path. - for { - compute_attempt: - var ( - emptyb *bucketOfPadded - emptyidx int - hintNonEmpty int - ) - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - tableLen := len(table.buckets) - hash := shiftHash(m.hasher(key, table.seed)) - bidx := uint64(len(table.buckets)-1) & hash - rootb := &table.buckets[bidx] - rootb.mu.Lock() - // The following two checks must go in reverse to what's - // in the resize method. - if m.resizeInProgress() { - // Resize is in progress. Wait, then go for another attempt. - rootb.mu.Unlock() - m.waitForResize() - goto compute_attempt - } - if m.newerTableExists(table) { - // Someone resized the table. Go for another attempt. - rootb.mu.Unlock() - goto compute_attempt - } - b := rootb - for { - for i := 0; i < entriesPerMapBucket; i++ { - h := atomic.LoadUint64(&b.hashes[i]) - if h == uint64(0) { - if emptyb == nil { - emptyb = b - emptyidx = i - } - continue - } - if h != hash { - hintNonEmpty++ - continue - } - e := (*entryOf[K, V])(b.entries[i]) - if e.key == key { - if loadIfExists { - rootb.mu.Unlock() - return e.value, !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. - oldv := e.value - newv, del := valueFn(oldv, true) - if del { - // Deletion. - // First we update the hash, then the entry. - atomic.StoreUint64(&b.hashes[i], uint64(0)) - atomic.StorePointer(&b.entries[i], nil) - leftEmpty := false - if hintNonEmpty == 0 { - leftEmpty = isEmptyBucketOf(b) - } - rootb.mu.Unlock() - table.addSize(bidx, -1) - // Might need to shrink the table. - if leftEmpty { - m.resize(table, mapShrinkHint) - } - return oldv, !computeOnly - } - newe := new(entryOf[K, V]) - newe.key = key - newe.value = newv - atomic.StorePointer(&b.entries[i], unsafe.Pointer(newe)) - rootb.mu.Unlock() - if computeOnly { - // Compute expects the new value to be returned. - return newv, true - } - // LoadAndStore expects the old value to be returned. - return oldv, true - } - hintNonEmpty++ - } - if b.next == nil { - if emptyb != nil { - // Insertion into an existing bucket. - var zeroedV V - newValue, del := valueFn(zeroedV, false) - if del { - rootb.mu.Unlock() - return zeroedV, false - } - newe := new(entryOf[K, V]) - newe.key = key - newe.value = newValue - // First we update the hash, then the entry. - atomic.StoreUint64(&emptyb.hashes[emptyidx], hash) - atomic.StorePointer(&emptyb.entries[emptyidx], unsafe.Pointer(newe)) - rootb.mu.Unlock() - table.addSize(bidx, 1) - return newValue, computeOnly - } - growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor - if table.sumSize() > int64(growThreshold) { - // Need to grow the table. Then go for another attempt. - rootb.mu.Unlock() - m.resize(table, mapGrowHint) - goto compute_attempt - } - // Insertion into a new bucket. - var zeroedV V - newValue, del := valueFn(zeroedV, false) - if del { - rootb.mu.Unlock() - return newValue, false - } - // Create and append the bucket. - newb := new(bucketOfPadded) - newb.hashes[0] = hash - newe := new(entryOf[K, V]) - newe.key = key - newe.value = newValue - newb.entries[0] = unsafe.Pointer(newe) - atomic.StorePointer(&b.next, unsafe.Pointer(newb)) - rootb.mu.Unlock() - table.addSize(bidx, 1) - return newValue, computeOnly - } - b = (*bucketOfPadded)(b.next) - } - } -} - -func (m *MapOf[K, V]) newerTableExists(table *mapOfTable[K, V]) bool { - curTablePtr := atomic.LoadPointer(&m.table) - return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table)) -} - -func (m *MapOf[K, V]) resizeInProgress() bool { - return atomic.LoadInt64(&m.resizing) == 1 -} - -func (m *MapOf[K, V]) waitForResize() { - m.resizeMu.Lock() - for m.resizeInProgress() { - m.resizeCond.Wait() - } - m.resizeMu.Unlock() -} - -func (m *MapOf[K, V]) resize(knownTable *mapOfTable[K, V], hint mapResizeHint) { - knownTableLen := len(knownTable.buckets) - // Fast path for shrink attempts. - if hint == mapShrinkHint { - if m.growOnly || - m.minTableLen == knownTableLen || - knownTable.sumSize() > int64((knownTableLen*entriesPerMapBucket)/mapShrinkFraction) { - return - } - } - // Slow path. - if !atomic.CompareAndSwapInt64(&m.resizing, 0, 1) { - // Someone else started resize. Wait for it to finish. - m.waitForResize() - return - } - var newTable *mapOfTable[K, V] - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - tableLen := len(table.buckets) - switch hint { - case mapGrowHint: - // Grow the table with factor of 2. - atomic.AddInt64(&m.totalGrowths, 1) - newTable = newMapOfTable[K, V](tableLen << 1) - case mapShrinkHint: - shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction) - if tableLen > m.minTableLen && table.sumSize() <= shrinkThreshold { - // Shrink the table with factor of 2. - atomic.AddInt64(&m.totalShrinks, 1) - newTable = newMapOfTable[K, V](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 - } - case mapClearHint: - newTable = newMapOfTable[K, V](m.minTableLen) - default: - panic(fmt.Sprintf("unexpected resize hint: %d", hint)) - } - // Copy the data only if we're not clearing the map. - if hint != mapClearHint { - for i := 0; i < tableLen; i++ { - copied := copyBucketOf(&table.buckets[i], newTable, m.hasher) - newTable.addSizePlain(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() -} - -func copyBucketOf[K comparable, V any]( - b *bucketOfPadded, - destTable *mapOfTable[K, V], - hasher func(K, uint64) uint64, -) (copied int) { - rootb := b - rootb.mu.Lock() - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.entries[i] != nil { - e := (*entryOf[K, V])(b.entries[i]) - hash := shiftHash(hasher(e.key, destTable.seed)) - bidx := uint64(len(destTable.buckets)-1) & hash - destb := &destTable.buckets[bidx] - appendToBucketOf(hash, b.entries[i], destb) - copied++ - } - } - if b.next == nil { - rootb.mu.Unlock() - return - } - b = (*bucketOfPadded)(b.next) - } -} - -// 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, including entry -// creation, modification and deletion. However, the concurrent -// modification rule apply, i.e. the changes may be not reflected -// in the subsequently iterated entries. -func (m *MapOf[K, V]) Range(f func(key K, value V) bool) { - var zeroPtr unsafe.Pointer - // Pre-allocate array big enough to fit entries for most hash tables. - bentries := make([]unsafe.Pointer, 0, 16*entriesPerMapBucket) - tablep := atomic.LoadPointer(&m.table) - table := *(*mapOfTable[K, V])(tablep) - for i := range table.buckets { - rootb := &table.buckets[i] - b := rootb - // Prevent concurrent modifications and copy all entries into - // the intermediate slice. - rootb.mu.Lock() - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.entries[i] != nil { - bentries = append(bentries, b.entries[i]) - } - } - if b.next == nil { - rootb.mu.Unlock() - break - } - b = (*bucketOfPadded)(b.next) - } - // Call the function for all copied entries. - for j := range bentries { - entry := (*entryOf[K, V])(bentries[j]) - if !f(entry.key, entry.value) { - return - } - // Remove the reference to avoid preventing the copied - // entries from being GCed until this method finishes. - bentries[j] = zeroPtr - } - bentries = bentries[:0] - } -} - -// Clear deletes all keys and values currently stored in the map. -func (m *MapOf[K, V]) Clear() { - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - m.resize(table, mapClearHint) -} - -// Size returns current size of the map. -func (m *MapOf[K, V]) Size() int { - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - return int(table.sumSize()) -} - -func appendToBucketOf(hash uint64, entryPtr unsafe.Pointer, b *bucketOfPadded) { - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.entries[i] == nil { - b.hashes[i] = hash - b.entries[i] = entryPtr - return - } - } - if b.next == nil { - newb := new(bucketOfPadded) - newb.hashes[0] = hash - newb.entries[0] = entryPtr - b.next = unsafe.Pointer(newb) - return - } - b = (*bucketOfPadded)(b.next) - } -} - -func isEmptyBucketOf(rootb *bucketOfPadded) bool { - b := rootb - for { - for i := 0; i < entriesPerMapBucket; i++ { - if b.entries[i] != nil { - return false - } - } - if b.next == nil { - return true - } - b = (*bucketOfPadded)(b.next) - } -} - -func (table *mapOfTable[K, V]) addSize(bucketIdx uint64, delta int) { - cidx := uint64(len(table.size)-1) & bucketIdx - atomic.AddInt64(&table.size[cidx].c, int64(delta)) -} - -func (table *mapOfTable[K, V]) addSizePlain(bucketIdx uint64, delta int) { - cidx := uint64(len(table.size)-1) & bucketIdx - table.size[cidx].c += int64(delta) -} - -func (table *mapOfTable[K, V]) sumSize() int64 { - sum := int64(0) - for i := range table.size { - sum += atomic.LoadInt64(&table.size[i].c) - } - return sum -} - -func shiftHash(h uint64) uint64 { - // uint64(0) is a reserved value which stands for an empty slot. - if h == uint64(0) { - return uint64(1) - } - return h -} - -// O(N) operation; use for debug purposes only -func (m *MapOf[K, V]) stats() mapStats { - stats := mapStats{ - TotalGrowths: atomic.LoadInt64(&m.totalGrowths), - TotalShrinks: atomic.LoadInt64(&m.totalShrinks), - MinEntries: math.MaxInt32, - } - table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) - stats.RootBuckets = len(table.buckets) - stats.Counter = int(table.sumSize()) - stats.CounterLen = len(table.size) - for i := range table.buckets { - nentries := 0 - b := &table.buckets[i] - stats.TotalBuckets++ - for { - nentriesLocal := 0 - stats.Capacity += entriesPerMapBucket - for i := 0; i < entriesPerMapBucket; i++ { - if atomic.LoadPointer(&b.entries[i]) != nil { - stats.Size++ - nentriesLocal++ - } - } - nentries += nentriesLocal - if nentriesLocal == 0 { - stats.EmptyBuckets++ - } - if b.next == nil { - break - } - b = (*bucketOfPadded)(b.next) - stats.TotalBuckets++ - } - if nentries < stats.MinEntries { - stats.MinEntries = nentries - } - if nentries > stats.MaxEntries { - stats.MaxEntries = nentries - } - } - return stats -} diff --git a/xsync/mapof_helper.go b/xsync/mapof_helper.go deleted file mode 100644 index 4a09822..0000000 --- a/xsync/mapof_helper.go +++ /dev/null @@ -1,72 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync - -type HashMapOf[K comparable, V any] interface { - // 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. - Load(key K) (value V, ok bool) - - // Store sets the value for a key. - Store(key K, value V) - - // 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. - LoadOrStore(key K, value V) (actual V, loaded bool) - - // LoadAndStore returns the existing value for the key if present, - // while setting the new value for the key. - // It stores the new value and returns the existing one, if present. - // The loaded result is true if the existing value was loaded, - // false otherwise. - LoadAndStore(key K, value V) (actual V, loaded bool) - - // LoadOrCompute returns the existing value for the key if present. - // Otherwise, it computes the value using the provided function and - // returns the computed value. The loaded result is true if the value - // was loaded, false if stored. - LoadOrCompute(key K, valueFn func() V) (actual V, loaded 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. - Compute( - key K, - valueFn func(oldValue V, loaded bool) (newValue V, delete bool), - ) (actual V, ok bool) - - // LoadAndDelete deletes the value for a key, returning the previous - // value if any. The loaded result reports whether the key was - // present. - LoadAndDelete(key K) (value V, loaded bool) - - // Delete deletes the value for a key. - Delete(key K) - - // 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. - Range(f func(key K, value V) bool) - - // Clear deletes all keys and values currently stored in the map. - Clear() - - // Size returns current size of the map. - Size() int -} diff --git a/xsync/mapof_test.go b/xsync/mapof_test.go deleted file mode 100644 index 617807d..0000000 --- a/xsync/mapof_test.go +++ /dev/null @@ -1,1320 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync_test - -import ( - "math" - "math/rand" - "strconv" - "sync" - "sync/atomic" - "testing" - "time" - "unsafe" - - . "github.com/fufuok/utils/xsync" -) - -func TestMap_BucketOfStructSize(t *testing.T) { - size := unsafe.Sizeof(BucketOfPadded{}) - if size != 64 { - t.Fatalf("size of 64B (one cache line) is expected, got: %d", size) - } -} - -func TestMapOf_MissingEntry(t *testing.T) { - m := NewMapOf[string, string]() - v, ok := m.Load("foo") - if ok { - t.Fatalf("value was not expected: %v", v) - } - if deleted, loaded := m.LoadAndDelete("foo"); loaded { - t.Fatalf("value was not expected %v", deleted) - } - if actual, loaded := m.LoadOrStore("foo", "bar"); loaded { - t.Fatalf("value was not expected %v", actual) - } -} - -func TestMapOf_EmptyStringKey(t *testing.T) { - m := NewMapOf[string, string]() - m.Store("", "foobar") - v, ok := m.Load("") - if !ok { - t.Fatal("value was expected") - } - if v != "foobar" { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapOfStore_NilValue(t *testing.T) { - m := NewMapOf[string, *struct{}]() - m.Store("foo", nil) - v, ok := m.Load("foo") - if !ok { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapOfLoadOrStore_NilValue(t *testing.T) { - m := NewMapOf[string, *struct{}]() - m.LoadOrStore("foo", nil) - v, loaded := m.LoadOrStore("foo", nil) - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapOfLoadOrStore_NonNilValue(t *testing.T) { - type foo struct{} - m := NewMapOf[string, *foo]() - newv := &foo{} - v, loaded := m.LoadOrStore("foo", newv) - if loaded { - t.Fatal("no value was expected") - } - if v != newv { - t.Fatalf("value does not match: %v", v) - } - newv2 := &foo{} - v, loaded = m.LoadOrStore("foo", newv2) - if !loaded { - t.Fatal("value was expected") - } - if v != newv { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapOfLoadAndStore_NilValue(t *testing.T) { - m := NewMapOf[string, *struct{}]() - m.LoadAndStore("foo", nil) - v, loaded := m.LoadAndStore("foo", nil) - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } - v, loaded = m.Load("foo") - if !loaded { - t.Fatal("nil value was expected") - } - if v != nil { - t.Fatalf("value was not nil: %v", v) - } -} - -func TestMapOfLoadAndStore_NonNilValue(t *testing.T) { - m := NewMapOf[string, int]() - v1 := 1 - v, loaded := m.LoadAndStore("foo", v1) - if loaded { - t.Fatal("no value was expected") - } - if v != v1 { - t.Fatalf("value does not match: %v", v) - } - v2 := 2 - v, loaded = m.LoadAndStore("foo", v2) - if !loaded { - t.Fatal("value was expected") - } - if v != v1 { - t.Fatalf("value does not match: %v", v) - } - v, loaded = m.Load("foo") - if !loaded { - t.Fatal("value was expected") - } - if v != v2 { - t.Fatalf("value does not match: %v", v) - } -} - -func TestMapOfRange(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, 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.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value) - return false - } - met[key] += 1 - iters++ - return true - }) - if iters != numEntries { - t.Fatalf("got unexpected number of iterations: %d", iters) - } - for i := 0; i < numEntries; i++ { - if c := met[strconv.Itoa(i)]; c != 1 { - t.Fatalf("range did not iterate correctly over %d: %d", i, c) - } - } -} - -func TestMapOfRange_FalseReturned(t *testing.T) { - m := NewMapOf[string, 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.Fatalf("got unexpected number of iterations: %d", iters) - } -} - -func TestMapOfRange_NestedDelete(t *testing.T) { - const numEntries = 256 - m := NewMapOf[string, 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.Fatalf("value found for %d", i) - } - } -} - -func TestMapOfStringStore(t *testing.T) { - const numEntries = 128 - m := NewMapOf[string, 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.Fatalf("value not found for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapOfIntStore(t *testing.T) { - const numEntries = 128 - m := NewMapOf[int, int]() - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - for i := 0; i < numEntries; i++ { - v, ok := m.Load(i) - if !ok { - t.Fatalf("value not found for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapOfStore_StructKeys_IntValues(t *testing.T) { - const numEntries = 128 - m := NewMapOf[point, int]() - for i := 0; i < numEntries; i++ { - m.Store(point{int32(i), -int32(i)}, i) - } - for i := 0; i < numEntries; i++ { - v, ok := m.Load(point{int32(i), -int32(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 TestMapOfStore_StructKeys_StructValues(t *testing.T) { - const numEntries = 128 - m := NewMapOf[point, point]() - for i := 0; i < numEntries; i++ { - m.Store(point{int32(i), -int32(i)}, point{-int32(i), int32(i)}) - } - for i := 0; i < numEntries; i++ { - v, ok := m.Load(point{int32(i), -int32(i)}) - if !ok { - t.Fatalf("value not found for %d", i) - } - if v.x != -int32(i) { - t.Fatalf("x 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 TestMapOfStore_HashCodeCollisions(t *testing.T) { - const numEntries = 1000 - m := NewMapOfWithHasher[int, int](func(i int, _ uint64) uint64 { - // We intentionally use an awful hash function here to make sure - // that the map copes with key collisions. - return 42 - }, WithPresize(numEntries)) - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - for i := 0; i < numEntries; i++ { - v, ok := m.Load(i) - if !ok { - t.Fatalf("value not found for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapOfLoadOrStore(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, 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.Fatalf("value not found for %d", i) - } - } -} - -func TestMapOfLoadOrCompute(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, int]() - for i := 0; i < numEntries; i++ { - v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() int { - return i - }) - if loaded { - t.Fatalf("value not computed for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - for i := 0; i < numEntries; i++ { - v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() int { - return i - }) - if !loaded { - t.Fatalf("value not loaded for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } -} - -func TestMapOfLoadOrCompute_FunctionCalledOnce(t *testing.T) { - m := NewMapOf[int, int]() - for i := 0; i < 100; { - m.LoadOrCompute(i, func() (v int) { - v, i = i, i+1 - return v - }) - } - m.Range(func(k, v int) bool { - if k != v { - t.Fatalf("%dth key is not equal to value %d", k, v) - } - return true - }) -} - -func TestMapOfCompute(t *testing.T) { - m := NewMapOf[string, 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 TestMapOfStringStoreThenDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, 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.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfIntStoreThenDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[int32, int32]() - for i := 0; i < numEntries; i++ { - m.Store(int32(i), int32(i)) - } - for i := 0; i < numEntries; i++ { - m.Delete(int32(i)) - if _, ok := m.Load(int32(i)); ok { - t.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfStructStoreThenDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[point, string]() - for i := 0; i < numEntries; i++ { - m.Store(point{int32(i), 42}, strconv.Itoa(i)) - } - for i := 0; i < numEntries; i++ { - m.Delete(point{int32(i), 42}) - if _, ok := m.Load(point{int32(i), 42}); ok { - t.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfStringStoreThenLoadAndDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, int]() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - for i := 0; i < numEntries; 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.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfIntStoreThenLoadAndDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[int, int]() - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - for i := 0; i < numEntries; i++ { - if _, loaded := m.LoadAndDelete(i); !loaded { - t.Fatalf("value was not found for %d", i) - } - if _, ok := m.Load(i); ok { - t.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfStructStoreThenLoadAndDelete(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[point, int]() - for i := 0; i < numEntries; i++ { - m.Store(point{42, int32(i)}, i) - } - for i := 0; i < numEntries; i++ { - if _, loaded := m.LoadAndDelete(point{42, int32(i)}); !loaded { - t.Fatalf("value was not found for %d", i) - } - if _, ok := m.Load(point{42, int32(i)}); ok { - t.Fatalf("value was not expected for %d", i) - } - } -} - -func TestMapOfStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[int, int]() - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - - cdone := make(chan bool) - go func() { - for i := 0; i < numEntries; i++ { - m.Delete(i) - } - cdone <- true - }() - go func() { - for i := 0; i < numEntries; i++ { - m.Delete(i) - } - cdone <- true - }() - - // Wait for the goroutines to finish. - <-cdone - <-cdone - - stats := CollectMapOfStats(m) - if stats.RootBuckets != DefaultMinMapTableLen { - t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) - } -} - -func sizeBasedOnTypedRange(m *MapOf[string, int]) int { - size := 0 - m.Range(func(key string, value int) bool { - size++ - return true - }) - return size -} - -func TestMapOfSize(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, int]() - size := m.Size() - if size != 0 { - t.Fatalf("zero size expected: %d", size) - } - expectedSize := 0 - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - expectedSize++ - size := m.Size() - if size != expectedSize { - t.Fatalf("size of %d was expected, got: %d", expectedSize, size) - } - rsize := sizeBasedOnTypedRange(m) - if size != rsize { - t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) - } - } - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(i)) - expectedSize-- - size := m.Size() - if size != expectedSize { - t.Fatalf("size of %d was expected, got: %d", expectedSize, size) - } - rsize := sizeBasedOnTypedRange(m) - if size != rsize { - t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize) - } - } -} - -func TestMapOfClear(t *testing.T) { - const numEntries = 1000 - m := NewMapOf[string, int]() - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - size := m.Size() - if size != numEntries { - t.Fatalf("size of %d was expected, got: %d", numEntries, size) - } - m.Clear() - size = m.Size() - if size != 0 { - t.Fatalf("zero size was expected, got: %d", size) - } - rsize := sizeBasedOnTypedRange(m) - if rsize != 0 { - t.Fatalf("zero number of entries in Range was expected, got: %d", rsize) - } -} - -func assertMapOfCapacity[K comparable, V any](t *testing.T, m *MapOf[K, V], expectedCap int) { - stats := CollectMapOfStats(m) - if stats.Capacity != expectedCap { - t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity) - } -} - -func TestNewMapOfPresized(t *testing.T) { - assertMapOfCapacity(t, NewMapOf[string, string](), DefaultMinMapTableCap) - assertMapOfCapacity(t, NewMapOfPresized[string, string](0), DefaultMinMapTableCap) - assertMapOfCapacity(t, NewMapOf[string, string](WithPresize(0)), DefaultMinMapTableCap) - assertMapOfCapacity(t, NewMapOfPresized[string, string](-100), DefaultMinMapTableCap) - assertMapOfCapacity(t, NewMapOf[string, string](WithPresize(-100)), DefaultMinMapTableCap) - assertMapOfCapacity(t, NewMapOfPresized[string, string](500), 768) - assertMapOfCapacity(t, NewMapOf[string, string](WithPresize(500)), 768) - assertMapOfCapacity(t, NewMapOfPresized[int, int](1_000_000), 1_572_864) - assertMapOfCapacity(t, NewMapOf[int, int](WithPresize(1_000_000)), 1_572_864) - assertMapOfCapacity(t, NewMapOfPresized[point, point](100), 192) - assertMapOfCapacity(t, NewMapOf[point, point](WithPresize(100)), 192) -} - -func TestNewMapOfPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) { - const minTableLen = 1024 - const numEntries = minTableLen * EntriesPerMapBucket - m := NewMapOf[int, int](WithPresize(numEntries)) - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - - stats := CollectMapOfStats(m) - if stats.RootBuckets <= minTableLen { - t.Fatalf("table did not grow: %d", stats.RootBuckets) - } - - for i := 0; i < numEntries; i++ { - m.Delete(i) - } - - stats = CollectMapOfStats(m) - if stats.RootBuckets != minTableLen { - t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets) - } -} - -func TestNewMapOfGrowOnly_OnlyShrinksOnClear(t *testing.T) { - const minTableLen = 128 - const numEntries = minTableLen * EntriesPerMapBucket - m := NewMapOf[int, int](WithPresize(numEntries), WithGrowOnly()) - - stats := CollectMapOfStats(m) - initialTableLen := stats.RootBuckets - - for i := 0; i < 2*numEntries; i++ { - m.Store(i, i) - } - stats = CollectMapOfStats(m) - maxTableLen := stats.RootBuckets - if maxTableLen <= minTableLen { - t.Fatalf("table did not grow: %d", maxTableLen) - } - - for i := 0; i < numEntries; i++ { - m.Delete(i) - } - stats = CollectMapOfStats(m) - if stats.RootBuckets != maxTableLen { - t.Fatalf("table length was different from the expected: %d", stats.RootBuckets) - } - - m.Clear() - stats = CollectMapOfStats(m) - if stats.RootBuckets != initialTableLen { - t.Fatalf("table length was different from the initial: %d", stats.RootBuckets) - } -} - -func TestMapOfResize(t *testing.T) { - const numEntries = 100_000 - m := NewMapOf[string, int]() - - for i := 0; i < numEntries; i++ { - m.Store(strconv.Itoa(i), i) - } - stats := CollectMapOfStats(m) - if stats.Size != numEntries { - t.Fatalf("size was too small: %d", stats.Size) - } - expectedCapacity := int(math.RoundToEven(MapLoadFactor+1)) * stats.RootBuckets * EntriesPerMapBucket - if stats.Capacity > expectedCapacity { - t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) - } - if stats.RootBuckets <= DefaultMinMapTableLen { - t.Fatalf("table was too small: %d", stats.RootBuckets) - } - if stats.TotalGrowths == 0 { - t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths) - } - if stats.TotalShrinks > 0 { - t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks) - } - // This is useful when debugging table resize and occupancy. - // Use -v flag to see the output. - t.Log(stats.ToString()) - - for i := 0; i < numEntries; i++ { - m.Delete(strconv.Itoa(i)) - } - stats = CollectMapOfStats(m) - if stats.Size > 0 { - t.Fatalf("zero size was expected: %d", stats.Size) - } - expectedCapacity = stats.RootBuckets * EntriesPerMapBucket - if stats.Capacity != expectedCapacity { - t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity) - } - if stats.RootBuckets != DefaultMinMapTableLen { - t.Fatalf("table was too large: %d", stats.RootBuckets) - } - if stats.TotalShrinks == 0 { - t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks) - } - t.Log(stats.ToString()) -} - -func TestMapOfResize_CounterLenLimit(t *testing.T) { - const numEntries = 1_000_000 - m := NewMapOf[string, 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.Fatalf("size was too small: %d", stats.Size) - } - if stats.CounterLen != MaxMapCounterLen { - t.Fatalf("number of counter stripes was too large: %d, expected: %d", - stats.CounterLen, MaxMapCounterLen) - } -} - -func parallelSeqTypedResizer(t *testing.T, m *MapOf[int, int], numEntries int, positive bool, cdone chan bool) { - for i := 0; i < numEntries; i++ { - if positive { - m.Store(i, i) - } else { - m.Store(-i, -i) - } - } - cdone <- true -} - -func TestMapOfParallelResize_GrowOnly(t *testing.T) { - const numEntries = 100_000 - m := NewMapOf[int, int]() - cdone := make(chan bool) - go parallelSeqTypedResizer(t, m, numEntries, true, cdone) - go parallelSeqTypedResizer(t, m, numEntries, false, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - for i := -numEntries + 1; i < numEntries; i++ { - v, ok := m.Load(i) - if !ok { - t.Fatalf("value not found for %d", i) - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - if s := m.Size(); s != 2*numEntries-1 { - t.Fatalf("unexpected size: %v", s) - } -} - -func parallelRandTypedResizer(t *testing.T, m *MapOf[string, int], numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - coin := r.Int63n(2) - for j := 0; j < numEntries; j++ { - if coin == 1 { - m.Store(strconv.Itoa(j), j) - } else { - m.Delete(strconv.Itoa(j)) - } - } - } - cdone <- true -} - -func TestMapOfParallelResize(t *testing.T) { - const numIters = 1_000 - const numEntries = 2 * EntriesPerMapBucket * DefaultMinMapTableLen - m := NewMapOf[string, int]() - cdone := make(chan bool) - go parallelRandTypedResizer(t, m, numIters, numEntries, cdone) - go parallelRandTypedResizer(t, m, numIters, numEntries, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - for i := 0; i < numEntries; i++ { - v, ok := m.Load(strconv.Itoa(i)) - if !ok { - // The entry may be deleted and that's ok. - continue - } - if v != i { - t.Fatalf("values do not match for %d: %v", i, v) - } - } - s := m.Size() - if s > numEntries { - t.Fatalf("unexpected size: %v", s) - } - rs := sizeBasedOnTypedRange(m) - if s != rs { - t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) - } -} - -func parallelRandTypedClearer(t *testing.T, m *MapOf[string, int], numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - coin := r.Int63n(2) - for j := 0; j < numEntries; j++ { - if coin == 1 { - m.Store(strconv.Itoa(j), j) - } else { - m.Clear() - } - } - } - cdone <- true -} - -func TestMapOfParallelClear(t *testing.T) { - const numIters = 100 - const numEntries = 1_000 - m := NewMapOf[string, int]() - cdone := make(chan bool) - go parallelRandTypedClearer(t, m, numIters, numEntries, cdone) - go parallelRandTypedClearer(t, m, numIters, numEntries, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map size. - s := m.Size() - if s > numEntries { - t.Fatalf("unexpected size: %v", s) - } - rs := sizeBasedOnTypedRange(m) - if s != rs { - t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs) - } -} - -func parallelSeqTypedStorer(t *testing.T, m *MapOf[string, int], storeEach, numIters, numEntries int, cdone chan bool) { - for i := 0; i < numIters; i++ { - for j := 0; j < numEntries; j++ { - if storeEach == 0 || j%storeEach == 0 { - m.Store(strconv.Itoa(j), j) - // Due to atomic snapshots we must see a ""/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[string, 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[string, int], numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - j := r.Intn(numEntries) - 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[string, int], numIters, numEntries int, cdone chan bool) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numIters; i++ { - j := r.Intn(numEntries) - 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[string, int], numIters, numEntries int, cdone chan bool) { - for i := 0; i < numIters; i++ { - for j := 0; j < numEntries; j++ { - // Due to atomic snapshots we must either see no entry, or a ""/j 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[string, 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[string, 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 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 := NewMapOf[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 parallelTypedRangeStorer(t *testing.T, m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) { - for { - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - if atomic.LoadInt64(stopFlag) != 0 { - break - } - } - cdone <- true -} - -func parallelTypedRangeDeleter(t *testing.T, m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) { - for { - for i := 0; i < numEntries; i++ { - m.Delete(i) - } - if atomic.LoadInt64(stopFlag) != 0 { - break - } - } - cdone <- true -} - -func TestMapOfParallelRange(t *testing.T) { - const numEntries = 10_000 - m := NewMapOfPresized[int, int](numEntries) - for i := 0; i < numEntries; i++ { - m.Store(i, i) - } - // Start goroutines that would be storing and deleting items in parallel. - cdone := make(chan bool) - stopFlag := int64(0) - go parallelTypedRangeStorer(t, m, numEntries, &stopFlag, cdone) - go parallelTypedRangeDeleter(t, m, numEntries, &stopFlag, cdone) - // Iterate the map and verify that no duplicate keys were met. - met := make(map[int]int) - m.Range(func(key int, value int) bool { - if key != value { - t.Fatalf("got unexpected value for key %d: %d", key, value) - return false - } - met[key] += 1 - return true - }) - if len(met) == 0 { - t.Fatal("no entries were met when iterating") - } - for k, c := range met { - if c != 1 { - t.Fatalf("met key %d multiple times: %d", k, c) - } - } - // Make sure that both goroutines finish. - atomic.StoreInt64(&stopFlag, 1) - <-cdone - <-cdone -} - -func parallelTypedShrinker(t *testing.T, m *MapOf[uint64, *point], numIters, numEntries int, stopFlag *int64, cdone chan bool) { - for i := 0; i < numIters; i++ { - for j := 0; j < numEntries; j++ { - if p, loaded := m.LoadOrStore(uint64(j), &point{int32(j), int32(j)}); loaded { - t.Errorf("value was present for %d: %v", j, p) - } - } - for j := 0; j < numEntries; j++ { - m.Delete(uint64(j)) - } - } - atomic.StoreInt64(stopFlag, 1) - cdone <- true -} - -func parallelTypedUpdater(t *testing.T, m *MapOf[uint64, *point], idx int, stopFlag *int64, cdone chan bool) { - for atomic.LoadInt64(stopFlag) != 1 { - sleepUs := int(Fastrand() % 10) - if p, loaded := m.LoadOrStore(uint64(idx), &point{int32(idx), int32(idx)}); loaded { - t.Errorf("value was present for %d: %v", idx, p) - } - time.Sleep(time.Duration(sleepUs) * time.Microsecond) - if _, ok := m.Load(uint64(idx)); !ok { - t.Errorf("value was not found for %d", idx) - } - m.Delete(uint64(idx)) - } - cdone <- true -} - -func TestMapOfDoesNotLoseEntriesOnResize(t *testing.T) { - const numIters = 10_000 - const numEntries = 128 - m := NewMapOf[uint64, *point]() - cdone := make(chan bool) - stopFlag := int64(0) - go parallelTypedShrinker(t, m, numIters, numEntries, &stopFlag, cdone) - go parallelTypedUpdater(t, m, numEntries, &stopFlag, cdone) - // Wait for the goroutines to finish. - <-cdone - <-cdone - // Verify map contents. - if s := m.Size(); s != 0 { - t.Fatalf("map is not empty: %d", s) - } -} - -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[string, int]() - benchmarkMapOfStringKeys(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 := NewMapOfPresized[string, int](benchmarkNumEntries) - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i) - } - b.ResetTimer() - benchmarkMapOfStringKeys(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 benchmarkMapOfStringKeys( - b *testing.B, - loadFn func(k string) (int, bool), - storeFn func(k string, v int), - deleteFn func(k string), - readPercentage int, -) { - runParallel(b, func(pb *testing.PB) { - // convert percent to permille to support 99% case - storeThreshold := 10 * readPercentage - deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2) - for pb.Next() { - op := int(Fastrand() % 1000) - i := int(Fastrand() % benchmarkNumEntries) - if op >= deleteThreshold { - deleteFn(benchmarkKeys[i]) - } else if op >= storeThreshold { - storeFn(benchmarkKeys[i], i) - } else { - loadFn(benchmarkKeys[i]) - } - } - }) -} - -func BenchmarkMapOfInt_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, int]() - benchmarkMapOfIntKeys(b, func(k int) (int, bool) { - return m.Load(k) - }, func(k int, v int) { - m.Store(k, v) - }, func(k int) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func BenchmarkMapOfInt_WarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - b.Run(bc.name, func(b *testing.B) { - m := NewMapOfPresized[int, int](benchmarkNumEntries) - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(i, i) - } - b.ResetTimer() - benchmarkMapOfIntKeys(b, func(k int) (int, bool) { - return m.Load(k) - }, func(k int, v int) { - m.Store(k, v) - }, func(k int) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func BenchmarkIntMapStandard_NoWarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - if bc.readPercentage == 100 { - // This benchmark doesn't make sense without a warm-up. - continue - } - b.Run(bc.name, func(b *testing.B) { - var m sync.Map - benchmarkMapOfIntKeys(b, func(k int) (value int, ok bool) { - v, ok := m.Load(k) - if ok { - return v.(int), ok - } else { - return 0, false - } - }, func(k int, v int) { - m.Store(k, v) - }, func(k int) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -// This is a nice scenario for sync.Map since a lot of updates -// will hit the readOnly part of the map. -func BenchmarkIntMapStandard_WarmUp(b *testing.B) { - for _, bc := range benchmarkCases { - b.Run(bc.name, func(b *testing.B) { - var m sync.Map - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(i, i) - } - b.ResetTimer() - benchmarkMapOfIntKeys(b, func(k int) (value int, ok bool) { - v, ok := m.Load(k) - if ok { - return v.(int), ok - } else { - return 0, false - } - }, func(k int, v int) { - m.Store(k, v) - }, func(k int) { - m.Delete(k) - }, bc.readPercentage) - }) - } -} - -func benchmarkMapOfIntKeys( - b *testing.B, - loadFn func(k int) (int, bool), - storeFn func(k int, v int), - deleteFn func(k int), - readPercentage int, -) { - runParallel(b, func(pb *testing.PB) { - // convert percent to permille to support 99% case - storeThreshold := 10 * readPercentage - deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2) - for pb.Next() { - op := int(Fastrand() % 1000) - i := int(Fastrand() % benchmarkNumEntries) - if op >= deleteThreshold { - deleteFn(i) - } else if op >= storeThreshold { - storeFn(i, i) - } else { - loadFn(i) - } - } - }) -} - -func BenchmarkMapOfRange(b *testing.B) { - m := NewMapOfPresized[string, int](benchmarkNumEntries) - for i := 0; i < benchmarkNumEntries; i++ { - m.Store(benchmarkKeys[i], i) - } - b.ResetTimer() - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - m.Range(func(key string, value int) bool { - foo++ - return true - }) - _ = foo - } - }) -} diff --git a/xsync/mpmcqueue.go b/xsync/mpmcqueue.go deleted file mode 100644 index 96584e6..0000000 --- a/xsync/mpmcqueue.go +++ /dev/null @@ -1,137 +0,0 @@ -package xsync - -import ( - "runtime" - "sync/atomic" - "unsafe" -) - -// A MPMCQueue is a bounded multi-producer multi-consumer concurrent -// queue. -// -// MPMCQueue instances must be created with NewMPMCQueue function. -// A MPMCQueue must not be copied after first use. -// -// Based on the data structure from the following C++ library: -// https://github.com/rigtorp/MPMCQueue -type MPMCQueue struct { - cap uint64 - head uint64 - //lint:ignore U1000 prevents false sharing - hpad [cacheLineSize - 8]byte - tail uint64 - //lint:ignore U1000 prevents false sharing - tpad [cacheLineSize - 8]byte - slots []slotPadded -} - -type slotPadded struct { - slot - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - unsafe.Sizeof(slot{})]byte -} - -type slot struct { - turn uint64 - item interface{} -} - -// NewMPMCQueue creates a new MPMCQueue instance with the given -// capacity. -func NewMPMCQueue(capacity int) *MPMCQueue { - if capacity < 1 { - panic("capacity must be positive number") - } - return &MPMCQueue{ - cap: uint64(capacity), - slots: make([]slotPadded, capacity), - } -} - -// Enqueue inserts the given item into the queue. -// Blocks, if the queue is full. -func (q *MPMCQueue) Enqueue(item interface{}) { - head := atomic.AddUint64(&q.head, 1) - 1 - slot := &q.slots[q.idx(head)] - turn := q.turn(head) * 2 - for atomic.LoadUint64(&slot.turn) != turn { - runtime.Gosched() - } - slot.item = item - atomic.StoreUint64(&slot.turn, turn+1) -} - -// Dequeue retrieves and removes the item from the head of the queue. -// Blocks, if the queue is empty. -func (q *MPMCQueue) Dequeue() interface{} { - tail := atomic.AddUint64(&q.tail, 1) - 1 - slot := &q.slots[q.idx(tail)] - turn := q.turn(tail)*2 + 1 - for atomic.LoadUint64(&slot.turn) != turn { - runtime.Gosched() - } - item := slot.item - slot.item = nil - atomic.StoreUint64(&slot.turn, turn+1) - return item -} - -// TryEnqueue inserts the given item into the queue. Does not block -// and returns immediately. The result indicates that the queue isn't -// full and the item was inserted. -func (q *MPMCQueue) TryEnqueue(item interface{}) bool { - head := atomic.LoadUint64(&q.head) - for { - slot := &q.slots[q.idx(head)] - turn := q.turn(head) * 2 - if atomic.LoadUint64(&slot.turn) == turn { - if atomic.CompareAndSwapUint64(&q.head, head, head+1) { - slot.item = item - atomic.StoreUint64(&slot.turn, turn+1) - return true - } - } else { - prevHead := head - head = atomic.LoadUint64(&q.head) - if head == prevHead { - return false - } - } - runtime.Gosched() - } -} - -// TryDequeue retrieves and removes the item from the head of the -// queue. Does not block and returns immediately. The ok result -// indicates that the queue isn't empty and an item was retrieved. -func (q *MPMCQueue) TryDequeue() (item interface{}, ok bool) { - tail := atomic.LoadUint64(&q.tail) - for { - slot := &q.slots[q.idx(tail)] - turn := q.turn(tail)*2 + 1 - if atomic.LoadUint64(&slot.turn) == turn { - if atomic.CompareAndSwapUint64(&q.tail, tail, tail+1) { - item = slot.item - ok = true - slot.item = nil - atomic.StoreUint64(&slot.turn, turn+1) - return - } - } else { - prevTail := tail - tail = atomic.LoadUint64(&q.tail) - if tail == prevTail { - return - } - } - runtime.Gosched() - } -} - -func (q *MPMCQueue) idx(i uint64) uint64 { - return i % q.cap -} - -func (q *MPMCQueue) turn(i uint64) uint64 { - return i / q.cap -} diff --git a/xsync/mpmcqueue_test.go b/xsync/mpmcqueue_test.go deleted file mode 100644 index 964f794..0000000 --- a/xsync/mpmcqueue_test.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright notice. The following tests are partially based on -// the following file from the Go Programming Language core repo: -// https://github.com/golang/go/blob/831f9376d8d730b16fb33dfd775618dffe13ce7a/src/runtime/chan_test.go - -package xsync_test - -import ( - "runtime" - "sync" - "sync/atomic" - "testing" - "time" - - . "github.com/fufuok/utils/xsync" -) - -func TestQueue_InvalidSize(t *testing.T) { - defer func() { recover() }() - NewMPMCQueue(0) - t.Fatal("no panic detected") -} - -func TestQueueEnqueueDequeue(t *testing.T) { - q := NewMPMCQueue(10) - for i := 0; i < 10; i++ { - q.Enqueue(i) - } - for i := 0; i < 10; i++ { - if got := q.Dequeue(); got != i { - t.Fatalf("got %v, want %d", got, i) - } - } -} - -func TestQueueEnqueueBlocksOnFull(t *testing.T) { - q := NewMPMCQueue(1) - q.Enqueue("foo") - cdone := make(chan bool) - flag := int32(0) - go func() { - q.Enqueue("bar") - if atomic.LoadInt32(&flag) == 0 { - t.Error("enqueue on full queue didn't wait for dequeue") - } - cdone <- true - }() - time.Sleep(50 * time.Millisecond) - atomic.StoreInt32(&flag, 1) - if got := q.Dequeue(); got != "foo" { - t.Fatalf("got %v, want foo", got) - } - <-cdone -} - -func TestQueueDequeueBlocksOnEmpty(t *testing.T) { - q := NewMPMCQueue(2) - cdone := make(chan bool) - flag := int32(0) - go func() { - q.Dequeue() - if atomic.LoadInt32(&flag) == 0 { - t.Error("dequeue on empty queue didn't wait for enqueue") - } - cdone <- true - }() - time.Sleep(50 * time.Millisecond) - atomic.StoreInt32(&flag, 1) - q.Enqueue("foobar") - <-cdone -} - -func TestQueueTryEnqueueDequeue(t *testing.T) { - q := NewMPMCQueue(10) - for i := 0; i < 10; i++ { - if !q.TryEnqueue(i) { - t.Fatalf("failed to enqueue for %d", i) - } - } - for i := 0; i < 10; i++ { - if got, ok := q.TryDequeue(); !ok || got != i { - t.Fatalf("got %v, want %d, for status %v", got, i, ok) - } - } -} - -func TestQueueTryEnqueueOnFull(t *testing.T) { - q := NewMPMCQueue(1) - if !q.TryEnqueue("foo") { - t.Error("failed to enqueue initial item") - } - if q.TryEnqueue("bar") { - t.Error("got success for enqueue on full queue") - } -} - -func TestQueueTryDequeueBlocksOnEmpty(t *testing.T) { - q := NewMPMCQueue(2) - if _, ok := q.TryDequeue(); ok { - t.Error("got success for enqueue on empty queue") - } -} - -func hammerQueueBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) { - runtime.GOMAXPROCS(gomaxprocs) - q := NewMPMCQueue(numThreads) - startwg := sync.WaitGroup{} - startwg.Add(1) - csum := make(chan int, numThreads) - // Start producers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - for j := n; j < numOps; j += numThreads { - q.Enqueue(j) - } - }(i) - } - // Start consumers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - sum := 0 - for j := n; j < numOps; j += numThreads { - item := q.Dequeue() - sum += item.(int) - } - csum <- sum - }(i) - } - startwg.Done() - // Wait for all the sums from producers. - sum := 0 - for i := 0; i < numThreads; i++ { - s := <-csum - sum += s - } - // Assert the total sum. - expectedSum := numOps * (numOps - 1) / 2 - if sum != expectedSum { - t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d", - numOps, numThreads, sum, expectedSum) - } -} - -func TestQueueBlockingCalls(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) - n := 100 - if testing.Short() { - n = 10 - } - hammerQueueBlockingCalls(t, 1, 100*n, n) - hammerQueueBlockingCalls(t, 1, 1000*n, 10*n) - hammerQueueBlockingCalls(t, 4, 100*n, n) - hammerQueueBlockingCalls(t, 4, 1000*n, 10*n) - hammerQueueBlockingCalls(t, 8, 100*n, n) - hammerQueueBlockingCalls(t, 8, 1000*n, 10*n) -} - -func hammerQueueNonBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) { - runtime.GOMAXPROCS(gomaxprocs) - q := NewMPMCQueue(numThreads) - startwg := sync.WaitGroup{} - startwg.Add(1) - csum := make(chan int, numThreads) - // Start producers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - for j := n; j < numOps; j += numThreads { - for !q.TryEnqueue(j) { - // busy spin until success - } - } - }(i) - } - // Start consumers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - sum := 0 - for j := n; j < numOps; j += numThreads { - var ( - item interface{} - ok bool - ) - for { - // busy spin until success - if item, ok = q.TryDequeue(); ok { - sum += item.(int) - break - } - } - } - csum <- sum - }(i) - } - startwg.Done() - // Wait for all the sums from producers. - sum := 0 - for i := 0; i < numThreads; i++ { - s := <-csum - sum += s - } - // Assert the total sum. - expectedSum := numOps * (numOps - 1) / 2 - if sum != expectedSum { - t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d", - numOps, numThreads, sum, expectedSum) - } -} - -func TestQueueNonBlockingCalls(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) - n := 10 - if testing.Short() { - n = 1 - } - hammerQueueNonBlockingCalls(t, 1, n, n) - hammerQueueNonBlockingCalls(t, 2, 10*n, 2*n) - hammerQueueNonBlockingCalls(t, 4, 100*n, 4*n) -} - -func benchmarkQueueProdCons(b *testing.B, queueSize, localWork int) { - callsPerSched := queueSize - procs := runtime.GOMAXPROCS(-1) / 2 - if procs == 0 { - procs = 1 - } - N := int32(b.N / callsPerSched) - c := make(chan bool, 2*procs) - q := NewMPMCQueue(queueSize) - for p := 0; p < procs; p++ { - go func() { - foo := 0 - for atomic.AddInt32(&N, -1) >= 0 { - for g := 0; g < callsPerSched; g++ { - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - q.Enqueue(1) - } - } - q.Enqueue(0) - c <- foo == 42 - }() - go func() { - foo := 0 - for { - v := q.Dequeue().(int) - if v == 0 { - break - } - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - } - c <- foo == 42 - }() - } - for p := 0; p < procs; p++ { - <-c - <-c - } -} - -func BenchmarkQueueProdCons(b *testing.B) { - benchmarkQueueProdCons(b, 1000, 0) -} - -func BenchmarkQueueProdConsWork100(b *testing.B) { - benchmarkQueueProdCons(b, 1000, 100) -} - -func benchmarkChanProdCons(b *testing.B, chanSize, localWork int) { - callsPerSched := chanSize - procs := runtime.GOMAXPROCS(-1) / 2 - if procs == 0 { - procs = 1 - } - N := int32(b.N / callsPerSched) - c := make(chan bool, 2*procs) - myc := make(chan int, chanSize) - for p := 0; p < procs; p++ { - go func() { - foo := 0 - for atomic.AddInt32(&N, -1) >= 0 { - for g := 0; g < callsPerSched; g++ { - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - myc <- 1 - } - } - myc <- 0 - c <- foo == 42 - }() - go func() { - foo := 0 - for { - v := <-myc - if v == 0 { - break - } - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - } - c <- foo == 42 - }() - } - for p := 0; p < procs; p++ { - <-c - <-c - } -} - -func BenchmarkChanProdCons(b *testing.B) { - benchmarkChanProdCons(b, 1000, 0) -} - -func BenchmarkChanProdConsWork100(b *testing.B) { - benchmarkChanProdCons(b, 1000, 100) -} diff --git a/xsync/mpmcqueueof.go b/xsync/mpmcqueueof.go deleted file mode 100644 index 38a8fa3..0000000 --- a/xsync/mpmcqueueof.go +++ /dev/null @@ -1,150 +0,0 @@ -//go:build go1.19 -// +build go1.19 - -package xsync - -import ( - "runtime" - "sync/atomic" - "unsafe" -) - -// A MPMCQueueOf is a bounded multi-producer multi-consumer concurrent -// queue. It's a generic version of MPMCQueue. -// -// MPMCQueue instances must be created with NewMPMCQueueOf function. -// A MPMCQueueOf must not be copied after first use. -// -// Based on the data structure from the following C++ library: -// https://github.com/rigtorp/MPMCQueue -type MPMCQueueOf[I any] struct { - cap uint64 - head uint64 - //lint:ignore U1000 prevents false sharing - hpad [cacheLineSize - 8]byte - tail uint64 - //lint:ignore U1000 prevents false sharing - tpad [cacheLineSize - 8]byte - slots []slotOfPadded[I] -} - -type slotOfPadded[I any] struct { - slotOf[I] - // Unfortunately, proper padding like the below one: - // - // pad [cacheLineSize - (unsafe.Sizeof(slotOf[I]{}) % cacheLineSize)]byte - // - // won't compile, so here we add a best-effort padding for items up to - // 56 bytes size. - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte -} - -type slotOf[I any] struct { - // atomic.Uint64 is used here to get proper 8 byte alignment on - // 32-bit archs. - turn atomic.Uint64 - item I -} - -// NewMPMCQueueOf creates a new MPMCQueueOf instance with the given -// capacity. -func NewMPMCQueueOf[I any](capacity int) *MPMCQueueOf[I] { - if capacity < 1 { - panic("capacity must be positive number") - } - return &MPMCQueueOf[I]{ - cap: uint64(capacity), - slots: make([]slotOfPadded[I], capacity), - } -} - -// Enqueue inserts the given item into the queue. -// Blocks, if the queue is full. -func (q *MPMCQueueOf[I]) Enqueue(item I) { - head := atomic.AddUint64(&q.head, 1) - 1 - slot := &q.slots[q.idx(head)] - turn := q.turn(head) * 2 - for slot.turn.Load() != turn { - runtime.Gosched() - } - slot.item = item - slot.turn.Store(turn + 1) -} - -// Dequeue retrieves and removes the item from the head of the queue. -// Blocks, if the queue is empty. -func (q *MPMCQueueOf[I]) Dequeue() I { - var zeroedI I - tail := atomic.AddUint64(&q.tail, 1) - 1 - slot := &q.slots[q.idx(tail)] - turn := q.turn(tail)*2 + 1 - for slot.turn.Load() != turn { - runtime.Gosched() - } - item := slot.item - slot.item = zeroedI - slot.turn.Store(turn + 1) - return item -} - -// TryEnqueue inserts the given item into the queue. Does not block -// and returns immediately. The result indicates that the queue isn't -// full and the item was inserted. -func (q *MPMCQueueOf[I]) TryEnqueue(item I) bool { - head := atomic.LoadUint64(&q.head) - for { - slot := &q.slots[q.idx(head)] - turn := q.turn(head) * 2 - if slot.turn.Load() == turn { - if atomic.CompareAndSwapUint64(&q.head, head, head+1) { - slot.item = item - slot.turn.Store(turn + 1) - return true - } - } else { - prevHead := head - head = atomic.LoadUint64(&q.head) - if head == prevHead { - return false - } - } - runtime.Gosched() - } -} - -// TryDequeue retrieves and removes the item from the head of the -// queue. Does not block and returns immediately. The ok result -// indicates that the queue isn't empty and an item was retrieved. -func (q *MPMCQueueOf[I]) TryDequeue() (item I, ok bool) { - tail := atomic.LoadUint64(&q.tail) - for { - slot := &q.slots[q.idx(tail)] - turn := q.turn(tail)*2 + 1 - if slot.turn.Load() == turn { - if atomic.CompareAndSwapUint64(&q.tail, tail, tail+1) { - var zeroedI I - item = slot.item - ok = true - slot.item = zeroedI - slot.turn.Store(turn + 1) - return - } - } else { - prevTail := tail - tail = atomic.LoadUint64(&q.tail) - if tail == prevTail { - return - } - } - runtime.Gosched() - } -} - -func (q *MPMCQueueOf[I]) idx(i uint64) uint64 { - return i % q.cap -} - -func (q *MPMCQueueOf[I]) turn(i uint64) uint64 { - return i / q.cap -} diff --git a/xsync/mpmcqueueof_test.go b/xsync/mpmcqueueof_test.go deleted file mode 100644 index 2e3d45d..0000000 --- a/xsync/mpmcqueueof_test.go +++ /dev/null @@ -1,326 +0,0 @@ -//go:build go1.19 -// +build go1.19 - -// Copyright notice. The following tests are partially based on -// the following file from the Go Programming Language core repo: -// https://github.com/golang/go/blob/831f9376d8d730b16fb33dfd775618dffe13ce7a/src/runtime/chan_test.go - -package xsync_test - -import ( - "runtime" - "strconv" - "sync" - "sync/atomic" - "testing" - "time" - - . "github.com/fufuok/utils/xsync" -) - -func TestQueueOf_InvalidSize(t *testing.T) { - defer func() { recover() }() - NewMPMCQueueOf[int](0) - t.Fatal("no panic detected") -} - -func TestQueueOfEnqueueDequeueInt(t *testing.T) { - q := NewMPMCQueueOf[int](10) - for i := 0; i < 10; i++ { - q.Enqueue(i) - } - for i := 0; i < 10; i++ { - if got := q.Dequeue(); got != i { - t.Fatalf("got %v, want %d", got, i) - } - } -} - -func TestQueueOfEnqueueDequeueString(t *testing.T) { - q := NewMPMCQueueOf[string](10) - for i := 0; i < 10; i++ { - q.Enqueue(strconv.Itoa(i)) - } - for i := 0; i < 10; i++ { - if got := q.Dequeue(); got != strconv.Itoa(i) { - t.Fatalf("got %v, want %d", got, i) - } - } -} - -func TestQueueOfEnqueueDequeueStruct(t *testing.T) { - type foo struct { - bar int - baz int - } - q := NewMPMCQueueOf[foo](10) - for i := 0; i < 10; i++ { - q.Enqueue(foo{i, i}) - } - for i := 0; i < 10; i++ { - if got := q.Dequeue(); got.bar != i || got.baz != i { - t.Fatalf("got %v, want %d", got, i) - } - } -} - -func TestQueueOfEnqueueDequeueStructRef(t *testing.T) { - type foo struct { - bar int - baz int - } - q := NewMPMCQueueOf[*foo](11) - for i := 0; i < 10; i++ { - q.Enqueue(&foo{i, i}) - } - q.Enqueue(nil) - for i := 0; i < 10; i++ { - if got := q.Dequeue(); got.bar != i || got.baz != i { - t.Fatalf("got %v, want %d", got, i) - } - } - if last := q.Dequeue(); last != nil { - t.Fatalf("got %v, want nil", last) - } -} - -func TestQueueOfEnqueueBlocksOnFull(t *testing.T) { - q := NewMPMCQueueOf[string](1) - q.Enqueue("foo") - cdone := make(chan bool) - flag := int32(0) - go func() { - q.Enqueue("bar") - if atomic.LoadInt32(&flag) == 0 { - t.Error("enqueue on full queue didn't wait for dequeue") - } - cdone <- true - }() - time.Sleep(50 * time.Millisecond) - atomic.StoreInt32(&flag, 1) - if got := q.Dequeue(); got != "foo" { - t.Fatalf("got %v, want foo", got) - } - <-cdone -} - -func TestQueueOfDequeueBlocksOnEmpty(t *testing.T) { - q := NewMPMCQueueOf[string](2) - cdone := make(chan bool) - flag := int32(0) - go func() { - q.Dequeue() - if atomic.LoadInt32(&flag) == 0 { - t.Error("dequeue on empty queue didn't wait for enqueue") - } - cdone <- true - }() - time.Sleep(50 * time.Millisecond) - atomic.StoreInt32(&flag, 1) - q.Enqueue("foobar") - <-cdone -} - -func TestQueueOfTryEnqueueDequeue(t *testing.T) { - q := NewMPMCQueueOf[int](10) - for i := 0; i < 10; i++ { - if !q.TryEnqueue(i) { - t.Fatalf("failed to enqueue for %d", i) - } - } - for i := 0; i < 10; i++ { - if got, ok := q.TryDequeue(); !ok || got != i { - t.Fatalf("got %v, want %d, for status %v", got, i, ok) - } - } -} - -func TestQueueOfTryEnqueueOnFull(t *testing.T) { - q := NewMPMCQueueOf[string](1) - if !q.TryEnqueue("foo") { - t.Error("failed to enqueue initial item") - } - if q.TryEnqueue("bar") { - t.Error("got success for enqueue on full queue") - } -} - -func TestQueueOfTryDequeueBlocksOnEmpty(t *testing.T) { - q := NewMPMCQueueOf[int](2) - if _, ok := q.TryDequeue(); ok { - t.Error("got success for enqueue on empty queue") - } -} - -func hammerQueueOfBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) { - runtime.GOMAXPROCS(gomaxprocs) - q := NewMPMCQueueOf[int](numThreads) - startwg := sync.WaitGroup{} - startwg.Add(1) - csum := make(chan int, numThreads) - // Start producers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - for j := n; j < numOps; j += numThreads { - q.Enqueue(j) - } - }(i) - } - // Start consumers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - sum := 0 - for j := n; j < numOps; j += numThreads { - item := q.Dequeue() - sum += item - } - csum <- sum - }(i) - } - startwg.Done() - // Wait for all the sums from producers. - sum := 0 - for i := 0; i < numThreads; i++ { - s := <-csum - sum += s - } - // Assert the total sum. - expectedSum := numOps * (numOps - 1) / 2 - if sum != expectedSum { - t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d", - numOps, numThreads, sum, expectedSum) - } -} - -func TestQueueOfBlockingCalls(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) - n := 100 - if testing.Short() { - n = 10 - } - hammerQueueOfBlockingCalls(t, 1, 100*n, n) - hammerQueueOfBlockingCalls(t, 1, 1000*n, 10*n) - hammerQueueOfBlockingCalls(t, 4, 100*n, n) - hammerQueueOfBlockingCalls(t, 4, 1000*n, 10*n) - hammerQueueOfBlockingCalls(t, 8, 100*n, n) - hammerQueueOfBlockingCalls(t, 8, 1000*n, 10*n) -} - -func hammerQueueOfNonBlockingCalls(t *testing.T, gomaxprocs, numOps, numThreads int) { - runtime.GOMAXPROCS(gomaxprocs) - q := NewMPMCQueueOf[int](numThreads) - startwg := sync.WaitGroup{} - startwg.Add(1) - csum := make(chan int, numThreads) - // Start producers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - for j := n; j < numOps; j += numThreads { - for !q.TryEnqueue(j) { - // busy spin until success - } - } - }(i) - } - // Start consumers. - for i := 0; i < numThreads; i++ { - go func(n int) { - startwg.Wait() - sum := 0 - for j := n; j < numOps; j += numThreads { - var ( - item int - ok bool - ) - for { - // busy spin until success - if item, ok = q.TryDequeue(); ok { - sum += item - break - } - } - } - csum <- sum - }(i) - } - startwg.Done() - // Wait for all the sums from producers. - sum := 0 - for i := 0; i < numThreads; i++ { - s := <-csum - sum += s - } - // Assert the total sum. - expectedSum := numOps * (numOps - 1) / 2 - if sum != expectedSum { - t.Fatalf("sums don't match for %d num ops, %d num threads: got %d, want %d", - numOps, numThreads, sum, expectedSum) - } -} - -func TestQueueOfNonBlockingCalls(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) - n := 10 - if testing.Short() { - n = 1 - } - hammerQueueOfNonBlockingCalls(t, 1, n, n) - hammerQueueOfNonBlockingCalls(t, 2, 10*n, 2*n) - hammerQueueOfNonBlockingCalls(t, 4, 100*n, 4*n) -} - -func benchmarkQueueOfProdCons(b *testing.B, queueSize, localWork int) { - callsPerSched := queueSize - procs := runtime.GOMAXPROCS(-1) / 2 - if procs == 0 { - procs = 1 - } - N := int32(b.N / callsPerSched) - c := make(chan bool, 2*procs) - q := NewMPMCQueueOf[int](queueSize) - for p := 0; p < procs; p++ { - go func() { - foo := 0 - for atomic.AddInt32(&N, -1) >= 0 { - for g := 0; g < callsPerSched; g++ { - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - q.Enqueue(1) - } - } - q.Enqueue(0) - c <- foo == 42 - }() - go func() { - foo := 0 - for { - v := q.Dequeue() - if v == 0 { - break - } - for i := 0; i < localWork; i++ { - foo *= 2 - foo /= 2 - } - } - c <- foo == 42 - }() - } - for p := 0; p < procs; p++ { - <-c - <-c - } -} - -func BenchmarkQueueOfProdCons(b *testing.B) { - benchmarkQueueOfProdCons(b, 1000, 0) -} - -func BenchmarkOfQueueProdConsWork100(b *testing.B) { - benchmarkQueueOfProdCons(b, 1000, 100) -} diff --git a/xsync/rbmutex.go b/xsync/rbmutex.go deleted file mode 100644 index a20a141..0000000 --- a/xsync/rbmutex.go +++ /dev/null @@ -1,145 +0,0 @@ -package xsync - -import ( - "runtime" - "sync" - "sync/atomic" - "time" -) - -// slow-down guard -const nslowdown = 7 - -// pool for reader tokens -var rtokenPool sync.Pool - -// RToken is a reader lock token. -type RToken struct { - slot uint32 - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - 4]byte -} - -// A RBMutex is a reader biased reader/writer mutual exclusion lock. -// The lock can be held by an many readers or a single writer. -// The zero value for a RBMutex is an unlocked mutex. -// -// A RBMutex must not be copied after first use. -// -// RBMutex is based on a modified version of BRAVO -// (Biased Locking for Reader-Writer Locks) algorithm: -// https://arxiv.org/pdf/1810.01553.pdf -// -// RBMutex is a specialized mutex for scenarios, such as caches, -// where the vast majority of locks are acquired by readers and write -// lock acquire attempts are infrequent. In such scenarios, RBMutex -// performs better than sync.RWMutex on large multicore machines. -// -// RBMutex extends sync.RWMutex internally and uses it as the "reader -// bias disabled" fallback, so the same semantics apply. The only -// noticeable difference is in reader tokens returned from the -// RLock/RUnlock methods. -type RBMutex struct { - rslots []rslot - rmask uint32 - rbias int32 - inhibitUntil time.Time - rw sync.RWMutex -} - -type rslot struct { - mu int32 - //lint:ignore U1000 prevents false sharing - pad [cacheLineSize - 4]byte -} - -// NewRBMutex creates a new RBMutex instance. -func NewRBMutex() *RBMutex { - nslots := nextPowOf2(parallelism()) - mu := RBMutex{ - rslots: make([]rslot, nslots), - rmask: nslots - 1, - rbias: 1, - } - return &mu -} - -// RLock locks m for reading and returns a reader token. The -// token must be used in the later RUnlock call. -// -// Should not be used for recursive read locking; a blocked Lock -// call excludes new readers from acquiring the lock. -func (mu *RBMutex) RLock() *RToken { - if atomic.LoadInt32(&mu.rbias) == 1 { - t, ok := rtokenPool.Get().(*RToken) - if !ok { - t = new(RToken) - t.slot = runtime_fastrand() - } - // Try all available slots to distribute reader threads to slots. - for i := 0; i < len(mu.rslots); i++ { - slot := t.slot + uint32(i) - rslot := &mu.rslots[slot&mu.rmask] - rslotmu := atomic.LoadInt32(&rslot.mu) - if atomic.CompareAndSwapInt32(&rslot.mu, rslotmu, rslotmu+1) { - if atomic.LoadInt32(&mu.rbias) == 1 { - // Hot path succeeded. - t.slot = slot - return t - } - // The mutex is no longer reader biased. Go to the slow path. - atomic.AddInt32(&rslot.mu, -1) - rtokenPool.Put(t) - break - } - // Contention detected. Give a try with the next slot. - } - } - // Slow path. - mu.rw.RLock() - if atomic.LoadInt32(&mu.rbias) == 0 && time.Now().After(mu.inhibitUntil) { - atomic.StoreInt32(&mu.rbias, 1) - } - return nil -} - -// RUnlock undoes a single RLock call. A reader token obtained from -// the RLock call must be provided. RUnlock does not affect other -// simultaneous readers. A panic is raised if m is not locked for -// reading on entry to RUnlock. -func (mu *RBMutex) RUnlock(t *RToken) { - if t == nil { - mu.rw.RUnlock() - return - } - if atomic.AddInt32(&mu.rslots[t.slot&mu.rmask].mu, -1) < 0 { - panic("invalid reader state detected") - } - rtokenPool.Put(t) -} - -// Lock locks m for writing. If the lock is already locked for -// reading or writing, Lock blocks until the lock is available. -func (mu *RBMutex) Lock() { - mu.rw.Lock() - if atomic.LoadInt32(&mu.rbias) == 1 { - atomic.StoreInt32(&mu.rbias, 0) - start := time.Now() - for i := 0; i < len(mu.rslots); i++ { - for atomic.LoadInt32(&mu.rslots[i].mu) > 0 { - runtime.Gosched() - } - } - mu.inhibitUntil = time.Now().Add(time.Since(start) * nslowdown) - } -} - -// Unlock unlocks m for writing. A panic is raised if m is not locked -// for writing on entry to Unlock. -// -// As with RWMutex, a locked RBMutex is not associated with a -// particular goroutine. One goroutine may RLock (Lock) a RBMutex and -// then arrange for another goroutine to RUnlock (Unlock) it. -func (mu *RBMutex) Unlock() { - mu.rw.Unlock() -} diff --git a/xsync/rbmutex_test.go b/xsync/rbmutex_test.go deleted file mode 100644 index 24009d8..0000000 --- a/xsync/rbmutex_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright notice. Initial version of the following tests was based on -// the following file from the Go Programming Language core repo: -// https://github.com/golang/go/blob/831f9376d8d730b16fb33dfd775618dffe13ce7a/src/sync/rwmutex_test.go - -package xsync_test - -import ( - "fmt" - "runtime" - "sync" - "sync/atomic" - "testing" - - . "github.com/fufuok/utils/xsync" -) - -func TestRBMutexSerialReader(t *testing.T) { - const numIters = 10 - mu := NewRBMutex() - var rtokens [numIters]*RToken - for i := 0; i < numIters; i++ { - rtokens[i] = mu.RLock() - - } - for i := 0; i < numIters; i++ { - mu.RUnlock(rtokens[i]) - } -} - -func parallelReader(mu *RBMutex, clocked, cunlock, cdone chan bool) { - tk := mu.RLock() - clocked <- true - <-cunlock - mu.RUnlock(tk) - cdone <- true -} - -func doTestParallelReaders(numReaders, gomaxprocs int) { - runtime.GOMAXPROCS(gomaxprocs) - mu := NewRBMutex() - clocked := make(chan bool) - cunlock := make(chan bool) - cdone := make(chan bool) - for i := 0; i < numReaders; i++ { - go parallelReader(mu, clocked, cunlock, cdone) - } - // Wait for all parallel RLock()s to succeed. - for i := 0; i < numReaders; i++ { - <-clocked - } - for i := 0; i < numReaders; i++ { - cunlock <- true - } - // Wait for the goroutines to finish. - for i := 0; i < numReaders; i++ { - <-cdone - } -} - -func TestRBMutexParallelReaders(t *testing.T) { - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(0)) - doTestParallelReaders(1, 4) - doTestParallelReaders(3, 4) - doTestParallelReaders(4, 2) -} - -func reader(mu *RBMutex, numIterations int, activity *int32, cdone chan bool) { - for i := 0; i < numIterations; i++ { - tk := mu.RLock() - n := atomic.AddInt32(activity, 1) - if n < 1 || n >= 10000 { - mu.RUnlock(tk) - panic(fmt.Sprintf("rlock(%d)\n", n)) - } - for i := 0; i < 100; i++ { - } - atomic.AddInt32(activity, -1) - mu.RUnlock(tk) - } - cdone <- true -} - -func writer(mu *RBMutex, numIterations int, activity *int32, cdone chan bool) { - for i := 0; i < numIterations; i++ { - mu.Lock() - n := atomic.AddInt32(activity, 10000) - if n != 10000 { - mu.Unlock() - panic(fmt.Sprintf("wlock(%d)\n", n)) - } - for i := 0; i < 100; i++ { - } - atomic.AddInt32(activity, -10000) - mu.Unlock() - } - cdone <- true -} - -func hammerRBMutex(gomaxprocs, numReaders, numIterations int) { - runtime.GOMAXPROCS(gomaxprocs) - // Number of active readers + 10000 * number of active writers. - var activity int32 - mu := NewRBMutex() - cdone := make(chan bool) - go writer(mu, numIterations, &activity, cdone) - var i int - for i = 0; i < numReaders/2; i++ { - go reader(mu, numIterations, &activity, cdone) - } - go writer(mu, numIterations, &activity, cdone) - for ; i < numReaders; i++ { - go reader(mu, numIterations, &activity, cdone) - } - // Wait for the 2 writers and all readers to finish. - for i := 0; i < 2+numReaders; i++ { - <-cdone - } -} - -func TestRBMutex(t *testing.T) { - const n = 1000 - defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(0)) - hammerRBMutex(1, 1, n) - hammerRBMutex(1, 3, n) - hammerRBMutex(1, 10, n) - hammerRBMutex(4, 1, n) - hammerRBMutex(4, 3, n) - hammerRBMutex(4, 10, n) - hammerRBMutex(10, 1, n) - hammerRBMutex(10, 3, n) - hammerRBMutex(10, 10, n) - hammerRBMutex(10, 5, n) -} - -func benchmarkRBMutex(b *testing.B, parallelism, localWork, writeRatio int) { - mu := NewRBMutex() - b.SetParallelism(parallelism) - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - foo++ - if writeRatio > 0 && foo%writeRatio == 0 { - mu.Lock() - for i := 0; i != localWork; i += 1 { - foo *= 2 - foo /= 2 - } - mu.Unlock() - } else { - tk := mu.RLock() - for i := 0; i != localWork; i += 1 { - foo *= 2 - foo /= 2 - } - mu.RUnlock(tk) - } - } - _ = foo - }) -} - -func BenchmarkRBMutexWorkReadOnly_HighParallelism(b *testing.B) { - benchmarkRBMutex(b, 1024, 100, -1) -} - -func BenchmarkRBMutexWorkReadOnly(b *testing.B) { - benchmarkRBMutex(b, -1, 100, -1) -} - -func BenchmarkRBMutexWorkWrite100000(b *testing.B) { - benchmarkRBMutex(b, -1, 100, 100000) -} - -func BenchmarkRBMutexWorkWrite1000(b *testing.B) { - benchmarkRBMutex(b, -1, 100, 1000) -} - -func benchmarkRWMutex(b *testing.B, parallelism, localWork, writeRatio int) { - var mu sync.RWMutex - b.SetParallelism(parallelism) - runParallel(b, func(pb *testing.PB) { - foo := 0 - for pb.Next() { - foo++ - if writeRatio > 0 && foo%writeRatio == 0 { - mu.Lock() - for i := 0; i != localWork; i += 1 { - foo *= 2 - foo /= 2 - } - mu.Unlock() - } else { - mu.RLock() - for i := 0; i != localWork; i += 1 { - foo *= 2 - foo /= 2 - } - mu.RUnlock() - } - } - _ = foo - }) -} - -func BenchmarkRWMutexWorkReadOnly_HighParallelism(b *testing.B) { - benchmarkRWMutex(b, 1024, 100, -1) -} - -func BenchmarkRWMutexWorkReadOnly(b *testing.B) { - benchmarkRWMutex(b, -1, 100, -1) -} - -func BenchmarkRWMutexWorkWrite100000(b *testing.B) { - benchmarkRWMutex(b, -1, 100, 100000) -} - -func BenchmarkRWMutexWorkWrite1000(b *testing.B) { - benchmarkRWMutex(b, -1, 100, 1000) -} diff --git a/xsync/util.go b/xsync/util.go deleted file mode 100644 index 7368912..0000000 --- a/xsync/util.go +++ /dev/null @@ -1,46 +0,0 @@ -package xsync - -import ( - "runtime" - _ "unsafe" -) - -// test-only assert()-like flag -var assertionsEnabled = false - -const ( - // cacheLineSize is used in paddings to prevent false sharing; - // 64B are used instead of 128B as a compromise between - // memory footprint and performance; 128B usage may give ~30% - // improvement on NUMA machines. - cacheLineSize = 64 -) - -// nextPowOf2 computes the next highest power of 2 of 32-bit v. -// Source: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 -func nextPowOf2(v uint32) uint32 { - if v == 0 { - return 1 - } - v-- - v |= v >> 1 - v |= v >> 2 - v |= v >> 4 - v |= v >> 8 - v |= v >> 16 - v++ - return v -} - -func parallelism() uint32 { - maxProcs := uint32(runtime.GOMAXPROCS(0)) - numCores := uint32(runtime.NumCPU()) - if maxProcs < numCores { - return maxProcs - } - return numCores -} - -//go:noescape -//go:linkname runtime_fastrand runtime.fastrand -func runtime_fastrand() uint32 diff --git a/xsync/util_hash.go b/xsync/util_hash.go deleted file mode 100644 index bdab51e..0000000 --- a/xsync/util_hash.go +++ /dev/null @@ -1,56 +0,0 @@ -package xsync - -import ( - "reflect" - "unsafe" -) - -// makeSeed creates a random seed. -func makeSeed() uint64 { - var s1 uint32 - for { - s1 = runtime_fastrand() - // We use seed 0 to indicate an uninitialized seed/hash, - // so keep trying until we get a non-zero seed. - if s1 != 0 { - break - } - } - s2 := runtime_fastrand() - return uint64(s1)<<32 | uint64(s2) -} - -// hashString calculates a hash of s with the given seed. -func hashString(s string, seed uint64) uint64 { - if s == "" { - return seed - } - strh := (*reflect.StringHeader)(unsafe.Pointer(&s)) - return uint64(runtime_memhash(unsafe.Pointer(strh.Data), uintptr(seed), uintptr(strh.Len))) -} - -//go:noescape -//go:linkname runtime_memhash runtime.memhash -func runtime_memhash(p unsafe.Pointer, h, s uintptr) uintptr - -// how interface is represented in memory -type iface struct { - typ uintptr - word unsafe.Pointer -} - -// same as runtime_typehash, but always returns a uint64 -// see: maphash.rthash function for details -func runtime_typehash64(t uintptr, p unsafe.Pointer, seed uint64) uint64 { - if unsafe.Sizeof(uintptr(0)) == 8 { - return uint64(runtime_typehash(t, p, uintptr(seed))) - } - - lo := runtime_typehash(t, p, uintptr(seed)) - hi := runtime_typehash(t, p, uintptr(seed>>32)) - return uint64(hi)<<32 | uint64(lo) -} - -//go:noescape -//go:linkname runtime_typehash runtime.typehash -func runtime_typehash(t uintptr, p unsafe.Pointer, h uintptr) uintptr diff --git a/xsync/util_hash_mapof.go b/xsync/util_hash_mapof.go deleted file mode 100644 index 1e8bc85..0000000 --- a/xsync/util_hash_mapof.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync - -import ( - "reflect" - "unsafe" -) - -// makeHasher creates a fast hash function for the given comparable type. -// The only limitation is that the type should not contain interfaces inside -// based on runtime.typehash. -func makeHasher[T comparable]() func(T, uint64) uint64 { - var zero T - - if reflect.TypeOf(&zero).Elem().Kind() == reflect.Interface { - return func(value T, seed uint64) uint64 { - iValue := any(value) - i := (*iface)(unsafe.Pointer(&iValue)) - return runtime_typehash64(i.typ, i.word, seed) - } - } else { - var iZero any = zero - i := (*iface)(unsafe.Pointer(&iZero)) - return func(value T, seed uint64) uint64 { - return runtime_typehash64(i.typ, unsafe.Pointer(&value), seed) - } - } -} diff --git a/xsync/util_hash_mapof_test.go b/xsync/util_hash_mapof_test.go deleted file mode 100644 index d00add8..0000000 --- a/xsync/util_hash_mapof_test.go +++ /dev/null @@ -1,186 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package xsync_test - -//lint:file-ignore U1000 unused fields are necessary to access the hasher -//lint:file-ignore SA4000 hash code comparisons use identical expressions - -import ( - "fmt" - "testing" - "unsafe" - - . "github.com/fufuok/utils/xsync" -) - -func TestMakeHashFunc(t *testing.T) { - type User struct { - Name string - City string - } - - seed := MakeSeed() - - hashString := MakeHasher[string]() - hashUser := MakeHasher[User]() - - hashUserMap := makeMapHasher[User]() - - // Not that much to test TBH. - - // check that hash is not always the same - for i := 0; ; i++ { - if hashString("foo", seed) != hashString("bar", seed) { - break - } - if i >= 100 { - t.Error("hashString is always the same") - break - } - - seed = MakeSeed() // try with a new seed - } - - if hashString("foo", seed) != hashString("foo", seed) { - t.Error("hashString is not deterministic") - } - - if hashUser(User{Name: "Ivan", City: "Sofia"}, seed) != hashUser(User{Name: "Ivan", City: "Sofia"}, seed) { - t.Error("hashUser is not deterministic") - } - - // just for fun, compare with native hash function - if hashUser(User{Name: "Ivan", City: "Sofia"}, seed) != hashUserMap(User{Name: "Ivan", City: "Sofia"}, seed) { - t.Error("hashUser and hashUserNative return different values") - } -} - -func makeMapHasher[T comparable]() func(T, uint64) uint64 { - hasher := makeMapHasherInternal(make(map[T]struct{})) - - is64Bit := unsafe.Sizeof(uintptr(0)) == 8 - - if is64Bit { - return func(value T, seed uint64) uint64 { - seed64 := *(*uint64)(unsafe.Pointer(&seed)) - return uint64(hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64))) - } - } else { - return func(value T, seed uint64) uint64 { - seed64 := *(*uint64)(unsafe.Pointer(&seed)) - lo := hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64)) - hi := hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64>>32)) - return uint64(hi)<<32 | uint64(lo) - } - } -} - -//go:noescape -//go:linkname runtime_noescape runtime.noescape -func runtime_noescape(p unsafe.Pointer) unsafe.Pointer - -type nativeHasher func(unsafe.Pointer, uintptr) uintptr - -func makeMapHasherInternal(mapValue any) nativeHasher { - // go/src/runtime/type.go - type tflag uint8 - type nameOff int32 - type typeOff int32 - - // go/src/runtime/type.go - type _type struct { - size uintptr - ptrdata uintptr - hash uint32 - tflag tflag - align uint8 - fieldAlign uint8 - kind uint8 - equal func(unsafe.Pointer, unsafe.Pointer) bool - gcdata *byte - str nameOff - ptrToThis typeOff - } - - // go/src/runtime/type.go - type maptype struct { - typ _type - key *_type - elem *_type - bucket *_type - // function for hashing keys (ptr to key, seed) -> hash - hasher nativeHasher - keysize uint8 - elemsize uint8 - bucketsize uint16 - flags uint32 - } - - type mapiface struct { - typ *maptype - val uintptr - } - - i := (*mapiface)(unsafe.Pointer(&mapValue)) - return i.typ.hasher -} - -func BenchmarkMakeHashFunc(b *testing.B) { - type Point struct { - X, Y, Z int - } - - type User struct { - ID int - FirstName string - LastName string - IsActive bool - City string - } - - type PadInside struct { - A int - B byte - C int - } - - type PadTrailing struct { - A int - B byte - } - - doBenchmarkMakeHashFunc(b, int64(116)) - doBenchmarkMakeHashFunc(b, int32(116)) - doBenchmarkMakeHashFunc(b, 3.14) - doBenchmarkMakeHashFunc(b, "test key test key test key test key test key test key test key test key test key ") - doBenchmarkMakeHashFunc(b, Point{1, 2, 3}) - doBenchmarkMakeHashFunc(b, User{ID: 1, FirstName: "Ivan", LastName: "Ivanov", IsActive: true, City: "Sofia"}) - doBenchmarkMakeHashFunc(b, PadInside{}) - doBenchmarkMakeHashFunc(b, PadTrailing{}) - doBenchmarkMakeHashFunc(b, [1024]byte{}) - doBenchmarkMakeHashFunc(b, [128]Point{}) - doBenchmarkMakeHashFunc(b, [128]User{}) - doBenchmarkMakeHashFunc(b, [128]PadInside{}) - doBenchmarkMakeHashFunc(b, [128]PadTrailing{}) -} - -func doBenchmarkMakeHashFunc[T comparable](b *testing.B, val T) { - hash := MakeHasher[T]() - hashNativeMap := makeMapHasher[T]() - seed := MakeSeed() - - b.Run(fmt.Sprintf("%T normal", val), func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = hash(val, seed) - } - }) - - b.Run(fmt.Sprintf("%T map native", val), func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = hashNativeMap(val, seed) - } - }) -} diff --git a/xsync/util_hash_test.go b/xsync/util_hash_test.go deleted file mode 100644 index bab399d..0000000 --- a/xsync/util_hash_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package xsync_test - -//lint:file-ignore U1000 unused fields are necessary to access the hasher -//lint:file-ignore SA4000 hash code comparisons use identical expressions - -import ( - "hash/maphash" - "testing" - - . "github.com/fufuok/utils/xsync" -) - -func BenchmarkMapHashString(b *testing.B) { - fn := func(seed maphash.Seed, s string) uint64 { - var h maphash.Hash - h.SetSeed(seed) - h.WriteString(s) - return h.Sum64() - } - seed := maphash.MakeSeed() - for i := 0; i < b.N; i++ { - _ = fn(seed, benchmarkKeyPrefix) - } - // about 13ns/op on x86-64 -} - -func BenchmarkHashString(b *testing.B) { - seed := MakeSeed() - for i := 0; i < b.N; i++ { - _ = HashString(benchmarkKeyPrefix, seed) - } - // about 4ns/op on x86-64 -} diff --git a/xsync/util_test.go b/xsync/util_test.go deleted file mode 100644 index 16a5143..0000000 --- a/xsync/util_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package xsync_test - -import ( - "math/rand" - "testing" - - . "github.com/fufuok/utils/xsync" -) - -func TestNextPowOf2(t *testing.T) { - if NextPowOf2(0) != 1 { - t.Error("nextPowOf2 failed") - } - if NextPowOf2(1) != 1 { - t.Error("nextPowOf2 failed") - } - if NextPowOf2(2) != 2 { - t.Error("nextPowOf2 failed") - } - if NextPowOf2(3) != 4 { - t.Error("nextPowOf2 failed") - } -} - -// This test is here to catch potential problems -// with fastrand-related changes. -func TestFastrand(t *testing.T) { - count := 100 - set := make(map[uint32]struct{}, count) - - for i := 0; i < count; i++ { - num := Fastrand() - set[num] = struct{}{} - } - - if len(set) != count { - t.Error("duplicated rand num") - } -} - -func BenchmarkFastrand(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = Fastrand() - } - // about 1.4 ns/op on x86-64 -} - -func BenchmarkRand(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = rand.Uint32() - } - // about 12 ns/op on x86-64 -}