├── .gitignore ├── go.mod ├── go.sum ├── benchmarks ├── go.mod ├── go.sum ├── map_test.go └── results.txt ├── examples ├── prealloc.go ├── custom_hash.go ├── simple.go └── concurrent_deletion.go ├── .github └── workflows │ └── go.yml ├── iterator.go ├── iterator_test.go ├── LICENSE.md ├── atomic.go ├── list.go ├── README.md ├── hash.go ├── e2e_test.go └── map.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.iml 3 | .idea 4 | *.exe 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alphadose/haxmap 2 | 3 | go 1.18 4 | 5 | require golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= 2 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 3 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alphadose/haxmap/benchmarks 2 | 3 | go 1.19 4 | 5 | replace github.com/alphadose/haxmap => ../ 6 | 7 | require ( 8 | github.com/alphadose/haxmap v0.0.0-00010101000000-000000000000 9 | github.com/cornelk/hashmap v1.0.8 10 | github.com/puzpuzpuz/xsync/v2 v2.3.1 11 | ) 12 | 13 | require golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect 14 | -------------------------------------------------------------------------------- /examples/prealloc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alphadose/haxmap" 5 | ) 6 | 7 | func main() { 8 | const initialSize = 1 << 10 9 | 10 | // pre-allocating the size of the map will prevent all grow operations 11 | // until that limit is hit thereby improving performance 12 | m := haxmap.New[int, string](initialSize) 13 | 14 | m.Set(1, "1") 15 | val, ok := m.Get(1) 16 | if ok { 17 | println(val) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | e2e: 11 | strategy: 12 | matrix: 13 | go-version: ["=1.18", "^1.23"] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: Test 24 | run: | 25 | go test -v . 26 | -------------------------------------------------------------------------------- /examples/custom_hash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alphadose/haxmap" 5 | ) 6 | 7 | // your custom hash function 8 | func customStringHasher(s string) uintptr { 9 | return uintptr(len(s)) 10 | } 11 | 12 | func main() { 13 | // initialize a string-string map with your custom hash function 14 | // this overrides the default xxHash algorithm 15 | m := haxmap.New[string, string]() 16 | m.SetHasher(customStringHasher) 17 | 18 | m.Set("one", "1") 19 | val, ok := m.Get("one") 20 | if ok { 21 | println(val) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc= 2 | github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k= 3 | github.com/puzpuzpuz/xsync/v2 v2.3.1 h1:oAm/nI4ZC+FqOM7t2fnA7DaQVsuj4fO2KcTcNTS1Q9Y= 4 | github.com/puzpuzpuz/xsync/v2 v2.3.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= 5 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= 6 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 7 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | //go:build go1.23 2 | // +build go1.23 3 | 4 | package haxmap 5 | 6 | import "iter" 7 | 8 | func (m *Map[K, V]) Iterator() iter.Seq2[K, V] { 9 | return func(yield func(key K, value V) bool) { 10 | for item := m.listHead.next(); item != nil; item = item.next() { 11 | if !yield(item.key, *item.value.Load()) { 12 | return 13 | } 14 | } 15 | } 16 | } 17 | 18 | func (m *Map[K, _]) Keys() iter.Seq[K] { 19 | return func(yield func(key K) bool) { 20 | for item := m.listHead.next(); item != nil; item = item.next() { 21 | if !yield(item.key) { 22 | return 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iterator_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.23 2 | // +build go1.23 3 | 4 | package haxmap 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestIterators(t *testing.T) { 11 | type Value = struct { 12 | key int 13 | } 14 | 15 | m := New[int, *Value]() 16 | 17 | itemCount := 16 18 | for i := itemCount; i > 0; i-- { 19 | m.Set(i, &Value{i}) 20 | } 21 | 22 | t.Run("iterator", func(t *testing.T) { 23 | counter := 0 24 | for k, v := range m.Iterator() { 25 | if v == nil { 26 | t.Error("Expecting an object.") 27 | } else if k != v.key { 28 | t.Error("Incorrect key/value pairs") 29 | } 30 | 31 | counter++ 32 | } 33 | 34 | if counter != itemCount { 35 | t.Error("Iterated item count did not match.") 36 | } 37 | }) 38 | 39 | t.Run("keys", func(t *testing.T) { 40 | counter := 0 41 | for k := range m.Keys() { 42 | _, ok := m.Get(k) 43 | if !ok { 44 | t.Error("The key is not is the map") 45 | } 46 | counter++ 47 | } 48 | 49 | if counter != itemCount { 50 | t.Error("Iterated item count did not match.") 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/alphadose/haxmap" 7 | ) 8 | 9 | func main() { 10 | // initialize map with key type `int` and value type `string` 11 | mep := haxmap.New[int, string]() 12 | 13 | // set a value (overwrites existing value if present) 14 | mep.Set(1, "one") 15 | 16 | // get the value and print it 17 | val, ok := mep.Get(1) 18 | if ok { 19 | println(val) 20 | } 21 | 22 | mep.Set(2, "two") 23 | mep.Set(3, "three") 24 | mep.Set(4, "four") 25 | 26 | // ForEach loop to iterate over all key-value pairs and execute the given lambda 27 | mep.ForEach(func(key int, value string) bool { 28 | fmt.Printf("Key -> %d | Value -> %s\n", key, value) 29 | return true // return `true` to continue iteration and `false` to break iteration 30 | }) 31 | 32 | mep.Del(1) // delete a value 33 | mep.Del(0) // delete is safe even if a key doesn't exists 34 | 35 | // bulk deletion is supported too in the same API call 36 | // has better performance than deleting keys one by one 37 | mep.Del(2, 3, 4) 38 | 39 | if mep.Len() == 0 { 40 | println("cleanup complete") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anish Mukherjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/concurrent_deletion.go: -------------------------------------------------------------------------------- 1 | // from bug https://github.com/alphadose/haxmap/issues/14 2 | // fixed in v1.0.1 and above 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "math/rand" 10 | "time" 11 | 12 | "github.com/alphadose/haxmap" 13 | ) 14 | 15 | type data struct { 16 | id int 17 | exp time.Time 18 | } 19 | 20 | func main() { 21 | c := haxmap.New[int, *data](256) 22 | ctx, cancel := context.WithCancel(context.Background()) 23 | defer cancel() 24 | 25 | go func() { 26 | t := time.NewTicker(time.Second * 2) 27 | defer t.Stop() 28 | var count int 29 | for { 30 | select { 31 | case <-t.C: 32 | count = 0 33 | c.ForEach(func(s int, b *data) bool { 34 | if time.Now().After(b.exp) { 35 | c.Del(s) 36 | count++ 37 | } 38 | return true 39 | }) 40 | fmt.Println("Del", count) 41 | case <-ctx.Done(): 42 | return 43 | } 44 | } 45 | }() 46 | 47 | for i := 0; i < 20000; i++ { 48 | c.Set(i, &data{id: i, exp: time.Now().Add(time.Millisecond * time.Duration((1000 + rand.Intn(800))))}) 49 | time.Sleep(time.Microsecond * time.Duration(rand.Intn(200)+10)) 50 | if i%100 == 0 { 51 | fmt.Println(i) 52 | } 53 | } 54 | 55 | time.Sleep(time.Second * 3) 56 | fmt.Println("LEN", c.Len()) 57 | } 58 | -------------------------------------------------------------------------------- /atomic.go: -------------------------------------------------------------------------------- 1 | package haxmap 2 | 3 | import ( 4 | "sync/atomic" 5 | "unsafe" 6 | ) 7 | 8 | // noCopy implements sync.Locker so that go vet can trigger 9 | // warnings when types embedding noCopy are copied. 10 | type noCopy struct{} 11 | 12 | func (c *noCopy) Lock() {} 13 | func (c *noCopy) Unlock() {} 14 | 15 | type atomicUint32 struct { 16 | _ noCopy 17 | v uint32 18 | } 19 | 20 | type atomicPointer[T any] struct { 21 | _ noCopy 22 | ptr unsafe.Pointer 23 | } 24 | 25 | type atomicUintptr struct { 26 | _ noCopy 27 | ptr uintptr 28 | } 29 | 30 | func (u *atomicUint32) Load() uint32 { return atomic.LoadUint32(&u.v) } 31 | func (u *atomicUint32) Store(v uint32) { atomic.StoreUint32(&u.v, v) } 32 | func (u *atomicUint32) Add(delta uint32) uint32 { return atomic.AddUint32(&u.v, delta) } 33 | func (u *atomicUint32) Swap(v uint32) uint32 { return atomic.SwapUint32(&u.v, v) } 34 | func (u *atomicUint32) CompareAndSwap(old, new uint32) bool { 35 | return atomic.CompareAndSwapUint32(&u.v, old, new) 36 | } 37 | 38 | func (p *atomicPointer[T]) Load() *T { return (*T)(atomic.LoadPointer(&p.ptr)) } 39 | func (p *atomicPointer[T]) Store(v *T) { atomic.StorePointer(&p.ptr, unsafe.Pointer(v)) } 40 | func (p *atomicPointer[T]) Swap(v *T) *T { return (*T)(atomic.SwapPointer(&p.ptr, unsafe.Pointer(v))) } 41 | func (p *atomicPointer[T]) CompareAndSwap(old, new *T) bool { 42 | return atomic.CompareAndSwapPointer(&p.ptr, unsafe.Pointer(old), unsafe.Pointer(new)) 43 | } 44 | 45 | func (u *atomicUintptr) Load() uintptr { return atomic.LoadUintptr(&u.ptr) } 46 | func (u *atomicUintptr) Store(v uintptr) { atomic.StoreUintptr(&u.ptr, v) } 47 | func (u *atomicUintptr) Add(delta uintptr) uintptr { return atomic.AddUintptr(&u.ptr, delta) } 48 | func (u *atomicUintptr) Swap(v uintptr) uintptr { return atomic.SwapUintptr(&u.ptr, v) } 49 | func (u *atomicUintptr) CompareAndSwap(old, new uintptr) bool { 50 | return atomic.CompareAndSwapUintptr(&u.ptr, old, new) 51 | } 52 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package haxmap 2 | 3 | import "sync/atomic" 4 | 5 | // states denoting whether a node is deleted or not 6 | const ( 7 | notDeleted uint32 = iota 8 | deleted 9 | ) 10 | 11 | // Below implementation is a lock-free linked list based on https://www.cl.cam.ac.uk/research/srg/netos/papers/2001-caslists.pdf by Timothy L. Harris 12 | // Performance improvements suggested in https://arxiv.org/pdf/2010.15755.pdf were also added 13 | 14 | // newListHead returns the new head of any list 15 | func newListHead[K hashable, V any]() *element[K, V] { 16 | e := &element[K, V]{keyHash: 0, key: *new(K)} 17 | e.nextPtr.Store(nil) 18 | e.value.Store(new(V)) 19 | return e 20 | } 21 | 22 | // a single node in the list 23 | type element[K hashable, V any] struct { 24 | keyHash uintptr 25 | key K 26 | // The next element in the list. If this pointer has the marked flag set it means THIS element, not the next one, is deleted. 27 | nextPtr atomicPointer[element[K, V]] 28 | value atomicPointer[V] 29 | deleted uint32 30 | } 31 | 32 | // next returns the next element 33 | // this also deletes all marked elements while traversing the list 34 | func (self *element[K, V]) next() *element[K, V] { 35 | for nextElement := self.nextPtr.Load(); nextElement != nil; { 36 | // if our next element is itself deleted (by the same criteria) then we will just replace 37 | // it with its next() (which should be the first node behind it that isn't itself deleted) and then check again 38 | if nextElement.isDeleted() { 39 | self.nextPtr.CompareAndSwap(nextElement, nextElement.next()) // actual deletion happens here after nodes are marked deleted lazily 40 | nextElement = self.nextPtr.Load() 41 | } else { 42 | return nextElement 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | // addBefore inserts an element before the specified element 49 | func (self *element[K, V]) addBefore(allocatedElement, before *element[K, V]) bool { 50 | if self.next() != before { 51 | return false 52 | } 53 | allocatedElement.nextPtr.Store(before) 54 | return self.nextPtr.CompareAndSwap(before, allocatedElement) 55 | } 56 | 57 | // inject updates an existing value in the list if present or adds a new entry 58 | func (self *element[K, V]) inject(c uintptr, key K, value *V) (*element[K, V], bool) { 59 | var ( 60 | alloc *element[K, V] 61 | left, curr, right = self.search(c, key) 62 | ) 63 | if curr != nil { 64 | curr.value.Store(value) 65 | return curr, false 66 | } 67 | if left != nil { 68 | alloc = &element[K, V]{keyHash: c, key: key} 69 | alloc.value.Store(value) 70 | if left.addBefore(alloc, right) { 71 | return alloc, true 72 | } 73 | } 74 | return nil, false 75 | } 76 | 77 | // search for an element in the list and return left_element, searched_element and right_element respectively 78 | func (self *element[K, V]) search(c uintptr, key K) (*element[K, V], *element[K, V], *element[K, V]) { 79 | var ( 80 | left, right *element[K, V] 81 | curr = self 82 | ) 83 | for { 84 | if curr == nil { 85 | return left, curr, right 86 | } 87 | right = curr.next() 88 | if c < curr.keyHash { 89 | right = curr 90 | curr = nil 91 | return left, curr, right 92 | } else if c == curr.keyHash && key == curr.key { 93 | return left, curr, right 94 | } 95 | left = curr 96 | curr = left.next() 97 | right = nil 98 | } 99 | } 100 | 101 | // remove marks a node for deletion 102 | // the node will be removed in the next iteration via `element.next()` 103 | // CAS ensures each node can be marked for deletion exactly once 104 | func (self *element[K, V]) remove() bool { 105 | return atomic.CompareAndSwapUint32(&self.deleted, notDeleted, deleted) 106 | } 107 | 108 | // if current element is deleted 109 | func (self *element[K, V]) isDeleted() bool { 110 | return atomic.LoadUint32(&self.deleted) == deleted 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HaxMap 2 | 3 | [![GoDoc](https://godoc.org/github.com/alphadose/haxmap/svc?status.svg)](https://pkg.go.dev/github.com/alphadose/haxmap) 4 | [![Main Actions Status](https://github.com/alphadose/haxmap/workflows/Go/badge.svg)](https://github.com/alphadose/haxmap/actions) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/alphadose/haxmap)](https://goreportcard.com/report/github.com/alphadose/haxmap) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.md) 7 | > A lightning fast concurrent hashmap 8 | 9 | The hashing algorithm used was [xxHash](https://github.com/Cyan4973/xxHash) and the hashmap's buckets were implemented using [Harris lock-free list](https://www.cl.cam.ac.uk/research/srg/netos/papers/2001-caslists.pdf) 10 | 11 | ## Installation 12 | 13 | You need Golang [1.18.x](https://go.dev/dl/) or above 14 | 15 | ```bash 16 | $ go get github.com/alphadose/haxmap 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | 27 | "github.com/alphadose/haxmap" 28 | ) 29 | 30 | func main() { 31 | // initialize map with key type `int` and value type `string` 32 | mep := haxmap.New[int, string]() 33 | 34 | // set a value (overwrites existing value if present) 35 | mep.Set(1, "one") 36 | 37 | // get the value and print it 38 | val, ok := mep.Get(1) 39 | if ok { 40 | println(val) 41 | } 42 | 43 | mep.Set(2, "two") 44 | mep.Set(3, "three") 45 | mep.Set(4, "four") 46 | 47 | // ForEach loop to iterate over all key-value pairs and execute the given lambda 48 | mep.ForEach(func(key int, value string) bool { 49 | fmt.Printf("Key -> %d | Value -> %s\n", key, value) 50 | return true // return `true` to continue iteration and `false` to break iteration 51 | }) 52 | 53 | mep.Del(1) // delete a value 54 | mep.Del(0) // delete is safe even if a key doesn't exists 55 | 56 | // bulk deletion is supported too in the same API call 57 | // has better performance than deleting keys one by one 58 | mep.Del(2, 3, 4) 59 | 60 | if mep.Len() == 0 { 61 | println("cleanup complete") 62 | } 63 | } 64 | ``` 65 | 66 | ## Benchmarks 67 | 68 | Benchmarks were performed against [golang sync.Map](https://pkg.go.dev/sync#Map) and the latest [cornelk-hashmap](https://github.com/cornelk/hashmap) 69 | 70 | All results were computed from [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) of 20 runs (code available [here](./benchmarks)) 71 | 72 | 1. Concurrent Reads Only 73 | ``` 74 | name time/op 75 | HaxMapReadsOnly-8 6.94µs ± 4% 76 | GoSyncMapReadsOnly-8 21.5µs ± 3% 77 | CornelkMapReadsOnly-8 8.39µs ± 8% 78 | ``` 79 | 80 | 2. Concurrent Reads with Writes 81 | ``` 82 | name time/op 83 | HaxMapReadsWithWrites-8 8.23µs ± 3% 84 | GoSyncMapReadsWithWrites-8 25.0µs ± 2% 85 | CornelkMapReadsWithWrites-8 8.83µs ±20% 86 | 87 | name alloc/op 88 | HaxMapReadsWithWrites-8 1.25kB ± 5% 89 | GoSyncMapReadsWithWrites-8 6.20kB ± 7% 90 | CornelkMapReadsWithWrites-8 1.53kB ± 9% 91 | 92 | name allocs/op 93 | HaxMapReadsWithWrites-8 156 ± 5% 94 | GoSyncMapReadsWithWrites-8 574 ± 7% 95 | CornelkMapReadsWithWrites-8 191 ± 9% 96 | ``` 97 | 98 | From the above results it is evident that `haxmap` takes the least time, memory and allocations in all cases making it the best golang concurrent hashmap in this period of time 99 | 100 | ## Tips 101 | 102 | 1. HaxMap by default uses [xxHash](https://github.com/cespare/xxhash) algorithm, but you can override this and plug-in your own custom hash function. Beneath lies an example for the same. 103 | ```go 104 | package main 105 | 106 | import ( 107 | "github.com/alphadose/haxmap" 108 | ) 109 | 110 | // your custom hash function 111 | // the hash function signature must adhere to `func(keyType) uintptr` 112 | func customStringHasher(s string) uintptr { 113 | return uintptr(len(s)) 114 | } 115 | 116 | func main() { 117 | m := haxmap.New[string, string]() // initialize a string-string map 118 | m.SetHasher(customStringHasher) // this overrides the default xxHash algorithm 119 | 120 | m.Set("one", "1") 121 | val, ok := m.Get("one") 122 | if ok { 123 | println(val) 124 | } 125 | } 126 | ``` 127 | 128 | 2. You can pre-allocate the size of the map which will improve performance in some cases. 129 | ```go 130 | package main 131 | 132 | import ( 133 | "github.com/alphadose/haxmap" 134 | ) 135 | 136 | func main() { 137 | const initialSize = 1 << 10 138 | 139 | // pre-allocating the size of the map will prevent all grow operations 140 | // until that limit is hit thereby improving performance 141 | m := haxmap.New[int, string](initialSize) 142 | 143 | m.Set(1, "1") 144 | val, ok := m.Get(1) 145 | if ok { 146 | println(val) 147 | } 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /benchmarks/map_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/alphadose/haxmap" 9 | "github.com/cornelk/hashmap" 10 | "github.com/puzpuzpuz/xsync/v2" 11 | ) 12 | 13 | const ( 14 | epochs uintptr = 1 << 12 15 | mapSize = 8 16 | ) 17 | 18 | func setupHaxMap() *haxmap.Map[uintptr, uintptr] { 19 | m := haxmap.New[uintptr, uintptr](mapSize) 20 | for i := uintptr(0); i < epochs; i++ { 21 | m.Set(i, i) 22 | } 23 | return m 24 | } 25 | 26 | func setupGoSyncMap() *sync.Map { 27 | m := &sync.Map{} 28 | for i := uintptr(0); i < epochs; i++ { 29 | m.Store(i, i) 30 | } 31 | return m 32 | } 33 | 34 | func setupCornelkMap() *hashmap.Map[uintptr, uintptr] { 35 | m := hashmap.NewSized[uintptr, uintptr](mapSize) 36 | for i := uintptr(0); i < epochs; i++ { 37 | m.Set(i, i) 38 | } 39 | return m 40 | } 41 | 42 | func setupXsyncMap() *xsync.MapOf[uintptr, uintptr] { 43 | m := xsync.NewIntegerMapOf[uintptr, uintptr]() 44 | for i := uintptr(0); i < epochs; i++ { 45 | m.Store(i, i) 46 | } 47 | return m 48 | } 49 | 50 | func BenchmarkHaxMapReadsOnly(b *testing.B) { 51 | m := setupHaxMap() 52 | b.ResetTimer() 53 | b.RunParallel(func(pb *testing.PB) { 54 | for pb.Next() { 55 | for i := uintptr(0); i < epochs; i++ { 56 | j, _ := m.Get(i) 57 | if j != i { 58 | b.Fail() 59 | } 60 | } 61 | } 62 | }) 63 | } 64 | 65 | func BenchmarkHaxMapReadsWithWrites(b *testing.B) { 66 | m := setupHaxMap() 67 | var writer uintptr 68 | b.ResetTimer() 69 | b.RunParallel(func(pb *testing.PB) { 70 | // use 1 thread as writer 71 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 72 | for pb.Next() { 73 | for i := uintptr(0); i < epochs; i++ { 74 | m.Set(i, i) 75 | } 76 | } 77 | } else { 78 | for pb.Next() { 79 | for i := uintptr(0); i < epochs; i++ { 80 | j, _ := m.Get(i) 81 | if j != i { 82 | b.Fail() 83 | } 84 | } 85 | } 86 | } 87 | }) 88 | } 89 | 90 | func BenchmarkGoSyncMapReadsOnly(b *testing.B) { 91 | m := setupGoSyncMap() 92 | b.ResetTimer() 93 | b.RunParallel(func(pb *testing.PB) { 94 | for pb.Next() { 95 | for i := uintptr(0); i < epochs; i++ { 96 | j, _ := m.Load(i) 97 | if j != i { 98 | b.Fail() 99 | } 100 | } 101 | } 102 | }) 103 | } 104 | 105 | func BenchmarkGoSyncMapReadsWithWrites(b *testing.B) { 106 | m := setupGoSyncMap() 107 | var writer uintptr 108 | b.ResetTimer() 109 | b.RunParallel(func(pb *testing.PB) { 110 | // use 1 thread as writer 111 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 112 | for pb.Next() { 113 | for i := uintptr(0); i < epochs; i++ { 114 | m.Store(i, i) 115 | } 116 | } 117 | } else { 118 | for pb.Next() { 119 | for i := uintptr(0); i < epochs; i++ { 120 | j, _ := m.Load(i) 121 | if j != i { 122 | b.Fail() 123 | } 124 | } 125 | } 126 | } 127 | }) 128 | } 129 | 130 | func BenchmarkCornelkMapReadsOnly(b *testing.B) { 131 | m := setupCornelkMap() 132 | b.ResetTimer() 133 | b.RunParallel(func(pb *testing.PB) { 134 | for pb.Next() { 135 | for i := uintptr(0); i < epochs; i++ { 136 | j, _ := m.Get(i) 137 | if j != i { 138 | b.Fail() 139 | } 140 | } 141 | } 142 | }) 143 | } 144 | 145 | func BenchmarkCornelkMapReadsWithWrites(b *testing.B) { 146 | m := setupCornelkMap() 147 | var writer uintptr 148 | b.ResetTimer() 149 | b.RunParallel(func(pb *testing.PB) { 150 | // use 1 thread as writer 151 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 152 | for pb.Next() { 153 | for i := uintptr(0); i < epochs; i++ { 154 | m.Set(i, i) 155 | } 156 | } 157 | } else { 158 | for pb.Next() { 159 | for i := uintptr(0); i < epochs; i++ { 160 | j, _ := m.Get(i) 161 | if j != i { 162 | b.Fail() 163 | } 164 | } 165 | } 166 | } 167 | }) 168 | } 169 | 170 | func BenchmarkXsyncMapReadsOnly(b *testing.B) { 171 | m := setupXsyncMap() 172 | b.ResetTimer() 173 | b.RunParallel(func(pb *testing.PB) { 174 | for pb.Next() { 175 | for i := uintptr(0); i < epochs; i++ { 176 | j, _ := m.Load(i) 177 | if j != i { 178 | b.Fail() 179 | } 180 | } 181 | } 182 | }) 183 | } 184 | 185 | func BenchmarkXsyncMapReadsWithWrites(b *testing.B) { 186 | m := setupXsyncMap() 187 | var writer uintptr 188 | b.ResetTimer() 189 | b.RunParallel(func(pb *testing.PB) { 190 | // use 1 thread as writer 191 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 192 | for pb.Next() { 193 | for i := uintptr(0); i < epochs; i++ { 194 | m.Store(i, i) 195 | } 196 | } 197 | } else { 198 | for pb.Next() { 199 | for i := uintptr(0); i < epochs; i++ { 200 | j, _ := m.Load(i) 201 | if j != i { 202 | b.Fail() 203 | } 204 | } 205 | } 206 | } 207 | }) 208 | } 209 | -------------------------------------------------------------------------------- /hash.go: -------------------------------------------------------------------------------- 1 | package haxmap 2 | 3 | /* 4 | From https://github.com/cespare/xxhash 5 | 6 | Copyright (c) 2016 Caleb Spare 7 | 8 | MIT License 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining 11 | a copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | import ( 29 | "encoding/binary" 30 | "math/bits" 31 | "reflect" 32 | "unsafe" 33 | ) 34 | 35 | const ( 36 | // hash input allowed sizes 37 | byteSize = 1 << iota 38 | wordSize 39 | dwordSize 40 | qwordSize 41 | owordSize 42 | ) 43 | 44 | const ( 45 | prime1 uint64 = 11400714785074694791 46 | prime2 uint64 = 14029467366897019727 47 | prime3 uint64 = 1609587929392839161 48 | prime4 uint64 = 9650029242287828579 49 | prime5 uint64 = 2870177450012600261 50 | ) 51 | 52 | var prime1v = prime1 53 | 54 | func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } 55 | func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } 56 | 57 | func round(acc, input uint64) uint64 { 58 | acc += input * prime2 59 | acc = rol31(acc) 60 | acc *= prime1 61 | return acc 62 | } 63 | 64 | func mergeRound(acc, val uint64) uint64 { 65 | val = round(0, val) 66 | acc ^= val 67 | acc = acc*prime1 + prime4 68 | return acc 69 | } 70 | 71 | func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } 72 | func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } 73 | func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } 74 | func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } 75 | func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } 76 | func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } 77 | func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } 78 | func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } 79 | 80 | // xxHash implementation for known key type sizes, minimal with no branching 81 | var ( 82 | // byte hasher, key size -> 1 byte 83 | byteHasher = func(key uint8) uintptr { 84 | h := prime5 + 1 85 | h ^= uint64(key) * prime5 86 | h = bits.RotateLeft64(h, 11) * prime1 87 | h ^= h >> 33 88 | h *= prime2 89 | h ^= h >> 29 90 | h *= prime3 91 | h ^= h >> 32 92 | return uintptr(h) 93 | } 94 | 95 | // word hasher, key size -> 2 bytes 96 | wordHasher = func(key uint16) uintptr { 97 | h := prime5 + 2 98 | h ^= (uint64(key) & 0xff) * prime5 99 | h = bits.RotateLeft64(h, 11) * prime1 100 | h ^= ((uint64(key) >> 8) & 0xff) * prime5 101 | h = bits.RotateLeft64(h, 11) * prime1 102 | h ^= h >> 33 103 | h *= prime2 104 | h ^= h >> 29 105 | h *= prime3 106 | h ^= h >> 32 107 | return uintptr(h) 108 | } 109 | 110 | // dword hasher, key size -> 4 bytes 111 | dwordHasher = func(key uint32) uintptr { 112 | h := prime5 + 4 113 | h ^= uint64(key) * prime1 114 | h = bits.RotateLeft64(h, 23)*prime2 + prime3 115 | h ^= h >> 33 116 | h *= prime2 117 | h ^= h >> 29 118 | h *= prime3 119 | h ^= h >> 32 120 | return uintptr(h) 121 | } 122 | 123 | // separate dword hasher for float32 type 124 | // required for casting float32 to unsigned integer type without any loss of bits 125 | // Example :- casting uint32(1.3) will drop off the 0.3 decimal part but using *(*uint32)(unsafe.Pointer(&key)) will retain all bits (both the integer as well as the decimal part) 126 | // this will ensure correctness of the hash 127 | float32Hasher = func(key float32) uintptr { 128 | h := prime5 + 4 129 | h ^= uint64(*(*uint32)(unsafe.Pointer(&key))) * prime1 130 | h = bits.RotateLeft64(h, 23)*prime2 + prime3 131 | h ^= h >> 33 132 | h *= prime2 133 | h ^= h >> 29 134 | h *= prime3 135 | h ^= h >> 32 136 | return uintptr(h) 137 | } 138 | 139 | // qword hasher, key size -> 8 bytes 140 | qwordHasher = func(key uint64) uintptr { 141 | k1 := key * prime2 142 | k1 = bits.RotateLeft64(k1, 31) 143 | k1 *= prime1 144 | h := (prime5 + 8) ^ k1 145 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 146 | h ^= h >> 33 147 | h *= prime2 148 | h ^= h >> 29 149 | h *= prime3 150 | h ^= h >> 32 151 | return uintptr(h) 152 | } 153 | 154 | // separate qword hasher for float64 type 155 | // for reason see definition of float32Hasher on line 127 156 | float64Hasher = func(key float64) uintptr { 157 | k1 := *(*uint64)(unsafe.Pointer(&key)) * prime2 158 | k1 = bits.RotateLeft64(k1, 31) 159 | k1 *= prime1 160 | h := (prime5 + 8) ^ k1 161 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 162 | h ^= h >> 33 163 | h *= prime2 164 | h ^= h >> 29 165 | h *= prime3 166 | h ^= h >> 32 167 | return uintptr(h) 168 | } 169 | 170 | // separate qword hasher for complex64 type 171 | complex64Hasher = func(key complex64) uintptr { 172 | k1 := *(*uint64)(unsafe.Pointer(&key)) * prime2 173 | k1 = bits.RotateLeft64(k1, 31) 174 | k1 *= prime1 175 | h := (prime5 + 8) ^ k1 176 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 177 | h ^= h >> 33 178 | h *= prime2 179 | h ^= h >> 29 180 | h *= prime3 181 | h ^= h >> 32 182 | return uintptr(h) 183 | } 184 | ) 185 | 186 | func (m *Map[K, V]) setDefaultHasher() { 187 | // default hash functions 188 | switch reflect.TypeOf(*new(K)).Kind() { 189 | case reflect.String: 190 | // use default xxHash algorithm for key of any size for golang string data type 191 | m.hasher = func(key K) uintptr { 192 | sh := (*reflect.StringHeader)(unsafe.Pointer(&key)) 193 | b := unsafe.Slice((*byte)(unsafe.Pointer(sh.Data)), sh.Len) 194 | n := sh.Len 195 | var h uint64 196 | 197 | if n >= 32 { 198 | v1 := prime1v + prime2 199 | v2 := prime2 200 | v3 := uint64(0) 201 | v4 := -prime1v 202 | for len(b) >= 32 { 203 | v1 = round(v1, u64(b[0:8:len(b)])) 204 | v2 = round(v2, u64(b[8:16:len(b)])) 205 | v3 = round(v3, u64(b[16:24:len(b)])) 206 | v4 = round(v4, u64(b[24:32:len(b)])) 207 | b = b[32:len(b):len(b)] 208 | } 209 | h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) 210 | h = mergeRound(h, v1) 211 | h = mergeRound(h, v2) 212 | h = mergeRound(h, v3) 213 | h = mergeRound(h, v4) 214 | } else { 215 | h = prime5 216 | } 217 | 218 | h += uint64(n) 219 | 220 | i, end := 0, len(b) 221 | for ; i+8 <= end; i += 8 { 222 | k1 := round(0, u64(b[i:i+8:len(b)])) 223 | h ^= k1 224 | h = rol27(h)*prime1 + prime4 225 | } 226 | if i+4 <= end { 227 | h ^= uint64(u32(b[i:i+4:len(b)])) * prime1 228 | h = rol23(h)*prime2 + prime3 229 | i += 4 230 | } 231 | for ; i < end; i++ { 232 | h ^= uint64(b[i]) * prime5 233 | h = rol11(h) * prime1 234 | } 235 | 236 | h ^= h >> 33 237 | h *= prime2 238 | h ^= h >> 29 239 | h *= prime3 240 | h ^= h >> 32 241 | 242 | return uintptr(h) 243 | } 244 | case reflect.Int, reflect.Uint, reflect.Uintptr, reflect.UnsafePointer: 245 | switch intSizeBytes { 246 | case 2: 247 | // word hasher 248 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&wordHasher)) 249 | case 4: 250 | // dword hasher 251 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&dwordHasher)) 252 | case 8: 253 | // qword hasher 254 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&qwordHasher)) 255 | } 256 | case reflect.Int8, reflect.Uint8: 257 | // byte hasher 258 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&byteHasher)) 259 | case reflect.Int16, reflect.Uint16: 260 | // word hasher 261 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&wordHasher)) 262 | case reflect.Int32, reflect.Uint32: 263 | // dword hasher 264 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&dwordHasher)) 265 | case reflect.Float32: 266 | // custom float32 dword hasher 267 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&float32Hasher)) 268 | case reflect.Int64, reflect.Uint64: 269 | // qword hasher 270 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&qwordHasher)) 271 | case reflect.Float64: 272 | // custom float64 qword hasher 273 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&float64Hasher)) 274 | case reflect.Complex64: 275 | // custom complex64 qword hasher 276 | m.hasher = *(*func(K) uintptr)(unsafe.Pointer(&complex64Hasher)) 277 | case reflect.Complex128: 278 | // oword hasher, key size -> 16 bytes 279 | m.hasher = func(key K) uintptr { 280 | b := *(*[owordSize]byte)(unsafe.Pointer(&key)) 281 | h := prime5 + 16 282 | 283 | val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | 284 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 285 | 286 | k1 := val * prime2 287 | k1 = bits.RotateLeft64(k1, 31) 288 | k1 *= prime1 289 | 290 | h ^= k1 291 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 292 | 293 | val = uint64(b[8]) | uint64(b[9])<<8 | uint64(b[10])<<16 | uint64(b[11])<<24 | 294 | uint64(b[12])<<32 | uint64(b[13])<<40 | uint64(b[14])<<48 | uint64(b[15])<<56 295 | 296 | k1 = val * prime2 297 | k1 = bits.RotateLeft64(k1, 31) 298 | k1 *= prime1 299 | 300 | h ^= k1 301 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 302 | 303 | h ^= h >> 33 304 | h *= prime2 305 | h ^= h >> 29 306 | h *= prime3 307 | h ^= h >> 32 308 | 309 | return uintptr(h) 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /e2e_test.go: -------------------------------------------------------------------------------- 1 | package haxmap 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "sync" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type Animal struct { 14 | name string 15 | } 16 | 17 | func TestMapCreation(t *testing.T) { 18 | m := New[int, int]() 19 | if m.Len() != 0 { 20 | t.Errorf("new map should be empty but has %d items.", m.Len()) 21 | } 22 | 23 | t.Run("default size is used when zero is provided", func(t *testing.T) { 24 | m := New[int, int](0) 25 | index := m.metadata.Load().index 26 | if len(index) != defaultSize { 27 | t.Error("map index size is not as expected") 28 | } 29 | }) 30 | } 31 | 32 | func TestOverwrite(t *testing.T) { 33 | type customUint uint 34 | m := New[customUint, string]() 35 | key := customUint(1) 36 | cat := "cat" 37 | tiger := "tiger" 38 | 39 | m.Set(key, cat) 40 | m.Set(key, tiger) 41 | 42 | if m.Len() != 1 { 43 | t.Errorf("map should contain exactly one element but has %v items.", m.Len()) 44 | } 45 | 46 | item, ok := m.Get(key) // Retrieve inserted element. 47 | if !ok { 48 | t.Error("ok should be true for item stored within the map.") 49 | } 50 | if item != tiger { 51 | t.Error("wrong item returned.") 52 | } 53 | } 54 | 55 | func TestSet(t *testing.T) { 56 | m := New[int, string](4) 57 | 58 | m.Set(4, "cat") 59 | m.Set(3, "cat") 60 | m.Set(2, "tiger") 61 | m.Set(1, "tiger") 62 | 63 | if m.Len() != 4 { 64 | t.Error("map should contain exactly 4 elements.") 65 | } 66 | } 67 | 68 | // From bug https://github.com/alphadose/haxmap/issues/33 69 | func TestSet2(t *testing.T) { 70 | h := New[int, string]() 71 | for i := 1; i <= 10; i++ { 72 | h.Set(i, strconv.Itoa(i)) 73 | } 74 | for i := 1; i <= 10; i++ { 75 | h.Del(i) 76 | } 77 | for i := 1; i <= 10; i++ { 78 | h.Set(i, strconv.Itoa(i)) 79 | } 80 | for i := 1; i <= 10; i++ { 81 | id, ok := h.Get(i) 82 | if !ok { 83 | t.Error("ok should be true for item stored within the map.") 84 | } 85 | if id != strconv.Itoa(i) { 86 | t.Error("item is not as expected.") 87 | } 88 | } 89 | } 90 | 91 | func TestGet(t *testing.T) { 92 | m := New[string, string]() 93 | cat := "cat" 94 | key := "animal" 95 | 96 | _, ok := m.Get(key) // Get a missing element. 97 | if ok { 98 | t.Error("ok should be false when item is missing from map.") 99 | } 100 | 101 | m.Set(key, cat) 102 | 103 | _, ok = m.Get("human") // Get a missing element. 104 | if ok { 105 | t.Error("ok should be false when item is missing from map.") 106 | } 107 | 108 | value, ok := m.Get(key) // Retrieve inserted element. 109 | if !ok { 110 | t.Error("ok should be true for item stored within the map.") 111 | } 112 | 113 | if value != cat { 114 | t.Error("item was modified.") 115 | } 116 | } 117 | 118 | func TestGrow(t *testing.T) { 119 | m := New[uint, uint]() 120 | m.Grow(63) 121 | d := m.metadata.Load() 122 | log := int(math.Log2(64)) 123 | expectedSize := uintptr(strconv.IntSize - log) 124 | if d.keyshifts != expectedSize { 125 | t.Errorf("Grow operation did not result in correct internal map data structure, Dump -> %#v", d) 126 | } 127 | } 128 | 129 | func TestGrow2(t *testing.T) { 130 | size := 64 131 | m := New[int, any](uintptr(size)) 132 | for i := 0; i < 10000; i++ { 133 | m.Set(i, nil) 134 | m.Del(i) 135 | if n := len(m.metadata.Load().index); n != size { 136 | t.Fatalf("map should not be resized, new size: %d", n) 137 | } 138 | } 139 | } 140 | 141 | func TestFillrate(t *testing.T) { 142 | m := New[int, any]() 143 | for i := 0; i < 1000; i++ { 144 | m.Set(i, nil) 145 | } 146 | for i := 0; i < 1000; i++ { 147 | m.Del(i) 148 | } 149 | if fr := m.Fillrate(); fr != 0 { 150 | t.Errorf("Fillrate should be zero when the map is empty, fillrate: %v", fr) 151 | } 152 | } 153 | 154 | func TestDelete(t *testing.T) { 155 | m := New[int, *Animal]() 156 | cat := &Animal{"cat"} 157 | tiger := &Animal{"tiger"} 158 | 159 | m.Set(1, cat) 160 | m.Set(2, tiger) 161 | m.Del(0) 162 | m.Del(3, 4, 5) 163 | if m.Len() != 2 { 164 | t.Error("map should contain exactly two elements.") 165 | } 166 | m.Del(1, 2, 1) 167 | 168 | if m.Len() != 0 { 169 | t.Error("map should be empty.") 170 | } 171 | 172 | _, ok := m.Get(1) // Get a missing element. 173 | if ok { 174 | t.Error("ok should be false when item is missing from map.") 175 | } 176 | } 177 | 178 | // From bug https://github.com/alphadose/haxmap/issues/11 179 | func TestDelete2(t *testing.T) { 180 | m := New[int, string]() 181 | m.Set(1, "one") 182 | m.Del(1) // delegate key 1 183 | if m.Len() != 0 { 184 | t.Fail() 185 | } 186 | // Still can traverse the key/value pair ? 187 | m.ForEach(func(key int, value string) bool { 188 | t.Fail() 189 | return true 190 | }) 191 | } 192 | 193 | // from https://pkg.go.dev/sync#Map.LoadOrStore 194 | func TestGetOrSet(t *testing.T) { 195 | var ( 196 | m = New[int, string]() 197 | data = "one" 198 | ) 199 | if val, loaded := m.GetOrSet(1, data); loaded { 200 | t.Error("Value should not have been present") 201 | } else if val != data { 202 | t.Error("Returned value should be the same as given value if absent") 203 | } 204 | if val, loaded := m.GetOrSet(1, data); !loaded { 205 | t.Error("Value should have been present") 206 | } else if val != data { 207 | t.Error("Returned value should be the same as given value") 208 | } 209 | } 210 | 211 | func TestForEach(t *testing.T) { 212 | m := New[int, *Animal]() 213 | 214 | m.ForEach(func(i int, a *Animal) bool { 215 | t.Errorf("map should be empty but got key -> %d and value -> %#v.", i, a) 216 | return true 217 | }) 218 | 219 | itemCount := 16 220 | for i := itemCount; i > 0; i-- { 221 | m.Set(i, &Animal{strconv.Itoa(i)}) 222 | } 223 | 224 | counter := 0 225 | m.ForEach(func(i int, a *Animal) bool { 226 | if a == nil { 227 | t.Error("Expecting an object.") 228 | } 229 | counter++ 230 | return true 231 | }) 232 | 233 | if counter != itemCount { 234 | t.Error("Returned item count did not match.") 235 | } 236 | } 237 | 238 | func TestClear(t *testing.T) { 239 | m := New[int, any]() 240 | for i := 0; i < 100; i++ { 241 | m.Set(i, nil) 242 | } 243 | m.Clear() 244 | if m.Len() != 0 { 245 | t.Error("map size should be zero after clear") 246 | } 247 | if m.Fillrate() != 0 { 248 | t.Error("fillrate should be zero after clear") 249 | } 250 | log := int(math.Log2(defaultSize)) 251 | expectedSize := uintptr(strconv.IntSize - log) 252 | if m.metadata.Load().keyshifts != expectedSize { 253 | t.Error("keyshift is not as expected after clear") 254 | } 255 | for i := 0; i < 100; i++ { 256 | if _, ok := m.Get(i); ok { 257 | t.Error("the entries should not be existing in the map after clear") 258 | } 259 | } 260 | } 261 | 262 | func TestMapParallel(t *testing.T) { 263 | max := 10 264 | dur := 2 * time.Second 265 | m := New[int, int]() 266 | do := func(t *testing.T, max int, d time.Duration, fn func(*testing.T, int)) <-chan error { 267 | t.Helper() 268 | done := make(chan error) 269 | var times int64 270 | // This goroutines will terminate test in case if closure hangs. 271 | go func() { 272 | for { 273 | select { 274 | case <-time.After(d + 500*time.Millisecond): 275 | if atomic.LoadInt64(×) == 0 { 276 | done <- fmt.Errorf("closure was not executed even once, something blocks it") 277 | } 278 | close(done) 279 | case <-done: 280 | } 281 | } 282 | }() 283 | go func() { 284 | timer := time.NewTimer(d) 285 | defer timer.Stop() 286 | InfLoop: 287 | for { 288 | for i := 0; i < max; i++ { 289 | select { 290 | case <-timer.C: 291 | break InfLoop 292 | default: 293 | } 294 | fn(t, i) 295 | atomic.AddInt64(×, 1) 296 | } 297 | } 298 | close(done) 299 | }() 300 | return done 301 | } 302 | wait := func(t *testing.T, done <-chan error) { 303 | t.Helper() 304 | if err := <-done; err != nil { 305 | t.Error(err) 306 | } 307 | } 308 | // Initial fill. 309 | for i := 0; i < max; i++ { 310 | m.Set(i, i) 311 | } 312 | t.Run("set_get", func(t *testing.T) { 313 | doneSet := do(t, max, dur, func(t *testing.T, i int) { 314 | m.Set(i, i) 315 | }) 316 | doneGet := do(t, max, dur, func(t *testing.T, i int) { 317 | if _, ok := m.Get(i); !ok { 318 | t.Errorf("missing value for key: %d", i) 319 | } 320 | }) 321 | wait(t, doneSet) 322 | wait(t, doneGet) 323 | }) 324 | t.Run("delete", func(t *testing.T) { 325 | doneDel := do(t, max, dur, func(t *testing.T, i int) { 326 | m.Del(i) 327 | }) 328 | wait(t, doneDel) 329 | }) 330 | } 331 | 332 | func TestMapConcurrentWrites(t *testing.T) { 333 | blocks := New[string, struct{}]() 334 | 335 | var wg sync.WaitGroup 336 | for i := 0; i < 100; i++ { 337 | 338 | wg.Add(1) 339 | go func(blocks *Map[string, struct{}], i int) { 340 | defer wg.Done() 341 | 342 | blocks.Set(strconv.Itoa(i), struct{}{}) 343 | 344 | wg.Add(1) 345 | go func(blocks *Map[string, struct{}], i int) { 346 | defer wg.Done() 347 | 348 | blocks.Get(strconv.Itoa(i)) 349 | }(blocks, i) 350 | }(blocks, i) 351 | } 352 | 353 | wg.Wait() 354 | } 355 | 356 | // Collision test case when hash key is 0 in value for all entries 357 | func TestHash0Collision(t *testing.T) { 358 | m := New[string, int]() 359 | staticHasher := func(key string) uintptr { 360 | return 0 361 | } 362 | m.SetHasher(staticHasher) 363 | m.Set("1", 1) 364 | m.Set("2", 2) 365 | _, ok := m.Get("1") 366 | if !ok { 367 | t.Error("1 not found") 368 | } 369 | _, ok = m.Get("2") 370 | if !ok { 371 | t.Error("2 not found") 372 | } 373 | } 374 | 375 | // test map freezing issue 376 | // https://github.com/alphadose/haxmap/issues/7 377 | // https://github.com/alphadose/haxmap/issues/8 378 | // Update:- Solved now 379 | func TestInfiniteLoop(t *testing.T) { 380 | t.Run("infinite loop", func(b *testing.T) { 381 | m := New[int, int](512) 382 | for i := 0; i < 112050; i++ { 383 | if i > 112024 { 384 | m.Set(i, i) // set debug point here and step into until .inject 385 | } else { 386 | m.Set(i, i) 387 | } 388 | } 389 | }) 390 | } 391 | 392 | // https://github.com/alphadose/haxmap/issues/18 393 | // test compare and swap 394 | func TestCAS(t *testing.T) { 395 | type custom struct { 396 | val int 397 | } 398 | m := New[string, custom]() 399 | m.Set("1", custom{val: 1}) 400 | if m.CompareAndSwap("1", custom{val: 420}, custom{val: 2}) { 401 | t.Error("Invalid Compare and Swap") 402 | } 403 | if !m.CompareAndSwap("1", custom{val: 1}, custom{val: 2}) { 404 | t.Error("Compare and Swap Failed") 405 | } 406 | val, ok := m.Get("1") 407 | if !ok { 408 | t.Error("Key doesnt exists") 409 | } 410 | if val.val != 2 { 411 | t.Error("Invalid Compare and Swap value returned") 412 | } 413 | } 414 | 415 | // https://github.com/alphadose/haxmap/issues/18 416 | // test swap 417 | func TestSwap(t *testing.T) { 418 | m := New[string, int]() 419 | m.Set("1", 1) 420 | val, swapped := m.Swap("1", 2) 421 | if !swapped { 422 | t.Error("Swap failed") 423 | } 424 | if val != 1 { 425 | t.Error("Old value not returned in swal") 426 | } 427 | val, ok := m.Get("1") 428 | if !ok { 429 | t.Error("Key doesnt exists") 430 | } 431 | if val != 2 { 432 | t.Error("New value not set") 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /benchmarks/results.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: arm64 3 | BenchmarkHaxMapReadsOnly-8 159657 7386 ns/op 0 B/op 0 allocs/op 4 | BenchmarkHaxMapReadsOnly-8 163044 7380 ns/op 0 B/op 0 allocs/op 5 | BenchmarkHaxMapReadsOnly-8 160443 7367 ns/op 0 B/op 0 allocs/op 6 | BenchmarkHaxMapReadsOnly-8 156307 7296 ns/op 0 B/op 0 allocs/op 7 | BenchmarkHaxMapReadsOnly-8 160837 7351 ns/op 0 B/op 0 allocs/op 8 | BenchmarkHaxMapReadsOnly-8 159753 7494 ns/op 0 B/op 0 allocs/op 9 | BenchmarkHaxMapReadsOnly-8 163380 7299 ns/op 0 B/op 0 allocs/op 10 | BenchmarkHaxMapReadsOnly-8 161568 7318 ns/op 0 B/op 0 allocs/op 11 | BenchmarkHaxMapReadsOnly-8 157852 7588 ns/op 0 B/op 0 allocs/op 12 | BenchmarkHaxMapReadsOnly-8 157222 7279 ns/op 0 B/op 0 allocs/op 13 | BenchmarkHaxMapReadsOnly-8 159903 7299 ns/op 0 B/op 0 allocs/op 14 | BenchmarkHaxMapReadsOnly-8 162555 7391 ns/op 0 B/op 0 allocs/op 15 | BenchmarkHaxMapReadsOnly-8 161818 7493 ns/op 0 B/op 0 allocs/op 16 | BenchmarkHaxMapReadsOnly-8 161666 7272 ns/op 0 B/op 0 allocs/op 17 | BenchmarkHaxMapReadsOnly-8 162126 7318 ns/op 0 B/op 0 allocs/op 18 | BenchmarkHaxMapReadsOnly-8 162903 7303 ns/op 0 B/op 0 allocs/op 19 | BenchmarkHaxMapReadsOnly-8 161302 7393 ns/op 0 B/op 0 allocs/op 20 | BenchmarkHaxMapReadsOnly-8 160670 8210 ns/op 0 B/op 0 allocs/op 21 | BenchmarkHaxMapReadsOnly-8 150055 7303 ns/op 0 B/op 0 allocs/op 22 | BenchmarkHaxMapReadsOnly-8 162675 7289 ns/op 0 B/op 0 allocs/op 23 | BenchmarkHaxMapReadsWithWrites-8 130348 9277 ns/op 1377 B/op 172 allocs/op 24 | BenchmarkHaxMapReadsWithWrites-8 138121 8572 ns/op 1338 B/op 167 allocs/op 25 | BenchmarkHaxMapReadsWithWrites-8 136130 8573 ns/op 1331 B/op 166 allocs/op 26 | BenchmarkHaxMapReadsWithWrites-8 126922 8712 ns/op 1339 B/op 167 allocs/op 27 | BenchmarkHaxMapReadsWithWrites-8 120144 8639 ns/op 1337 B/op 167 allocs/op 28 | BenchmarkHaxMapReadsWithWrites-8 135499 8929 ns/op 1324 B/op 165 allocs/op 29 | BenchmarkHaxMapReadsWithWrites-8 134961 8679 ns/op 1300 B/op 162 allocs/op 30 | BenchmarkHaxMapReadsWithWrites-8 132897 8833 ns/op 1385 B/op 173 allocs/op 31 | BenchmarkHaxMapReadsWithWrites-8 125212 8822 ns/op 1300 B/op 162 allocs/op 32 | BenchmarkHaxMapReadsWithWrites-8 132070 8849 ns/op 1342 B/op 167 allocs/op 33 | BenchmarkHaxMapReadsWithWrites-8 133047 8955 ns/op 1381 B/op 172 allocs/op 34 | BenchmarkHaxMapReadsWithWrites-8 129302 8866 ns/op 1300 B/op 162 allocs/op 35 | BenchmarkHaxMapReadsWithWrites-8 124704 9043 ns/op 1316 B/op 164 allocs/op 36 | BenchmarkHaxMapReadsWithWrites-8 130328 9063 ns/op 1342 B/op 167 allocs/op 37 | BenchmarkHaxMapReadsWithWrites-8 131508 9008 ns/op 1397 B/op 174 allocs/op 38 | BenchmarkHaxMapReadsWithWrites-8 120746 8964 ns/op 1316 B/op 164 allocs/op 39 | BenchmarkHaxMapReadsWithWrites-8 126921 9063 ns/op 1247 B/op 155 allocs/op 40 | BenchmarkHaxMapReadsWithWrites-8 129614 9004 ns/op 1281 B/op 160 allocs/op 41 | BenchmarkHaxMapReadsWithWrites-8 128635 9077 ns/op 1342 B/op 167 allocs/op 42 | BenchmarkHaxMapReadsWithWrites-8 128696 9073 ns/op 1270 B/op 158 allocs/op 43 | BenchmarkGoSyncMapReadsOnly-8 52282 21666 ns/op 0 B/op 0 allocs/op 44 | BenchmarkGoSyncMapReadsOnly-8 54615 21672 ns/op 0 B/op 0 allocs/op 45 | BenchmarkGoSyncMapReadsOnly-8 54711 22040 ns/op 0 B/op 0 allocs/op 46 | BenchmarkGoSyncMapReadsOnly-8 54650 22338 ns/op 0 B/op 0 allocs/op 47 | BenchmarkGoSyncMapReadsOnly-8 53565 21892 ns/op 0 B/op 0 allocs/op 48 | BenchmarkGoSyncMapReadsOnly-8 53130 22426 ns/op 0 B/op 0 allocs/op 49 | BenchmarkGoSyncMapReadsOnly-8 53137 22716 ns/op 0 B/op 0 allocs/op 50 | BenchmarkGoSyncMapReadsOnly-8 50728 22260 ns/op 0 B/op 0 allocs/op 51 | BenchmarkGoSyncMapReadsOnly-8 53535 22395 ns/op 0 B/op 0 allocs/op 52 | BenchmarkGoSyncMapReadsOnly-8 53095 22301 ns/op 0 B/op 0 allocs/op 53 | BenchmarkGoSyncMapReadsOnly-8 53906 22506 ns/op 0 B/op 0 allocs/op 54 | BenchmarkGoSyncMapReadsOnly-8 53560 22539 ns/op 0 B/op 0 allocs/op 55 | BenchmarkGoSyncMapReadsOnly-8 52941 22168 ns/op 0 B/op 0 allocs/op 56 | BenchmarkGoSyncMapReadsOnly-8 53383 22327 ns/op 0 B/op 0 allocs/op 57 | BenchmarkGoSyncMapReadsOnly-8 51583 22411 ns/op 0 B/op 0 allocs/op 58 | BenchmarkGoSyncMapReadsOnly-8 53574 22544 ns/op 0 B/op 0 allocs/op 59 | BenchmarkGoSyncMapReadsOnly-8 53410 22581 ns/op 0 B/op 0 allocs/op 60 | BenchmarkGoSyncMapReadsOnly-8 52837 22898 ns/op 0 B/op 0 allocs/op 61 | BenchmarkGoSyncMapReadsOnly-8 53112 22492 ns/op 0 B/op 0 allocs/op 62 | BenchmarkGoSyncMapReadsOnly-8 53144 22683 ns/op 0 B/op 0 allocs/op 63 | BenchmarkGoSyncMapReadsWithWrites-8 47115 25384 ns/op 6217 B/op 576 allocs/op 64 | BenchmarkGoSyncMapReadsWithWrites-8 43969 25381 ns/op 5847 B/op 542 allocs/op 65 | BenchmarkGoSyncMapReadsWithWrites-8 47504 25822 ns/op 6318 B/op 586 allocs/op 66 | BenchmarkGoSyncMapReadsWithWrites-8 46658 25463 ns/op 6115 B/op 567 allocs/op 67 | BenchmarkGoSyncMapReadsWithWrites-8 46920 25920 ns/op 5910 B/op 548 allocs/op 68 | BenchmarkGoSyncMapReadsWithWrites-8 46004 25933 ns/op 6011 B/op 557 allocs/op 69 | BenchmarkGoSyncMapReadsWithWrites-8 47110 25772 ns/op 6315 B/op 585 allocs/op 70 | BenchmarkGoSyncMapReadsWithWrites-8 45895 26077 ns/op 6092 B/op 565 allocs/op 71 | BenchmarkGoSyncMapReadsWithWrites-8 46814 25798 ns/op 6713 B/op 622 allocs/op 72 | BenchmarkGoSyncMapReadsWithWrites-8 47662 25835 ns/op 6034 B/op 559 allocs/op 73 | BenchmarkGoSyncMapReadsWithWrites-8 46875 25990 ns/op 6127 B/op 568 allocs/op 74 | BenchmarkGoSyncMapReadsWithWrites-8 46239 25744 ns/op 6154 B/op 570 allocs/op 75 | BenchmarkGoSyncMapReadsWithWrites-8 46124 26065 ns/op 6285 B/op 582 allocs/op 76 | BenchmarkGoSyncMapReadsWithWrites-8 46747 25493 ns/op 5883 B/op 545 allocs/op 77 | BenchmarkGoSyncMapReadsWithWrites-8 46593 26290 ns/op 6319 B/op 586 allocs/op 78 | BenchmarkGoSyncMapReadsWithWrites-8 46627 25546 ns/op 5963 B/op 553 allocs/op 79 | BenchmarkGoSyncMapReadsWithWrites-8 44914 25823 ns/op 5945 B/op 551 allocs/op 80 | BenchmarkGoSyncMapReadsWithWrites-8 46809 26123 ns/op 6144 B/op 569 allocs/op 81 | BenchmarkGoSyncMapReadsWithWrites-8 45345 25702 ns/op 6023 B/op 558 allocs/op 82 | BenchmarkGoSyncMapReadsWithWrites-8 46417 25688 ns/op 5761 B/op 534 allocs/op 83 | BenchmarkCornelkMapReadsOnly-8 145736 8236 ns/op 0 B/op 0 allocs/op 84 | BenchmarkCornelkMapReadsOnly-8 143426 8391 ns/op 0 B/op 0 allocs/op 85 | BenchmarkCornelkMapReadsOnly-8 147019 8262 ns/op 0 B/op 0 allocs/op 86 | BenchmarkCornelkMapReadsOnly-8 147057 8310 ns/op 0 B/op 0 allocs/op 87 | BenchmarkCornelkMapReadsOnly-8 144535 8345 ns/op 0 B/op 0 allocs/op 88 | BenchmarkCornelkMapReadsOnly-8 146323 8501 ns/op 0 B/op 0 allocs/op 89 | BenchmarkCornelkMapReadsOnly-8 147325 8513 ns/op 0 B/op 0 allocs/op 90 | BenchmarkCornelkMapReadsOnly-8 149396 8217 ns/op 0 B/op 0 allocs/op 91 | BenchmarkCornelkMapReadsOnly-8 146016 8270 ns/op 0 B/op 0 allocs/op 92 | BenchmarkCornelkMapReadsOnly-8 149578 8513 ns/op 0 B/op 0 allocs/op 93 | BenchmarkCornelkMapReadsOnly-8 148736 8447 ns/op 0 B/op 0 allocs/op 94 | BenchmarkCornelkMapReadsOnly-8 147804 8252 ns/op 0 B/op 0 allocs/op 95 | BenchmarkCornelkMapReadsOnly-8 150673 8885 ns/op 0 B/op 0 allocs/op 96 | BenchmarkCornelkMapReadsOnly-8 148531 8411 ns/op 0 B/op 0 allocs/op 97 | BenchmarkCornelkMapReadsOnly-8 150447 8392 ns/op 0 B/op 0 allocs/op 98 | BenchmarkCornelkMapReadsOnly-8 148462 8259 ns/op 0 B/op 0 allocs/op 99 | BenchmarkCornelkMapReadsOnly-8 151392 8406 ns/op 0 B/op 0 allocs/op 100 | BenchmarkCornelkMapReadsOnly-8 147799 8377 ns/op 0 B/op 0 allocs/op 101 | BenchmarkCornelkMapReadsOnly-8 146872 8368 ns/op 0 B/op 0 allocs/op 102 | BenchmarkCornelkMapReadsOnly-8 135110 8599 ns/op 0 B/op 0 allocs/op 103 | BenchmarkCornelkMapReadsWithWrites-8 109732 9542 ns/op 1531 B/op 191 allocs/op 104 | BenchmarkCornelkMapReadsWithWrites-8 127246 9609 ns/op 1586 B/op 198 allocs/op 105 | BenchmarkCornelkMapReadsWithWrites-8 116518 9563 ns/op 1597 B/op 199 allocs/op 106 | BenchmarkCornelkMapReadsWithWrites-8 130189 9723 ns/op 1505 B/op 188 allocs/op 107 | BenchmarkCornelkMapReadsWithWrites-8 117034 9792 ns/op 1489 B/op 186 allocs/op 108 | BenchmarkCornelkMapReadsWithWrites-8 127542 9586 ns/op 1603 B/op 200 allocs/op 109 | BenchmarkCornelkMapReadsWithWrites-8 126703 9995 ns/op 1577 B/op 197 allocs/op 110 | BenchmarkCornelkMapReadsWithWrites-8 127746 9778 ns/op 1610 B/op 201 allocs/op 111 | BenchmarkCornelkMapReadsWithWrites-8 122938 9670 ns/op 1524 B/op 190 allocs/op 112 | BenchmarkCornelkMapReadsWithWrites-8 128198 9610 ns/op 1559 B/op 194 allocs/op 113 | BenchmarkCornelkMapReadsWithWrites-8 127722 9522 ns/op 1552 B/op 194 allocs/op 114 | BenchmarkCornelkMapReadsWithWrites-8 129921 9758 ns/op 1556 B/op 194 allocs/op 115 | BenchmarkCornelkMapReadsWithWrites-8 127369 9552 ns/op 1538 B/op 192 allocs/op 116 | BenchmarkCornelkMapReadsWithWrites-8 126130 9609 ns/op 1608 B/op 201 allocs/op 117 | BenchmarkCornelkMapReadsWithWrites-8 126712 9732 ns/op 1567 B/op 195 allocs/op 118 | BenchmarkCornelkMapReadsWithWrites-8 125959 9492 ns/op 1511 B/op 188 allocs/op 119 | BenchmarkCornelkMapReadsWithWrites-8 124554 9492 ns/op 1583 B/op 197 allocs/op 120 | BenchmarkCornelkMapReadsWithWrites-8 127700 9539 ns/op 1583 B/op 197 allocs/op 121 | BenchmarkCornelkMapReadsWithWrites-8 123895 9706 ns/op 1520 B/op 190 allocs/op 122 | BenchmarkCornelkMapReadsWithWrites-8 129180 9588 ns/op 1560 B/op 195 allocs/op 123 | PASS 124 | ok command-line-arguments 163.562s 125 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package haxmap 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "sort" 7 | "strconv" 8 | "sync/atomic" 9 | "unsafe" 10 | 11 | "golang.org/x/exp/constraints" 12 | ) 13 | 14 | const ( 15 | // defaultSize is the default size for a zero allocated map 16 | defaultSize = 8 17 | 18 | // maxFillRate is the maximum fill rate for the slice before a resize will happen 19 | maxFillRate = 50 20 | 21 | // intSizeBytes is the size in byte of an int or uint value 22 | intSizeBytes = strconv.IntSize >> 3 23 | ) 24 | 25 | // indicates resizing operation status enums 26 | const ( 27 | notResizing uint32 = iota 28 | resizingInProgress 29 | ) 30 | 31 | type ( 32 | hashable interface { 33 | constraints.Integer | constraints.Float | constraints.Complex | ~string | uintptr | ~unsafe.Pointer 34 | } 35 | 36 | // metadata of the hashmap 37 | metadata[K hashable, V any] struct { 38 | keyshifts uintptr // array_size - log2(array_size) 39 | count atomicUintptr // number of filled items 40 | data unsafe.Pointer // pointer to array of map indexes 41 | 42 | // use a struct element with generic params to enable monomorphization (generic code copy-paste) for the parent metadata struct by golang compiler leading to best performance (truly hax) 43 | // else in other cases the generic params will be unnecessarily passed as function parameters everytime instead of monomorphization leading to slower performance 44 | index []*element[K, V] 45 | } 46 | 47 | // Map implements the concurrent hashmap 48 | Map[K hashable, V any] struct { 49 | listHead *element[K, V] // Harris lock-free list of elements in ascending order of hash 50 | hasher func(K) uintptr 51 | metadata atomicPointer[metadata[K, V]] // atomic.Pointer for safe access even during resizing 52 | resizing atomicUint32 53 | numItems atomicUintptr 54 | defaultSize uintptr 55 | } 56 | 57 | // used in deletion of map elements 58 | deletionRequest[K hashable] struct { 59 | keyHash uintptr 60 | key K 61 | } 62 | ) 63 | 64 | // New returns a new HashMap instance with an optional specific initialization size 65 | func New[K hashable, V any](size ...uintptr) *Map[K, V] { 66 | m := &Map[K, V]{listHead: newListHead[K, V]()} 67 | m.numItems.Store(0) 68 | m.defaultSize = defaultSize 69 | if len(size) > 0 && size[0] > 0 { 70 | m.defaultSize = size[0] 71 | } 72 | m.allocate(m.defaultSize) 73 | m.setDefaultHasher() 74 | return m 75 | } 76 | 77 | // Del deletes key/keys from the map 78 | // Bulk deletion is more efficient than deleting keys one by one 79 | func (m *Map[K, V]) Del(keys ...K) { 80 | size := len(keys) 81 | switch { 82 | case size == 0: 83 | return 84 | case size == 1: // delete one 85 | var ( 86 | h = m.hasher(keys[0]) 87 | existing = m.metadata.Load().indexElement(h) 88 | ) 89 | if existing == nil || existing.keyHash > h { 90 | existing = m.listHead.next() 91 | } 92 | for ; existing != nil && existing.keyHash <= h; existing = existing.next() { 93 | if existing.key == keys[0] { 94 | if existing.remove() { // mark node for lazy removal on next pass 95 | m.removeItemFromIndex(existing) // remove node from map index 96 | } 97 | return 98 | } 99 | } 100 | default: // delete multiple entries 101 | var ( 102 | delQ = make([]deletionRequest[K], size) 103 | iter = 0 104 | ) 105 | for idx := 0; idx < size; idx++ { 106 | delQ[idx].keyHash, delQ[idx].key = m.hasher(keys[idx]), keys[idx] 107 | } 108 | 109 | // sort in ascending order of keyhash 110 | sort.Slice(delQ, func(i, j int) bool { 111 | return delQ[i].keyHash < delQ[j].keyHash 112 | }) 113 | 114 | elem := m.metadata.Load().indexElement(delQ[0].keyHash) 115 | 116 | if elem == nil || elem.keyHash > delQ[0].keyHash { 117 | elem = m.listHead.next() 118 | } 119 | 120 | for elem != nil && iter < size { 121 | if elem.keyHash == delQ[iter].keyHash && elem.key == delQ[iter].key { 122 | if elem.remove() { // mark node for lazy removal on next pass 123 | m.removeItemFromIndex(elem) // remove node from map index 124 | } 125 | iter++ 126 | elem = elem.next() 127 | } else if elem.keyHash > delQ[iter].keyHash { 128 | iter++ 129 | } else { 130 | elem = elem.next() 131 | } 132 | } 133 | } 134 | } 135 | 136 | // Get retrieves an element from the map 137 | // returns `false“ if element is absent 138 | func (m *Map[K, V]) Get(key K) (value V, ok bool) { 139 | h := m.hasher(key) 140 | // inline search 141 | for elem := m.metadata.Load().indexElement(h); elem != nil && elem.keyHash <= h; elem = elem.nextPtr.Load() { 142 | if elem.key == key { 143 | value, ok = *elem.value.Load(), !elem.isDeleted() 144 | return 145 | } 146 | } 147 | ok = false 148 | return 149 | } 150 | 151 | // Set tries to update an element if key is present else it inserts a new element 152 | // If a resizing operation is happening concurrently while calling Set() 153 | // then the item might show up in the map only after the resize operation is finished 154 | func (m *Map[K, V]) Set(key K, value V) { 155 | var ( 156 | h = m.hasher(key) 157 | valPtr = &value 158 | alloc *element[K, V] 159 | created = false 160 | data = m.metadata.Load() 161 | existing = data.indexElement(h) 162 | ) 163 | 164 | if existing == nil || existing.keyHash > h { 165 | existing = m.listHead 166 | } 167 | if alloc, created = existing.inject(h, key, valPtr); alloc != nil { 168 | if created { 169 | m.numItems.Add(1) 170 | } 171 | } else { 172 | for existing = m.listHead; alloc == nil; alloc, created = existing.inject(h, key, valPtr) { 173 | } 174 | if created { 175 | m.numItems.Add(1) 176 | } 177 | } 178 | 179 | count := data.addItemToIndex(alloc) 180 | if resizeNeeded(uintptr(len(data.index)), count) && m.resizing.CompareAndSwap(notResizing, resizingInProgress) { 181 | m.grow(0) // double in size 182 | } 183 | } 184 | 185 | // GetOrSet returns the existing value for the key if present 186 | // Otherwise, it stores and returns the given value 187 | // The loaded result is true if the value was loaded, false if stored 188 | func (m *Map[K, V]) GetOrSet(key K, value V) (actual V, loaded bool) { 189 | var ( 190 | h = m.hasher(key) 191 | data = m.metadata.Load() 192 | existing = data.indexElement(h) 193 | ) 194 | // try to get the element if present 195 | for elem := existing; elem != nil && elem.keyHash <= h; elem = elem.nextPtr.Load() { 196 | if elem.key == key && !elem.isDeleted() { 197 | actual, loaded = *elem.value.Load(), true 198 | return 199 | } 200 | } 201 | // Get() failed because element is absent 202 | // store the value given by user 203 | actual, loaded = value, false 204 | 205 | var ( 206 | alloc *element[K, V] 207 | created = false 208 | valPtr = &value 209 | ) 210 | if existing == nil || existing.keyHash > h { 211 | existing = m.listHead 212 | } 213 | if alloc, created = existing.inject(h, key, valPtr); alloc != nil { 214 | if created { 215 | m.numItems.Add(1) 216 | } 217 | } else { 218 | for existing = m.listHead; alloc == nil; alloc, created = existing.inject(h, key, valPtr) { 219 | } 220 | if created { 221 | m.numItems.Add(1) 222 | } 223 | } 224 | 225 | count := data.addItemToIndex(alloc) 226 | if resizeNeeded(uintptr(len(data.index)), count) && m.resizing.CompareAndSwap(notResizing, resizingInProgress) { 227 | m.grow(0) // double in size 228 | } 229 | return 230 | } 231 | 232 | // GetOrCompute is similar to GetOrSet but the value to be set is obtained from a constructor 233 | // the value constructor is called only once 234 | func (m *Map[K, V]) GetOrCompute(key K, valueFn func() V) (actual V, loaded bool) { 235 | var ( 236 | h = m.hasher(key) 237 | data = m.metadata.Load() 238 | existing = data.indexElement(h) 239 | ) 240 | // try to get the element if present 241 | for elem := existing; elem != nil && elem.keyHash <= h; elem = elem.nextPtr.Load() { 242 | if elem.key == key && !elem.isDeleted() { 243 | actual, loaded = *elem.value.Load(), true 244 | return 245 | } 246 | } 247 | // Get() failed because element is absent 248 | // compute the value from the constructor and store it 249 | value := valueFn() 250 | actual, loaded = value, false 251 | 252 | var ( 253 | alloc *element[K, V] 254 | created = false 255 | valPtr = &value 256 | ) 257 | if existing == nil || existing.keyHash > h { 258 | existing = m.listHead 259 | } 260 | if alloc, created = existing.inject(h, key, valPtr); alloc != nil { 261 | if created { 262 | m.numItems.Add(1) 263 | } 264 | } else { 265 | for existing = m.listHead; alloc == nil; alloc, created = existing.inject(h, key, valPtr) { 266 | } 267 | if created { 268 | m.numItems.Add(1) 269 | } 270 | } 271 | 272 | count := data.addItemToIndex(alloc) 273 | if resizeNeeded(uintptr(len(data.index)), count) && m.resizing.CompareAndSwap(notResizing, resizingInProgress) { 274 | m.grow(0) // double in size 275 | } 276 | return 277 | } 278 | 279 | // GetAndDel deletes the key from the map, returning the previous value if any. 280 | func (m *Map[K, V]) GetAndDel(key K) (value V, ok bool) { 281 | var ( 282 | h = m.hasher(key) 283 | existing = m.metadata.Load().indexElement(h) 284 | ) 285 | if existing == nil || existing.keyHash > h { 286 | existing = m.listHead.next() 287 | } 288 | for ; existing != nil && existing.keyHash <= h; existing = existing.next() { 289 | if existing.key == key { 290 | value, ok = *existing.value.Load(), !existing.isDeleted() 291 | if existing.remove() { 292 | m.removeItemFromIndex(existing) 293 | } 294 | return 295 | } 296 | } 297 | return 298 | } 299 | 300 | // CompareAndSwap atomically updates a map entry given its key by comparing current value to `oldValue` 301 | // and setting it to `newValue` if the above comparison is successful 302 | // It returns a boolean indicating whether the CompareAndSwap was successful or not 303 | func (m *Map[K, V]) CompareAndSwap(key K, oldValue, newValue V) bool { 304 | var ( 305 | h = m.hasher(key) 306 | existing = m.metadata.Load().indexElement(h) 307 | ) 308 | if existing == nil || existing.keyHash > h { 309 | existing = m.listHead 310 | } 311 | if _, current, _ := existing.search(h, key); current != nil { 312 | if oldPtr := current.value.Load(); reflect.DeepEqual(*oldPtr, oldValue) { 313 | return current.value.CompareAndSwap(oldPtr, &newValue) 314 | } 315 | } 316 | return false 317 | } 318 | 319 | // Swap atomically swaps the value of a map entry given its key 320 | // It returns the old value if swap was successful and a boolean `swapped` indicating whether the swap was successful or not 321 | func (m *Map[K, V]) Swap(key K, newValue V) (oldValue V, swapped bool) { 322 | var ( 323 | h = m.hasher(key) 324 | existing = m.metadata.Load().indexElement(h) 325 | ) 326 | if existing == nil || existing.keyHash > h { 327 | existing = m.listHead 328 | } 329 | if _, current, _ := existing.search(h, key); current != nil { 330 | oldValue, swapped = *current.value.Swap(&newValue), true 331 | } else { 332 | swapped = false 333 | } 334 | return 335 | } 336 | 337 | // ForEach iterates over key-value pairs and executes the lambda provided for each such pair 338 | // lambda must return `true` to continue iteration and `false` to break iteration 339 | func (m *Map[K, V]) ForEach(lambda func(K, V) bool) { 340 | for item := m.listHead.next(); item != nil && lambda(item.key, *item.value.Load()); item = item.next() { 341 | } 342 | } 343 | 344 | // Grow resizes the hashmap to a new size, gets rounded up to next power of 2 345 | // To double the size of the hashmap use newSize 0 346 | // No resizing is done in case of another resize operation already being in progress 347 | // Growth and map bucket policy is inspired from https://github.com/cornelk/hashmap 348 | func (m *Map[K, V]) Grow(newSize uintptr) { 349 | if m.resizing.CompareAndSwap(notResizing, resizingInProgress) { 350 | m.grow(newSize) 351 | } 352 | } 353 | 354 | // Clear the map by removing all entries in the map. 355 | // This operation resets the underlying metadata to its initial state. 356 | func (m *Map[K, V]) Clear() { 357 | index := make([]*element[K, V], m.defaultSize) 358 | header := (*reflect.SliceHeader)(unsafe.Pointer(&index)) 359 | newdata := &metadata[K, V]{ 360 | keyshifts: strconv.IntSize - log2(m.defaultSize), 361 | data: unsafe.Pointer(header.Data), 362 | index: index, 363 | } 364 | m.listHead.nextPtr.Store(nil) 365 | m.metadata.Store(newdata) 366 | m.numItems.Store(0) 367 | } 368 | 369 | // SetHasher sets the hash function to the one provided by the user 370 | func (m *Map[K, V]) SetHasher(hs func(K) uintptr) { 371 | m.hasher = hs 372 | } 373 | 374 | // Len returns the number of key-value pairs within the map 375 | func (m *Map[K, V]) Len() uintptr { 376 | return m.numItems.Load() 377 | } 378 | 379 | // Fillrate returns the fill rate of the map as an percentage integer 380 | func (m *Map[K, V]) Fillrate() uintptr { 381 | data := m.metadata.Load() 382 | return (data.count.Load() * 100) / uintptr(len(data.index)) 383 | } 384 | 385 | // MarshalJSON implements the json.Marshaler interface. 386 | func (m *Map[K, V]) MarshalJSON() ([]byte, error) { 387 | gomap := make(map[K]V) 388 | for i := m.listHead.next(); i != nil; i = i.next() { 389 | gomap[i.key] = *i.value.Load() 390 | } 391 | return json.Marshal(gomap) 392 | } 393 | 394 | // UnmarshalJSON implements the json.Unmarshaler interface. 395 | func (m *Map[K, V]) UnmarshalJSON(i []byte) error { 396 | gomap := make(map[K]V) 397 | err := json.Unmarshal(i, &gomap) 398 | if err != nil { 399 | return err 400 | } 401 | for k, v := range gomap { 402 | m.Set(k, v) 403 | } 404 | return nil 405 | } 406 | 407 | // allocate map with the given size 408 | func (m *Map[K, V]) allocate(newSize uintptr) { 409 | if m.resizing.CompareAndSwap(notResizing, resizingInProgress) { 410 | m.grow(newSize) 411 | } 412 | } 413 | 414 | // fillIndexItems re-indexes the map given the latest state of the linked list 415 | func (m *Map[K, V]) fillIndexItems(mapData *metadata[K, V]) { 416 | var ( 417 | first = m.listHead.next() 418 | item = first 419 | lastIndex = uintptr(0) 420 | ) 421 | for item != nil { 422 | index := item.keyHash >> mapData.keyshifts 423 | if item == first || index != lastIndex { 424 | mapData.addItemToIndex(item) 425 | lastIndex = index 426 | } 427 | item = item.next() 428 | } 429 | } 430 | 431 | // removeItemFromIndex removes an item from the map index 432 | func (m *Map[K, V]) removeItemFromIndex(item *element[K, V]) { 433 | for { 434 | data := m.metadata.Load() 435 | index := item.keyHash >> data.keyshifts 436 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(data.data) + index*intSizeBytes)) 437 | 438 | next := item.next() 439 | if next != nil && next.keyHash>>data.keyshifts != index { 440 | next = nil // do not set index to next item if it's not the same slice index 441 | } 442 | swappedToNil := atomic.CompareAndSwapPointer(ptr, unsafe.Pointer(item), unsafe.Pointer(next)) && next == nil 443 | 444 | if data == m.metadata.Load() { // check that no resize happened 445 | m.numItems.Add(^uintptr(0)) // decrement counter 446 | if swappedToNil { // decrement the metadata count if the index is set to nil 447 | data.count.Add(^uintptr(0)) 448 | } 449 | return 450 | } 451 | } 452 | } 453 | 454 | // grow to the new size 455 | func (m *Map[K, V]) grow(newSize uintptr) { 456 | for { 457 | currentStore := m.metadata.Load() 458 | if newSize == 0 { 459 | newSize = uintptr(len(currentStore.index)) << 1 460 | } else { 461 | newSize = roundUpPower2(newSize) 462 | } 463 | 464 | index := make([]*element[K, V], newSize) 465 | header := (*reflect.SliceHeader)(unsafe.Pointer(&index)) 466 | 467 | newdata := &metadata[K, V]{ 468 | keyshifts: strconv.IntSize - log2(newSize), 469 | data: unsafe.Pointer(header.Data), 470 | index: index, 471 | } 472 | 473 | m.fillIndexItems(newdata) // re-index with longer and more widespread keys 474 | m.metadata.Store(newdata) 475 | 476 | if !resizeNeeded(newSize, uintptr(m.Len())) { 477 | m.resizing.Store(notResizing) 478 | return 479 | } 480 | newSize = 0 // 0 means double the current size 481 | } 482 | } 483 | 484 | // indexElement returns the index of a hash key, returns `nil` if absent 485 | func (md *metadata[K, V]) indexElement(hashedKey uintptr) *element[K, V] { 486 | index := hashedKey >> md.keyshifts 487 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(md.data) + index*intSizeBytes)) 488 | item := (*element[K, V])(atomic.LoadPointer(ptr)) 489 | for (item == nil || hashedKey < item.keyHash || item.isDeleted()) && index > 0 { 490 | index-- 491 | ptr = (*unsafe.Pointer)(unsafe.Pointer(uintptr(md.data) + index*intSizeBytes)) 492 | item = (*element[K, V])(atomic.LoadPointer(ptr)) 493 | } 494 | return item 495 | } 496 | 497 | // addItemToIndex adds an item to the index if needed and returns the new item counter if it changed, otherwise 0 498 | func (md *metadata[K, V]) addItemToIndex(item *element[K, V]) uintptr { 499 | index := item.keyHash >> md.keyshifts 500 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(md.data) + index*intSizeBytes)) 501 | for { 502 | elem := (*element[K, V])(atomic.LoadPointer(ptr)) 503 | if elem == nil { 504 | if atomic.CompareAndSwapPointer(ptr, nil, unsafe.Pointer(item)) { 505 | return md.count.Add(1) 506 | } 507 | continue 508 | } 509 | if item.keyHash < elem.keyHash { 510 | if !atomic.CompareAndSwapPointer(ptr, unsafe.Pointer(elem), unsafe.Pointer(item)) { 511 | continue 512 | } 513 | } 514 | return 0 515 | } 516 | } 517 | 518 | // check if resize is needed 519 | func resizeNeeded(length, count uintptr) bool { 520 | return (count*100)/length > maxFillRate 521 | } 522 | 523 | // roundUpPower2 rounds a number to the next power of 2 524 | func roundUpPower2(i uintptr) uintptr { 525 | i-- 526 | i |= i >> 1 527 | i |= i >> 2 528 | i |= i >> 4 529 | i |= i >> 8 530 | i |= i >> 16 531 | i |= i >> 32 532 | i++ 533 | return i 534 | } 535 | 536 | // log2 computes the binary logarithm of x, rounded up to the next integer 537 | func log2(i uintptr) (n uintptr) { 538 | for p := uintptr(1); p < i; p, n = p<<1, n+1 { 539 | } 540 | return 541 | } 542 | --------------------------------------------------------------------------------