├── .codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codecov.yaml │ └── go.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── assert └── assert.go ├── benchmarks ├── benchmark_test.go ├── go.mod └── go.sum ├── defines.go ├── example_test.go ├── go.mod ├── go.sum ├── hashmap.go ├── hashmap_test.go ├── list.go ├── list_element.go ├── list_test.go ├── store.go ├── util.go ├── util_hash.go ├── util_hash_test.go └── util_test.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 70% 6 | threshold: 5% 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **System (please complete the following information):** 20 | - OS: 21 | - Version / Commit: 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yaml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | codecov: 11 | timeout-minutes: 15 12 | 13 | name: Coverage 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: "1.22" 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Get dependencies 28 | run: go version && go mod download 29 | 30 | - name: Run tests with coverage 31 | run: make test-coverage 32 | 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v3 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | file: ./.testCoverage 38 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - ready_for_review 13 | 14 | jobs: 15 | build: 16 | if: "!contains(github.event.commits[0].message, '[skip ci]')" 17 | timeout-minutes: 15 18 | 19 | name: Build 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | go: [ "1.22", "1.23", "1.24" ] 24 | 25 | steps: 26 | - name: Set up Go 1.x 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: ${{ matrix.go }} 30 | id: go 31 | 32 | - name: Check out code into the Go module directory 33 | uses: actions/checkout@v3 34 | 35 | - name: Install linters 36 | run: make install-linters 37 | 38 | - name: Get dependencies 39 | run: go version && go mod download 40 | 41 | - name: Run tests 42 | run: make test 43 | 44 | - name: Run linter 45 | run: make lint 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | .idea 3 | .vscode 4 | *.iml 5 | *.local 6 | /*.log 7 | *.out 8 | *.prof 9 | *.test 10 | .DS_Store 11 | *.dmp 12 | *.db 13 | 14 | .bench* 15 | .testCoverage 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | linters: 5 | enable: 6 | - asasalint # check for pass []any as any in variadic func(...any) 7 | - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers 8 | - bidichk # Checks for dangerous unicode character sequences 9 | - containedctx # detects struct contained context.Context field 10 | - contextcheck # check the function whether use a non-inherited context 11 | - cyclop # checks function and package cyclomatic complexity 12 | - decorder # check declaration order and count of types, constants, variables and functions 13 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 14 | - durationcheck # check for two durations multiplied together 15 | - err113 # Golang linter to check the errors handling expressions 16 | - errcheck # checking for unchecked errors 17 | - errname # Checks that errors are prefixed with the `Err` and error types are suffixed with the `Error` 18 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 19 | - funlen # Tool for detection of long functions 20 | - gci # controls golang package import order and makes it always deterministic 21 | - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid 22 | - gocognit # Computes and checks the cognitive complexity of functions 23 | - gocritic # Provides diagnostics that check for bugs, performance and style issues 24 | - gocyclo # Computes and checks the cyclomatic complexity of functions 25 | - godot # Check if comments end in a period 26 | - gofmt # checks whether code was gofmt-ed 27 | - goimports # Check import statements are formatted according to the 'goimport' command 28 | - gosimple # Linter for Go source code that specializes in simplifying a code 29 | - govet # reports suspicious constructs, such as Printf calls with wrong arguments 30 | - grouper # An analyzer to analyze expression groups 31 | - ineffassign # Detects when assignments to existing variables are not used 32 | - maintidx # measures the maintainability index of each function 33 | - makezero # Finds slice declarations with non-zero initial length 34 | - mirror # reports wrong mirror patterns of bytes/strings usage 35 | - misspell # Finds commonly misspelled English words in comments 36 | - nakedret # Finds naked returns in functions 37 | - nestif # Reports deeply nested if statements 38 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil 39 | - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value 40 | - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative 41 | - prealloc # Finds slice declarations that could potentially be preallocated 42 | - predeclared # find code that shadows one of Go's predeclared identifiers 43 | - reassign # Checks that package variables are not reassigned 44 | - revive # drop-in replacement of golint 45 | - staticcheck # drop-in replacement of go vet 46 | - stylecheck # Stylecheck is a replacement for golint 47 | - testifylint # Checks usage of github.com/stretchr/testify 48 | - thelper # checks the consistency of test helpers 49 | - tparallel # detects inappropriate usage of t.Parallel() 50 | - typecheck # parses and type-checks Go code 51 | - unconvert # Remove unnecessary type conversions 52 | - unparam # Reports unused function parameters 53 | - unused # Checks Go code for unused constants, variables, functions and types 54 | - usestdlibvars # detect the possibility to use variables/constants from the Go standard library 55 | - usetesting # Reports uses of functions with replacement inside the testing package 56 | - wastedassign # finds wasted assignment statements 57 | - whitespace # detects leading and trailing whitespace 58 | - wrapcheck # Checks that errors returned from external packages are wrapped 59 | 60 | linters-settings: 61 | cyclop: 62 | max-complexity: 15 63 | gocritic: 64 | disabled-checks: 65 | - newDeref 66 | govet: 67 | disable: 68 | - unsafeptr 69 | staticcheck: 70 | # TODO SA1019 deprecated usage 71 | checks: [ "all", "-SA1019" ] 72 | 73 | issues: 74 | exclude-rules: 75 | - linters: 76 | - err113 77 | text: "do not define dynamic errors" 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright cornelk 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANGCI_VERSION = v1.64.6 2 | 3 | help: ## show help, shown by default if no target is specified 4 | @grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | lint: ## run code linters 7 | golangci-lint run 8 | 9 | benchmark: ## run benchmarks 10 | cd benchmarks && go test -cpu 8 -run=^# -bench=. 11 | 12 | benchmark-perflock: ## run benchmarks using perflock - https://github.com/aclements/perflock 13 | go install golang.org/x/perf/cmd/benchstat@latest 14 | cd benchmarks && perflock -governor 80% go test -count 3 -cpu 8 -run=^# -bench=. | tee .bench.output 15 | cd benchmarks && benchstat .bench.output 16 | 17 | test: ## run tests 18 | go test -race ./... 19 | GOARCH=386 go test ./... 20 | 21 | test-coverage: ## run unit tests and create test coverage 22 | go test ./... -coverprofile .testCoverage -covermode=atomic -coverpkg=./... 23 | 24 | test-coverage-web: test-coverage ## run unit tests and show test coverage in browser 25 | go tool cover -func .testCoverage | grep total | awk '{print "Total coverage: "$$3}' 26 | go tool cover -html=.testCoverage 27 | 28 | install-linters: ## install all used linters 29 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_VERSION} 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hashmap 2 | 3 | [![Build status](https://github.com/cornelk/hashmap/actions/workflows/go.yaml/badge.svg?branch=main)](https://github.com/cornelk/hashmap/actions) 4 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/cornelk/hashmap) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/cornelk/hashmap)](https://goreportcard.com/report/github.com/cornelk/hashmap) 6 | [![codecov](https://codecov.io/gh/cornelk/hashmap/branch/main/graph/badge.svg?token=NS5UY28V3A)](https://codecov.io/gh/cornelk/hashmap) 7 | 8 | ## Overview 9 | 10 | A Golang lock-free thread-safe HashMap optimized for fastest read access. 11 | 12 | It is not a general-use HashMap and currently has slow write performance for write heavy uses. 13 | 14 | The minimal supported Golang version is 1.19 as it makes use of Generics and the new atomic package helpers. 15 | 16 | ## Usage 17 | 18 | Example uint8 key map uses: 19 | 20 | ``` 21 | m := New[uint8, int]() 22 | m.Set(1, 123) 23 | value, ok := m.Get(1) 24 | ``` 25 | 26 | Example string key map uses: 27 | 28 | ``` 29 | m := New[string, int]() 30 | m.Set("amount", 123) 31 | value, ok := m.Get("amount") 32 | ``` 33 | 34 | Using the map to count URL requests: 35 | ``` 36 | m := New[string, *int64]() 37 | var i int64 38 | counter, _ := m.GetOrInsert("api/123", &i) 39 | atomic.AddInt64(counter, 1) // increase counter 40 | ... 41 | count := atomic.LoadInt64(counter) // read counter 42 | ``` 43 | 44 | ## Benchmarks 45 | 46 | Reading from the hash map for numeric key types in a thread-safe way is faster than reading from a standard Golang map 47 | in an unsafe way and four times faster than Golang's `sync.Map`: 48 | 49 | ``` 50 | ReadHashMapUint-8 676ns ± 0% 51 | ReadHaxMapUint-8 689ns ± 1% 52 | ReadGoMapUintUnsafe-8 792ns ± 0% 53 | ReadXsyncMapUint-8 954ns ± 0% 54 | ReadGoSyncMapUint-8 2.62µs ± 1% 55 | ReadSkipMapUint-8 3.27µs ±10% 56 | ReadGoMapUintMutex-8 29.6µs ± 2% 57 | ``` 58 | 59 | Reading from the map while writes are happening: 60 | ``` 61 | ReadHashMapWithWritesUint-8 860ns ± 1% 62 | ReadHaxMapWithWritesUint-8 930ns ± 1% 63 | ReadGoSyncMapWithWritesUint-8 3.06µs ± 2% 64 | ``` 65 | 66 | Write performance without any concurrent reads: 67 | 68 | ``` 69 | WriteGoMapMutexUint-8 14.8µs ± 2% 70 | WriteHashMapUint-8 22.3µs ± 1% 71 | WriteGoSyncMapUint-8 69.3µs ± 0% 72 | ``` 73 | 74 | The benchmarks were run with Golang 1.19.1 on Linux and a Ryzen 9 5900X CPU using `make benchmark-perflock`. 75 | 76 | ## Technical details 77 | 78 | * Technical design decisions have been made based on benchmarks that are stored in an external repository: 79 | [go-benchmark](https://github.com/cornelk/go-benchmark) 80 | 81 | * The library uses a sorted linked list and a slice as an index into that list. 82 | 83 | * The Get() function contains helper functions that have been inlined manually until the Golang compiler will inline them automatically. 84 | 85 | * It optimizes the slice access by circumventing the Golang size check when reading from the slice. 86 | Once a slice is allocated, the size of it does not change. 87 | The library limits the index into the slice, therefore the Golang size check is obsolete. 88 | When the slice reaches a defined fill rate, a bigger slice is allocated and all keys are recalculated and transferred into the new slice. 89 | 90 | * For hashing, specialized xxhash implementations are used that match the size of the key type where available 91 | -------------------------------------------------------------------------------- /assert/assert.go: -------------------------------------------------------------------------------- 1 | // Package assert contains test assertion helpers. 2 | package assert 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func equal(expected, actual any) bool { 11 | if expected == nil || actual == nil { 12 | return expected == actual 13 | } 14 | 15 | if reflect.DeepEqual(expected, actual) { 16 | return true 17 | } 18 | 19 | actualType := reflect.TypeOf(actual) 20 | if actualType == nil { 21 | return false 22 | } 23 | expectedValue := reflect.ValueOf(expected) 24 | if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { 25 | return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) 26 | } 27 | 28 | return false 29 | } 30 | 31 | func fail(t *testing.T, message string, errorMessage ...string) { 32 | t.Helper() 33 | if len(errorMessage) != 0 { 34 | message = fmt.Sprintf("%s\n%s", message, errorMessage) 35 | } 36 | t.Error(message) 37 | t.FailNow() 38 | } 39 | 40 | // Equal asserts that two objects are equal. 41 | func Equal(t *testing.T, expected, actual any, errorMessage ...string) { 42 | t.Helper() 43 | if equal(expected, actual) { 44 | return 45 | } 46 | 47 | msg := fmt.Sprintf("Not equal: \nexpected: %v\nactual : %v", expected, actual) 48 | fail(t, msg, errorMessage...) 49 | } 50 | 51 | // True asserts that the specified value is true. 52 | func True(t *testing.T, value bool, errorMessage ...string) { 53 | t.Helper() 54 | if value { 55 | return 56 | } 57 | fail(t, "Unexpected false", errorMessage...) 58 | } 59 | 60 | // False asserts that the specified value is false. 61 | func False(t *testing.T, value bool, errorMessage ...string) { 62 | t.Helper() 63 | if !value { 64 | return 65 | } 66 | fail(t, "Unexpected true", errorMessage...) 67 | } 68 | -------------------------------------------------------------------------------- /benchmarks/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | 9 | "github.com/alphadose/haxmap" 10 | "github.com/cornelk/hashmap" 11 | "github.com/puzpuzpuz/xsync/v2" 12 | "github.com/zhangyunhao116/skipmap" 13 | ) 14 | 15 | const benchmarkItemCount = 1024 16 | 17 | func setupHashMap(b *testing.B) *hashmap.Map[uintptr, uintptr] { 18 | b.Helper() 19 | 20 | m := hashmap.New[uintptr, uintptr]() 21 | for i := uintptr(0); i < benchmarkItemCount; i++ { 22 | m.Set(i, i) 23 | } 24 | return m 25 | } 26 | 27 | func setupHaxMap(b *testing.B) *haxmap.Map[uintptr, uintptr] { 28 | b.Helper() 29 | 30 | m := haxmap.New[uintptr, uintptr]() 31 | for i := uintptr(0); i < benchmarkItemCount; i++ { 32 | m.Set(i, i) 33 | } 34 | return m 35 | } 36 | 37 | func setupSkipMap(b *testing.B) *skipmap.Uint64Map[uint64] { 38 | b.Helper() 39 | 40 | m := skipmap.NewUint64[uint64]() 41 | for i := uint64(0); i < benchmarkItemCount; i++ { 42 | m.Store(i, i) 43 | } 44 | return m 45 | } 46 | 47 | func setupXsync(b *testing.B) *xsync.MapOf[uint64, uint64] { 48 | b.Helper() 49 | 50 | m := xsync.NewIntegerMapOf[uint64, uint64]() 51 | for i := uint64(0); i < benchmarkItemCount; i++ { 52 | m.Store(i, i) 53 | } 54 | return m 55 | } 56 | 57 | func setupHashMapString(b *testing.B) (*hashmap.Map[string, string], []string) { 58 | b.Helper() 59 | 60 | m := hashmap.New[string, string]() 61 | keys := make([]string, benchmarkItemCount) 62 | for i := 0; i < benchmarkItemCount; i++ { 63 | s := strconv.Itoa(i) 64 | m.Set(s, s) 65 | keys[i] = s 66 | } 67 | 68 | return m, keys 69 | } 70 | 71 | func setupGoMap(b *testing.B) map[uintptr]uintptr { 72 | b.Helper() 73 | 74 | m := make(map[uintptr]uintptr) 75 | for i := uintptr(0); i < benchmarkItemCount; i++ { 76 | m[i] = i 77 | } 78 | 79 | return m 80 | } 81 | 82 | func setupGoSyncMap(b *testing.B) *sync.Map { 83 | b.Helper() 84 | 85 | m := &sync.Map{} 86 | for i := uintptr(0); i < benchmarkItemCount; i++ { 87 | m.Store(i, i) 88 | } 89 | 90 | return m 91 | } 92 | 93 | func setupGoMapString(b *testing.B) (map[string]string, []string) { 94 | b.Helper() 95 | 96 | m := make(map[string]string) 97 | keys := make([]string, benchmarkItemCount) 98 | for i := 0; i < benchmarkItemCount; i++ { 99 | s := strconv.Itoa(i) 100 | m[s] = s 101 | keys[i] = s 102 | } 103 | return m, keys 104 | } 105 | 106 | func BenchmarkReadHashMapUint(b *testing.B) { 107 | m := setupHashMap(b) 108 | b.ResetTimer() 109 | 110 | b.RunParallel(func(pb *testing.PB) { 111 | for pb.Next() { 112 | for i := uintptr(0); i < benchmarkItemCount; i++ { 113 | j, _ := m.Get(i) 114 | if j != i { 115 | b.Fail() 116 | } 117 | } 118 | } 119 | }) 120 | } 121 | 122 | func BenchmarkReadHashMapWithWritesUint(b *testing.B) { 123 | m := setupHashMap(b) 124 | var writer uintptr 125 | b.ResetTimer() 126 | 127 | b.RunParallel(func(pb *testing.PB) { 128 | // use 1 thread as writer 129 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 130 | for pb.Next() { 131 | for i := uintptr(0); i < benchmarkItemCount; i++ { 132 | m.Set(i, i) 133 | } 134 | } 135 | } else { 136 | for pb.Next() { 137 | for i := uintptr(0); i < benchmarkItemCount; i++ { 138 | j, _ := m.Get(i) 139 | if j != i { 140 | b.Fail() 141 | } 142 | } 143 | } 144 | } 145 | }) 146 | } 147 | 148 | func BenchmarkReadHashMapString(b *testing.B) { 149 | m, keys := setupHashMapString(b) 150 | b.ResetTimer() 151 | 152 | b.RunParallel(func(pb *testing.PB) { 153 | for pb.Next() { 154 | for i := 0; i < benchmarkItemCount; i++ { 155 | s := keys[i] 156 | sVal, _ := m.Get(s) 157 | if sVal != s { 158 | b.Fail() 159 | } 160 | } 161 | } 162 | }) 163 | } 164 | 165 | func BenchmarkReadHaxMapUint(b *testing.B) { 166 | m := setupHaxMap(b) 167 | b.ResetTimer() 168 | 169 | b.RunParallel(func(pb *testing.PB) { 170 | for pb.Next() { 171 | for i := uintptr(0); i < benchmarkItemCount; i++ { 172 | j, _ := m.Get(i) 173 | if j != i { 174 | b.Fail() 175 | } 176 | } 177 | } 178 | }) 179 | } 180 | 181 | func BenchmarkReadHaxMapWithWritesUint(b *testing.B) { 182 | m := setupHaxMap(b) 183 | var writer uintptr 184 | b.ResetTimer() 185 | 186 | b.RunParallel(func(pb *testing.PB) { 187 | // use 1 thread as writer 188 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 189 | for pb.Next() { 190 | for i := uintptr(0); i < benchmarkItemCount; i++ { 191 | m.Set(i, i) 192 | } 193 | } 194 | } else { 195 | for pb.Next() { 196 | for i := uintptr(0); i < benchmarkItemCount; i++ { 197 | j, _ := m.Get(i) 198 | if j != i { 199 | b.Fail() 200 | } 201 | } 202 | } 203 | } 204 | }) 205 | } 206 | 207 | func BenchmarkReadXsyncMapUint(b *testing.B) { 208 | m := setupXsync(b) 209 | b.ResetTimer() 210 | 211 | b.RunParallel(func(pb *testing.PB) { 212 | for pb.Next() { 213 | for i := uint64(0); i < benchmarkItemCount; i++ { 214 | j, _ := m.Load(i) 215 | if j != i { 216 | b.Fail() 217 | } 218 | } 219 | } 220 | }) 221 | } 222 | 223 | func BenchmarkReadXsyncMapWithWritesUint(b *testing.B) { 224 | m := setupXsync(b) 225 | var writer uintptr 226 | b.ResetTimer() 227 | 228 | b.RunParallel(func(pb *testing.PB) { 229 | // use 1 thread as writer 230 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 231 | for pb.Next() { 232 | for i := uint64(0); i < benchmarkItemCount; i++ { 233 | m.Store(i, i) 234 | } 235 | } 236 | } else { 237 | for pb.Next() { 238 | for i := uint64(0); i < benchmarkItemCount; i++ { 239 | j, _ := m.Load(i) 240 | if j != i { 241 | b.Fail() 242 | } 243 | } 244 | } 245 | } 246 | }) 247 | } 248 | 249 | func BenchmarkReadSkipMapUint(b *testing.B) { 250 | m := setupSkipMap(b) 251 | b.ResetTimer() 252 | 253 | b.RunParallel(func(pb *testing.PB) { 254 | for pb.Next() { 255 | for i := uint64(0); i < benchmarkItemCount; i++ { 256 | j, _ := m.Load(i) 257 | if j != i { 258 | b.Fail() 259 | } 260 | } 261 | } 262 | }) 263 | } 264 | 265 | func BenchmarkReadGoMapUintUnsafe(b *testing.B) { 266 | m := setupGoMap(b) 267 | b.ResetTimer() 268 | b.RunParallel(func(pb *testing.PB) { 269 | for pb.Next() { 270 | for i := uintptr(0); i < benchmarkItemCount; i++ { 271 | j := m[i] 272 | if j != i { 273 | b.Fail() 274 | } 275 | } 276 | } 277 | }) 278 | } 279 | 280 | func BenchmarkReadGoMapUintMutex(b *testing.B) { 281 | m := setupGoMap(b) 282 | l := &sync.RWMutex{} 283 | b.ResetTimer() 284 | b.RunParallel(func(pb *testing.PB) { 285 | for pb.Next() { 286 | for i := uintptr(0); i < benchmarkItemCount; i++ { 287 | l.RLock() 288 | j := m[i] 289 | l.RUnlock() 290 | if j != i { 291 | b.Fail() 292 | } 293 | } 294 | } 295 | }) 296 | } 297 | 298 | func BenchmarkReadGoMapWithWritesUintMutex(b *testing.B) { 299 | m := setupGoMap(b) 300 | l := &sync.RWMutex{} 301 | var writer uintptr 302 | b.ResetTimer() 303 | 304 | b.RunParallel(func(pb *testing.PB) { 305 | // use 1 thread as writer 306 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 307 | for pb.Next() { 308 | for i := uintptr(0); i < benchmarkItemCount; i++ { 309 | l.Lock() 310 | m[i] = i 311 | l.Unlock() 312 | } 313 | } 314 | } else { 315 | for pb.Next() { 316 | for i := uintptr(0); i < benchmarkItemCount; i++ { 317 | l.RLock() 318 | j := m[i] 319 | l.RUnlock() 320 | if j != i { 321 | b.Fail() 322 | } 323 | } 324 | } 325 | } 326 | }) 327 | } 328 | 329 | func BenchmarkReadGoSyncMapUint(b *testing.B) { 330 | m := setupGoSyncMap(b) 331 | b.ResetTimer() 332 | b.RunParallel(func(pb *testing.PB) { 333 | for pb.Next() { 334 | for i := uintptr(0); i < benchmarkItemCount; i++ { 335 | j, _ := m.Load(i) 336 | if j != i { 337 | b.Fail() 338 | } 339 | } 340 | } 341 | }) 342 | } 343 | 344 | func BenchmarkReadGoSyncMapWithWritesUint(b *testing.B) { 345 | m := setupGoSyncMap(b) 346 | var writer uintptr 347 | b.ResetTimer() 348 | 349 | b.RunParallel(func(pb *testing.PB) { 350 | // use 1 thread as writer 351 | if atomic.CompareAndSwapUintptr(&writer, 0, 1) { 352 | for pb.Next() { 353 | for i := uintptr(0); i < benchmarkItemCount; i++ { 354 | m.Store(i, i) 355 | } 356 | } 357 | } else { 358 | for pb.Next() { 359 | for i := uintptr(0); i < benchmarkItemCount; i++ { 360 | j, _ := m.Load(i) 361 | if j != i { 362 | b.Fail() 363 | } 364 | } 365 | } 366 | } 367 | }) 368 | } 369 | 370 | func BenchmarkReadGoMapStringUnsafe(b *testing.B) { 371 | m, keys := setupGoMapString(b) 372 | b.ResetTimer() 373 | b.RunParallel(func(pb *testing.PB) { 374 | for pb.Next() { 375 | for i := 0; i < benchmarkItemCount; i++ { 376 | s := keys[i] 377 | sVal := m[s] 378 | if s != sVal { 379 | b.Fail() 380 | } 381 | } 382 | } 383 | }) 384 | } 385 | 386 | func BenchmarkReadGoMapStringMutex(b *testing.B) { 387 | m, keys := setupGoMapString(b) 388 | l := &sync.RWMutex{} 389 | b.ResetTimer() 390 | b.RunParallel(func(pb *testing.PB) { 391 | for pb.Next() { 392 | for i := 0; i < benchmarkItemCount; i++ { 393 | s := keys[i] 394 | l.RLock() 395 | sVal := m[s] 396 | l.RUnlock() 397 | if s != sVal { 398 | b.Fail() 399 | } 400 | } 401 | } 402 | }) 403 | } 404 | 405 | func BenchmarkWriteHashMapUint(b *testing.B) { 406 | m := hashmap.New[uintptr, uintptr]() 407 | b.ResetTimer() 408 | 409 | for n := 0; n < b.N; n++ { 410 | for i := uintptr(0); i < benchmarkItemCount; i++ { 411 | m.Set(i, i) 412 | } 413 | } 414 | } 415 | 416 | func BenchmarkWriteGoMapMutexUint(b *testing.B) { 417 | m := make(map[uintptr]uintptr) 418 | l := &sync.RWMutex{} 419 | b.ResetTimer() 420 | 421 | for n := 0; n < b.N; n++ { 422 | for i := uintptr(0); i < benchmarkItemCount; i++ { 423 | l.Lock() 424 | m[i] = i 425 | l.Unlock() 426 | } 427 | } 428 | } 429 | 430 | func BenchmarkWriteGoSyncMapUint(b *testing.B) { 431 | m := &sync.Map{} 432 | b.ResetTimer() 433 | 434 | for n := 0; n < b.N; n++ { 435 | for i := uintptr(0); i < benchmarkItemCount; i++ { 436 | m.Store(i, i) 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cornelk/hashmap/benchmarks 2 | 3 | go 1.19 4 | 5 | replace github.com/cornelk/hashmap => ../ 6 | 7 | require ( 8 | github.com/alphadose/haxmap v1.1.0 9 | github.com/cornelk/hashmap v1.0.8 10 | github.com/puzpuzpuz/xsync/v2 v2.3.1 11 | github.com/zhangyunhao116/skipmap v0.10.1 12 | ) 13 | 14 | require github.com/zhangyunhao116/fastrand v0.3.0 // indirect 15 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alphadose/haxmap v1.1.0 h1:M7Dxdr+civMQkWCgDpoktNpLofDBz7XzdS3rF3Y6r4U= 2 | github.com/alphadose/haxmap v1.1.0/go.mod h1:Pq2IXbl9/ytYHfrIAd7rIVtZQ2ezdIhZfvdqOizDeWY= 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 | github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= 6 | github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= 7 | github.com/zhangyunhao116/skipmap v0.10.1 h1:CMH4yGZQESBM1kUNozQqQ+Ra2pKqwF3HxaTADOaIfPs= 8 | github.com/zhangyunhao116/skipmap v0.10.1/go.mod h1:CClnLPHl3DI+hHgrcy0OZ/QJ45AWgA3ObVcQyJop12c= 9 | -------------------------------------------------------------------------------- /defines.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | // defaultSize is the default size for a map. 4 | const defaultSize = 8 5 | 6 | // maxFillRate is the maximum fill rate for the slice before a resize will happen. 7 | const maxFillRate = 50 8 | 9 | // support all numeric and string types and aliases of those. 10 | type hashable interface { 11 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string 12 | } 13 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/cornelk/hashmap/assert" 9 | ) 10 | 11 | // TestAPICounter shows how to use the hashmap to count REST server API calls. 12 | func TestAPICounter(t *testing.T) { 13 | t.Parallel() 14 | m := New[string, *int64]() 15 | 16 | for i := 0; i < 100; i++ { 17 | s := fmt.Sprintf("/api%d/", i%4) 18 | 19 | counter := int64(0) 20 | actual, _ := m.GetOrInsert(s, &counter) 21 | atomic.AddInt64(actual, 1) 22 | } 23 | 24 | s := fmt.Sprintf("/api%d/", 0) 25 | value, ok := m.Get(s) 26 | assert.True(t, ok) 27 | assert.Equal(t, 25, *value) 28 | } 29 | 30 | func TestExample(t *testing.T) { 31 | m := New[string, int]() 32 | m.Set("amount", 123) 33 | value, ok := m.Get("amount") 34 | assert.True(t, ok) 35 | assert.Equal(t, 123, value) 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cornelk/hashmap 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornelk/hashmap/59e00f4630994730a90b01fe37eab6dba7a86109/go.sum -------------------------------------------------------------------------------- /hashmap.go: -------------------------------------------------------------------------------- 1 | // Package hashmap provides a lock-free and thread-safe HashMap. 2 | package hashmap 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "sync/atomic" 10 | "unsafe" 11 | ) 12 | 13 | // Map implements a read optimized hash map. 14 | type Map[Key hashable, Value any] struct { 15 | hasher func(Key) uintptr 16 | store atomic.Pointer[store[Key, Value]] // pointer to a map instance that gets replaced if the map resizes 17 | linkedList *List[Key, Value] // key sorted linked list of elements 18 | // resizing marks a resizing operation in progress. 19 | // this is using uintptr instead of atomic.Bool to avoid using 32 bit int on 64 bit systems 20 | resizing atomic.Uintptr 21 | } 22 | 23 | // New returns a new map instance. 24 | func New[Key hashable, Value any]() *Map[Key, Value] { 25 | return NewSized[Key, Value](defaultSize) 26 | } 27 | 28 | // NewSized returns a new map instance with a specific initialization size. 29 | func NewSized[Key hashable, Value any](size uintptr) *Map[Key, Value] { 30 | m := &Map[Key, Value]{} 31 | m.allocate(size) 32 | m.setDefaultHasher() 33 | return m 34 | } 35 | 36 | // SetHasher sets a custom hasher. 37 | func (m *Map[Key, Value]) SetHasher(hasher func(Key) uintptr) { 38 | m.hasher = hasher 39 | } 40 | 41 | // Len returns the number of elements within the map. 42 | func (m *Map[Key, Value]) Len() int { 43 | return m.linkedList.Len() 44 | } 45 | 46 | // Get retrieves an element from the map under given hash key. 47 | func (m *Map[Key, Value]) Get(key Key) (Value, bool) { 48 | hash := m.hasher(key) 49 | 50 | for element := m.store.Load().item(hash); element != nil; element = element.Next() { 51 | if element.keyHash == hash && element.key == key { 52 | return element.Value(), true 53 | } 54 | 55 | if element.keyHash > hash { 56 | return *new(Value), false 57 | } 58 | } 59 | return *new(Value), false 60 | } 61 | 62 | // GetOrInsert returns the existing value for the key if present. 63 | // Otherwise, it stores and returns the given value. 64 | // The returned bool is true if the key existed, false if inserted. 65 | func (m *Map[Key, Value]) GetOrInsert(key Key, value Value) (Value, bool) { 66 | hash := m.hasher(key) 67 | var ( 68 | existed, inserted bool 69 | element *ListElement[Key, Value] 70 | ) 71 | 72 | for { 73 | store := m.store.Load() 74 | searchStart := store.item(hash) 75 | 76 | if !inserted { // if retrying after insert during grow, do not add to list again 77 | element, existed, inserted = m.linkedList.Add(searchStart, hash, key, value) 78 | if existed { 79 | return element.Value(), true 80 | } 81 | if !inserted { 82 | continue // a concurrent add did interfere, try again 83 | } 84 | } 85 | 86 | count := store.addItem(element) 87 | currentStore := m.store.Load() 88 | if store != currentStore { // retry insert in case of insert during grow 89 | continue 90 | } 91 | 92 | if m.isResizeNeeded(store, count) && m.resizing.CompareAndSwap(0, 1) { 93 | go m.grow(0, true) 94 | } 95 | return value, false 96 | } 97 | } 98 | 99 | // FillRate returns the fill rate of the map as a percentage integer. 100 | func (m *Map[Key, Value]) FillRate() int { 101 | store := m.store.Load() 102 | count := int(store.count.Load()) 103 | l := len(store.index) 104 | return (count * 100) / l 105 | } 106 | 107 | // Del deletes the key from the map and returns whether the key was deleted. 108 | func (m *Map[Key, Value]) Del(key Key) bool { 109 | hash := m.hasher(key) 110 | store := m.store.Load() 111 | element := store.item(hash) 112 | 113 | for ; element != nil; element = element.Next() { 114 | if element.keyHash == hash && element.key == key { 115 | m.deleteElement(element) 116 | m.linkedList.Delete(element) 117 | return true 118 | } 119 | 120 | if element.keyHash > hash { 121 | return false 122 | } 123 | } 124 | return false 125 | } 126 | 127 | // Insert sets the value under the specified key to the map if it does not exist yet. 128 | // If a resizing operation is happening concurrently while calling Insert, the item might show up in the map 129 | // after the resize operation is finished. 130 | // Returns true if the item was inserted or false if it existed. 131 | func (m *Map[Key, Value]) Insert(key Key, value Value) bool { 132 | hash := m.hasher(key) 133 | var ( 134 | existed, inserted bool 135 | element *ListElement[Key, Value] 136 | ) 137 | 138 | for { 139 | store := m.store.Load() 140 | searchStart := store.item(hash) 141 | 142 | if !inserted { // if retrying after insert during grow, do not add to list again 143 | element, existed, inserted = m.linkedList.Add(searchStart, hash, key, value) 144 | if existed { 145 | return false 146 | } 147 | if !inserted { 148 | continue // a concurrent add did interfere, try again 149 | } 150 | } 151 | 152 | count := store.addItem(element) 153 | currentStore := m.store.Load() 154 | if store != currentStore { // retry insert in case of insert during grow 155 | continue 156 | } 157 | 158 | if m.isResizeNeeded(store, count) && m.resizing.CompareAndSwap(0, 1) { 159 | go m.grow(0, true) 160 | } 161 | return true 162 | } 163 | } 164 | 165 | // Set sets the value under the specified key to the map. An existing item for this key will be overwritten. 166 | // If a resizing operation is happening concurrently while calling Set, the item might show up in the map 167 | // after the resize operation is finished. 168 | func (m *Map[Key, Value]) Set(key Key, value Value) { 169 | hash := m.hasher(key) 170 | 171 | for { 172 | store := m.store.Load() 173 | searchStart := store.item(hash) 174 | 175 | element, added := m.linkedList.AddOrUpdate(searchStart, hash, key, value) 176 | if !added { 177 | continue // a concurrent add did interfere, try again 178 | } 179 | 180 | count := store.addItem(element) 181 | currentStore := m.store.Load() 182 | if store != currentStore { // retry insert in case of insert during grow 183 | continue 184 | } 185 | 186 | if m.isResizeNeeded(store, count) && m.resizing.CompareAndSwap(0, 1) { 187 | go m.grow(0, true) 188 | } 189 | return 190 | } 191 | } 192 | 193 | // Grow resizes the map to a new size, the size gets rounded up to next power of 2. 194 | // To double the size of the map use newSize 0. 195 | // This function returns immediately, the resize operation is done in a goroutine. 196 | // No resizing is done in case of another resize operation already being in progress. 197 | func (m *Map[Key, Value]) Grow(newSize uintptr) { 198 | if m.resizing.CompareAndSwap(0, 1) { 199 | go m.grow(newSize, true) 200 | } 201 | } 202 | 203 | // String returns the map as a string, only hashed keys are printed. 204 | func (m *Map[Key, Value]) String() string { 205 | buffer := bytes.NewBufferString("") 206 | buffer.WriteRune('[') 207 | 208 | first := m.linkedList.First() 209 | item := first 210 | 211 | for item != nil { 212 | if item != first { 213 | buffer.WriteRune(',') 214 | } 215 | fmt.Fprint(buffer, item.keyHash) 216 | item = item.Next() 217 | } 218 | buffer.WriteRune(']') 219 | return buffer.String() 220 | } 221 | 222 | // Range calls f sequentially for each key and value present in the map. 223 | // If f returns false, range stops the iteration. 224 | func (m *Map[Key, Value]) Range(f func(Key, Value) bool) { 225 | item := m.linkedList.First() 226 | 227 | for item != nil { 228 | value := item.Value() 229 | if !f(item.key, value) { 230 | return 231 | } 232 | item = item.Next() 233 | } 234 | } 235 | 236 | func (m *Map[Key, Value]) allocate(newSize uintptr) { 237 | m.linkedList = NewList[Key, Value]() 238 | if m.resizing.CompareAndSwap(0, 1) { 239 | m.grow(newSize, false) 240 | } 241 | } 242 | 243 | func (m *Map[Key, Value]) isResizeNeeded(store *store[Key, Value], count uintptr) bool { 244 | l := uintptr(len(store.index)) // l can't be 0 as it gets initialized in New() 245 | fillRate := (count * 100) / l 246 | return fillRate > maxFillRate 247 | } 248 | 249 | // deleteElement deletes an element from index. 250 | func (m *Map[Key, Value]) deleteElement(element *ListElement[Key, Value]) { 251 | for { 252 | store := m.store.Load() 253 | index := element.keyHash >> store.keyShifts 254 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(store.array) + index*intSizeBytes)) 255 | 256 | next := element.Next() 257 | if next != nil && element.keyHash>>store.keyShifts != index { 258 | next = nil // do not set index to next item if it's not the same slice index 259 | } 260 | atomic.CompareAndSwapPointer(ptr, unsafe.Pointer(element), unsafe.Pointer(next)) 261 | 262 | currentStore := m.store.Load() 263 | if store == currentStore { // check that no resize happened 264 | break 265 | } 266 | } 267 | } 268 | 269 | func (m *Map[Key, Value]) grow(newSize uintptr, loop bool) { 270 | defer m.resizing.CompareAndSwap(1, 0) 271 | 272 | for { 273 | currentStore := m.store.Load() 274 | if newSize == 0 { 275 | newSize = uintptr(len(currentStore.index)) << 1 276 | } else { 277 | newSize = roundUpPower2(newSize) 278 | } 279 | 280 | index := make([]*ListElement[Key, Value], newSize) 281 | header := (*reflect.SliceHeader)(unsafe.Pointer(&index)) 282 | 283 | newStore := &store[Key, Value]{ 284 | keyShifts: strconv.IntSize - log2(newSize), 285 | array: unsafe.Pointer(header.Data), // use address of slice data storage 286 | index: index, 287 | } 288 | 289 | m.fillIndexItems(newStore) // initialize new index slice with longer keys 290 | 291 | m.store.Store(newStore) 292 | 293 | m.fillIndexItems(newStore) // make sure that the new index is up-to-date with the current state of the linked list 294 | 295 | if !loop { 296 | return 297 | } 298 | 299 | // check if a new resize needs to be done already 300 | count := uintptr(m.Len()) 301 | if !m.isResizeNeeded(newStore, count) { 302 | return 303 | } 304 | newSize = 0 // 0 means double the current size 305 | } 306 | } 307 | 308 | func (m *Map[Key, Value]) fillIndexItems(store *store[Key, Value]) { 309 | first := m.linkedList.First() 310 | item := first 311 | lastIndex := uintptr(0) 312 | 313 | for item != nil { 314 | index := item.keyHash >> store.keyShifts 315 | if item == first || index != lastIndex { // store item with smallest hash key for every index 316 | store.addItem(item) 317 | lastIndex = index 318 | } 319 | item = item.Next() 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /hashmap_test.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "strconv" 9 | "sync" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | 14 | "github.com/cornelk/hashmap/assert" 15 | ) 16 | 17 | func TestNew(t *testing.T) { 18 | t.Parallel() 19 | m := New[uintptr, uintptr]() 20 | assert.Equal(t, 0, m.Len()) 21 | } 22 | 23 | func TestSetString(t *testing.T) { 24 | t.Parallel() 25 | m := New[int, string]() 26 | elephant := "elephant" 27 | monkey := "monkey" 28 | 29 | m.Set(1, elephant) // insert 30 | value, ok := m.Get(1) 31 | assert.True(t, ok) 32 | assert.Equal(t, elephant, value) 33 | 34 | m.Set(1, monkey) // overwrite 35 | value, ok = m.Get(1) 36 | assert.True(t, ok) 37 | assert.Equal(t, monkey, value) 38 | 39 | assert.Equal(t, 1, m.Len()) 40 | 41 | m.Set(2, elephant) // insert 42 | assert.Equal(t, 2, m.Len()) 43 | value, ok = m.Get(2) 44 | assert.True(t, ok) 45 | assert.Equal(t, elephant, value) 46 | } 47 | 48 | func TestSetUint8(t *testing.T) { 49 | t.Parallel() 50 | m := New[uint8, int]() 51 | 52 | m.Set(1, 128) // insert 53 | value, ok := m.Get(1) 54 | assert.True(t, ok) 55 | assert.Equal(t, 128, value) 56 | 57 | m.Set(2, 200) // insert 58 | assert.Equal(t, 2, m.Len()) 59 | value, ok = m.Get(2) 60 | assert.True(t, ok) 61 | assert.Equal(t, 200, value) 62 | } 63 | 64 | func TestSetInt16(t *testing.T) { 65 | t.Parallel() 66 | m := New[int16, int]() 67 | 68 | m.Set(1, 128) // insert 69 | value, ok := m.Get(1) 70 | assert.True(t, ok) 71 | assert.Equal(t, 128, value) 72 | 73 | m.Set(2, 200) // insert 74 | assert.Equal(t, 2, m.Len()) 75 | value, ok = m.Get(2) 76 | assert.True(t, ok) 77 | assert.Equal(t, 200, value) 78 | } 79 | 80 | func TestSetFloat32(t *testing.T) { 81 | t.Parallel() 82 | m := New[float32, int]() 83 | 84 | m.Set(1.1, 128) // insert 85 | value, ok := m.Get(1.1) 86 | assert.True(t, ok) 87 | assert.Equal(t, 128, value) 88 | 89 | m.Set(2.2, 200) // insert 90 | assert.Equal(t, 2, m.Len()) 91 | value, ok = m.Get(2.2) 92 | assert.True(t, ok) 93 | assert.Equal(t, 200, value) 94 | } 95 | 96 | func TestSetFloat64(t *testing.T) { 97 | t.Parallel() 98 | m := New[float64, int]() 99 | 100 | m.Set(1.1, 128) // insert 101 | value, ok := m.Get(1.1) 102 | assert.True(t, ok) 103 | assert.Equal(t, 128, value) 104 | 105 | m.Set(2.2, 200) // insert 106 | assert.Equal(t, 2, m.Len()) 107 | value, ok = m.Get(2.2) 108 | assert.True(t, ok) 109 | assert.Equal(t, 200, value) 110 | } 111 | 112 | func TestSetInt64(t *testing.T) { 113 | t.Parallel() 114 | m := New[int64, int]() 115 | 116 | m.Set(1, 128) // insert 117 | value, ok := m.Get(1) 118 | assert.True(t, ok) 119 | assert.Equal(t, 128, value) 120 | 121 | m.Set(2, 200) // insert 122 | assert.Equal(t, 2, m.Len()) 123 | value, ok = m.Get(2) 124 | assert.True(t, ok) 125 | assert.Equal(t, 200, value) 126 | } 127 | 128 | func TestInsert(t *testing.T) { 129 | t.Parallel() 130 | m := New[int, string]() 131 | elephant := "elephant" 132 | monkey := "monkey" 133 | 134 | inserted := m.Insert(1, elephant) 135 | assert.True(t, inserted) 136 | value, ok := m.Get(1) 137 | assert.True(t, ok) 138 | assert.Equal(t, elephant, value) 139 | 140 | inserted = m.Insert(1, monkey) 141 | assert.False(t, inserted) 142 | value, ok = m.Get(1) 143 | assert.True(t, ok) 144 | assert.Equal(t, elephant, value) 145 | 146 | assert.Equal(t, 1, m.Len()) 147 | 148 | inserted = m.Insert(2, monkey) 149 | assert.True(t, inserted) 150 | assert.Equal(t, 2, m.Len()) 151 | value, ok = m.Get(2) 152 | assert.True(t, ok) 153 | assert.Equal(t, monkey, value) 154 | } 155 | 156 | func TestGetNonExistingItem(t *testing.T) { 157 | t.Parallel() 158 | m := New[int, string]() 159 | value, ok := m.Get(1) 160 | assert.False(t, ok) 161 | assert.Equal(t, "", value) 162 | } 163 | 164 | func TestGrow(t *testing.T) { 165 | t.Parallel() 166 | m := New[int, string]() 167 | m.Grow(uintptr(63)) 168 | 169 | for { // make sure to wait for resize operation to finish 170 | if m.resizing.Load() == 0 { 171 | break 172 | } 173 | time.Sleep(time.Microsecond * 50) 174 | } 175 | 176 | store := m.store.Load() 177 | log := int(math.Log2(64)) 178 | expectedSize := uintptr(strconv.IntSize - log) 179 | assert.Equal(t, expectedSize, store.keyShifts) 180 | } 181 | 182 | func TestResize(t *testing.T) { 183 | t.Parallel() 184 | m := NewSized[uintptr, string](2) 185 | itemCount := uintptr(50) 186 | 187 | for i := uintptr(0); i < itemCount; i++ { 188 | m.Set(i, strconv.Itoa(int(i))) 189 | } 190 | 191 | assert.Equal(t, itemCount, m.Len()) 192 | 193 | for { // make sure to wait for resize operation to finish 194 | if m.resizing.Load() == 0 { 195 | break 196 | } 197 | time.Sleep(time.Microsecond * 50) 198 | } 199 | 200 | assert.True(t, m.FillRate() > 30) 201 | 202 | for i := uintptr(0); i < itemCount; i++ { 203 | value, ok := m.Get(i) 204 | assert.True(t, ok) 205 | expected := strconv.Itoa(int(i)) 206 | assert.Equal(t, expected, value) 207 | } 208 | } 209 | 210 | func TestStringer(t *testing.T) { 211 | t.Parallel() 212 | m := New[int, string]() 213 | elephant := "elephant" 214 | monkey := "monkey" 215 | 216 | assert.Equal(t, "[]", m.String()) 217 | 218 | m.Set(1, elephant) 219 | hashedKey0 := m.hasher(1) 220 | expected := fmt.Sprintf("[%v]", hashedKey0) 221 | assert.Equal(t, expected, m.String()) 222 | 223 | m.Set(2, monkey) 224 | hashedKey1 := m.hasher(2) 225 | if hashedKey0 < hashedKey1 { 226 | expected = fmt.Sprintf("[%v,%v]", hashedKey0, hashedKey1) 227 | } else { 228 | expected = fmt.Sprintf("[%v,%v]", hashedKey1, hashedKey0) 229 | } 230 | assert.Equal(t, expected, m.String()) 231 | } 232 | 233 | func TestDelete(t *testing.T) { 234 | t.Parallel() 235 | m := New[int, string]() 236 | elephant := "elephant" 237 | monkey := "monkey" 238 | 239 | deleted := m.Del(1) 240 | assert.False(t, deleted) 241 | 242 | m.Set(1, elephant) 243 | m.Set(2, monkey) 244 | 245 | deleted = m.Del(0) 246 | assert.False(t, deleted) 247 | deleted = m.Del(3) 248 | assert.False(t, deleted) 249 | assert.Equal(t, 2, m.Len()) 250 | 251 | deleted = m.Del(1) 252 | assert.True(t, deleted) 253 | deleted = m.Del(1) 254 | assert.False(t, deleted) 255 | deleted = m.Del(2) 256 | assert.True(t, deleted) 257 | assert.Equal(t, 0, m.Len()) 258 | } 259 | 260 | func TestRange(t *testing.T) { 261 | t.Parallel() 262 | m := New[int, string]() 263 | 264 | items := map[int]string{} 265 | m.Range(func(key int, value string) bool { 266 | items[key] = value 267 | return true 268 | }) 269 | assert.Equal(t, 0, len(items)) 270 | 271 | itemCount := 16 272 | for i := itemCount; i > 0; i-- { 273 | m.Set(i, strconv.Itoa(i)) 274 | } 275 | 276 | items = map[int]string{} 277 | m.Range(func(key int, value string) bool { 278 | items[key] = value 279 | return true 280 | }) 281 | 282 | assert.Equal(t, itemCount, len(items)) 283 | for i := 1; i <= itemCount; i++ { 284 | value, ok := items[i] 285 | assert.True(t, ok) 286 | expected := strconv.Itoa(i) 287 | assert.Equal(t, expected, value) 288 | } 289 | 290 | items = map[int]string{} // test aborting range 291 | m.Range(func(key int, value string) bool { 292 | items[key] = value 293 | return false 294 | }) 295 | assert.Equal(t, 1, len(items)) 296 | } 297 | 298 | // nolint: funlen, gocognit 299 | func TestHashMap_parallel(t *testing.T) { 300 | m := New[int, int]() 301 | 302 | maxVal := 10 303 | dur := 2 * time.Second 304 | 305 | do := func(t *testing.T, maxVal int, d time.Duration, fn func(*testing.T, int)) <-chan error { 306 | t.Helper() 307 | done := make(chan error) 308 | var times int64 309 | // This goroutines will terminate test in case if closure hangs. 310 | go func() { 311 | for { 312 | select { 313 | case <-time.After(d + 500*time.Millisecond): 314 | if atomic.LoadInt64(×) == 0 { 315 | done <- errors.New("closure was not executed even once, something blocks it") 316 | } 317 | close(done) 318 | case <-done: 319 | } 320 | } 321 | }() 322 | go func() { 323 | timer := time.NewTimer(d) 324 | defer timer.Stop() 325 | InfLoop: 326 | for { 327 | for i := 0; i < maxVal; i++ { 328 | select { 329 | case <-timer.C: 330 | break InfLoop 331 | default: 332 | } 333 | fn(t, i) 334 | atomic.AddInt64(×, 1) 335 | } 336 | } 337 | close(done) 338 | }() 339 | return done 340 | } 341 | 342 | wait := func(t *testing.T, done <-chan error) { 343 | t.Helper() 344 | if err := <-done; err != nil { 345 | t.Error(err) 346 | } 347 | } 348 | 349 | // Initial fill. 350 | for i := 0; i < maxVal; i++ { 351 | m.Set(i, i) 352 | } 353 | t.Run("set_get", func(t *testing.T) { 354 | doneSet := do(t, maxVal, dur, func(t *testing.T, i int) { 355 | t.Helper() 356 | m.Set(i, i) 357 | }) 358 | doneGet := do(t, maxVal, dur, func(t *testing.T, i int) { 359 | t.Helper() 360 | if _, ok := m.Get(i); !ok { 361 | t.Errorf("missing value for key: %d", i) 362 | } 363 | }) 364 | wait(t, doneSet) 365 | wait(t, doneGet) 366 | }) 367 | t.Run("get-or-insert-and-delete", func(t *testing.T) { 368 | doneGetOrInsert := do(t, maxVal, dur, func(t *testing.T, i int) { 369 | t.Helper() 370 | m.GetOrInsert(i, i) 371 | }) 372 | doneDel := do(t, maxVal, dur, func(t *testing.T, i int) { 373 | t.Helper() 374 | m.Del(i) 375 | }) 376 | wait(t, doneGetOrInsert) 377 | wait(t, doneDel) 378 | }) 379 | } 380 | 381 | func TestHashMap_SetConcurrent(t *testing.T) { 382 | t.Parallel() 383 | m := New[string, int]() 384 | 385 | var wg sync.WaitGroup 386 | for i := 0; i < 100; i++ { 387 | wg.Add(1) 388 | 389 | go func(i int) { 390 | defer wg.Done() 391 | 392 | m.Set(strconv.Itoa(i), i) 393 | 394 | wg.Add(1) 395 | go func(i int) { 396 | defer wg.Done() 397 | 398 | m.Get(strconv.Itoa(i)) 399 | }(i) 400 | }(i) 401 | } 402 | 403 | wg.Wait() 404 | } 405 | 406 | func TestConcurrentInsertDelete(t *testing.T) { 407 | t.Parallel() 408 | 409 | for i := 0; i < 200; i++ { 410 | el1 := &ListElement[int, int]{ 411 | key: 111, 412 | keyHash: 111, 413 | } 414 | el2 := &ListElement[int, int]{ 415 | key: 222, 416 | keyHash: 222, 417 | } 418 | el3 := &ListElement[int, int]{ 419 | key: 333, 420 | keyHash: 333, 421 | } 422 | newIl := &ListElement[int, int]{ 423 | key: 223, 424 | keyHash: 223, 425 | } 426 | l := NewList[int, int]() 427 | l.Add(nil, el1.keyHash, el1.key, 111) 428 | l.Add(nil, el2.keyHash, el2.key, 222) 429 | l.Add(nil, el3.keyHash, el3.key, 333) 430 | wg := sync.WaitGroup{} 431 | wg.Add(2) 432 | 433 | go func() { 434 | rand.Seed(int64(time.Now().Nanosecond())) 435 | time.Sleep(time.Duration(rand.Intn(10))) 436 | l.Delete(el2) 437 | wg.Done() 438 | }() 439 | go func() { 440 | defer wg.Done() 441 | rand.Seed(int64(time.Now().Nanosecond())) 442 | time.Sleep(time.Duration(rand.Intn(10))) 443 | for { 444 | if _, _, inserted := l.Add(nil, newIl.keyHash, newIl.key, 223); inserted { 445 | return 446 | } 447 | } 448 | }() 449 | wg.Wait() 450 | 451 | assert.Equal(t, 3, l.Len()) 452 | _, found, _ := l.search(nil, newIl.keyHash, newIl.key) 453 | assert.True(t, found != nil) 454 | } 455 | } 456 | 457 | func TestGetOrInsert(t *testing.T) { 458 | t.Parallel() 459 | m := New[int, string]() 460 | value, ok := m.GetOrInsert(1, "1") 461 | assert.False(t, ok) 462 | assert.Equal(t, "1", value) 463 | 464 | value, ok = m.GetOrInsert(1, "2") 465 | assert.True(t, ok) 466 | assert.Equal(t, "1", value) 467 | } 468 | 469 | func TestGetOrInsertHangIssue67(_ *testing.T) { 470 | m := New[string, int]() 471 | 472 | var wg sync.WaitGroup 473 | key := "key" 474 | 475 | wg.Add(1) 476 | go func() { 477 | defer wg.Done() 478 | m.GetOrInsert(key, 9) 479 | m.Del(key) 480 | }() 481 | 482 | wg.Add(1) 483 | go func() { 484 | defer wg.Done() 485 | m.GetOrInsert(key, 9) 486 | m.Del(key) 487 | }() 488 | 489 | wg.Wait() 490 | } 491 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | // List is a sorted linked list. 8 | type List[Key comparable, Value any] struct { 9 | count atomic.Uintptr 10 | head *ListElement[Key, Value] 11 | } 12 | 13 | // NewList returns an initialized list. 14 | func NewList[Key comparable, Value any]() *List[Key, Value] { 15 | return &List[Key, Value]{ 16 | head: &ListElement[Key, Value]{}, 17 | } 18 | } 19 | 20 | // Len returns the number of elements within the list. 21 | func (l *List[Key, Value]) Len() int { 22 | return int(l.count.Load()) 23 | } 24 | 25 | // First returns the first item of the list. 26 | func (l *List[Key, Value]) First() *ListElement[Key, Value] { 27 | return l.head.Next() 28 | } 29 | 30 | // Add adds an item to the list and returns false if an item for the hash existed. 31 | // searchStart = nil will start to search at the head item. 32 | func (l *List[Key, Value]) Add(searchStart *ListElement[Key, Value], hash uintptr, key Key, value Value) (element *ListElement[Key, Value], existed bool, inserted bool) { 33 | left, found, right := l.search(searchStart, hash, key) 34 | if found != nil { // existing item found 35 | return found, true, false 36 | } 37 | 38 | element = &ListElement[Key, Value]{ 39 | key: key, 40 | keyHash: hash, 41 | } 42 | element.value.Store(&value) 43 | return element, false, l.insertAt(element, left, right) 44 | } 45 | 46 | // AddOrUpdate adds or updates an item to the list. 47 | func (l *List[Key, Value]) AddOrUpdate(searchStart *ListElement[Key, Value], hash uintptr, key Key, value Value) (*ListElement[Key, Value], bool) { 48 | left, found, right := l.search(searchStart, hash, key) 49 | if found != nil { // existing item found 50 | found.value.Store(&value) // update the value 51 | return found, true 52 | } 53 | 54 | element := &ListElement[Key, Value]{ 55 | key: key, 56 | keyHash: hash, 57 | } 58 | element.value.Store(&value) 59 | return element, l.insertAt(element, left, right) 60 | } 61 | 62 | // Delete deletes an element from the list. 63 | func (l *List[Key, Value]) Delete(element *ListElement[Key, Value]) { 64 | if !element.deleted.CompareAndSwap(0, 1) { 65 | return // concurrent delete of the item is in progress 66 | } 67 | 68 | right := element.Next() 69 | // point head to next element if element to delete was head 70 | l.head.next.CompareAndSwap(element, right) 71 | 72 | // element left from the deleted element will replace its next 73 | // pointer to the next valid element on call of Next(). 74 | 75 | l.count.Add(^uintptr(0)) // decrease counter 76 | } 77 | 78 | func (l *List[Key, Value]) search(searchStart *ListElement[Key, Value], hash uintptr, key Key) (left, found, right *ListElement[Key, Value]) { 79 | if searchStart != nil && hash < searchStart.keyHash { // key would remain left from item? { 80 | searchStart = nil // start search at head 81 | } 82 | 83 | if searchStart == nil { // start search at head? 84 | left = l.head 85 | found = left.Next() 86 | if found == nil { // no items beside head? 87 | return nil, nil, nil 88 | } 89 | } else { 90 | found = searchStart 91 | } 92 | 93 | for { 94 | if hash == found.keyHash && key == found.key { // key hash already exists, compare keys 95 | return nil, found, nil 96 | } 97 | 98 | if hash < found.keyHash { // new item needs to be inserted before the found value 99 | if l.head == left { 100 | return nil, nil, found 101 | } 102 | return left, nil, found 103 | } 104 | 105 | // go to next element in sorted linked list 106 | left = found 107 | found = left.Next() 108 | if found == nil { // no more items on the right 109 | return left, nil, nil 110 | } 111 | } 112 | } 113 | 114 | func (l *List[Key, Value]) insertAt(element, left, right *ListElement[Key, Value]) bool { 115 | if left == nil { 116 | left = l.head 117 | } 118 | 119 | element.next.Store(right) 120 | 121 | if !left.next.CompareAndSwap(right, element) { 122 | return false // item was modified concurrently 123 | } 124 | 125 | l.count.Add(1) 126 | return true 127 | } 128 | -------------------------------------------------------------------------------- /list_element.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | // ListElement is an element of a list. 8 | type ListElement[Key comparable, Value any] struct { 9 | keyHash uintptr 10 | 11 | // deleted marks the item as deleting or deleted 12 | // this is using uintptr instead of atomic.Bool to avoid using 32 bit int on 64 bit systems 13 | deleted atomic.Uintptr 14 | 15 | // next points to the next element in the list. 16 | // it is nil for the last item in the list. 17 | next atomic.Pointer[ListElement[Key, Value]] 18 | 19 | value atomic.Pointer[Value] 20 | 21 | key Key 22 | } 23 | 24 | // Value returns the value of the list item. 25 | func (e *ListElement[Key, Value]) Value() Value { 26 | return *e.value.Load() 27 | } 28 | 29 | // Next returns the item on the right. 30 | func (e *ListElement[Key, Value]) Next() *ListElement[Key, Value] { 31 | for next := e.next.Load(); next != nil; { 32 | // if the next item is not deleted, return it 33 | if next.deleted.Load() == 0 { 34 | return next 35 | } 36 | 37 | // point current elements next to the following item 38 | // after the deleted one until a non deleted or list end is found 39 | following := next.Next() 40 | if e.next.CompareAndSwap(next, following) { 41 | next = following 42 | } else { 43 | next = next.Next() 44 | } 45 | } 46 | return nil // end of the list reached 47 | } 48 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cornelk/hashmap/assert" 7 | ) 8 | 9 | func TestListNew(t *testing.T) { 10 | l := NewList[uintptr, uintptr]() 11 | node := l.First() 12 | assert.True(t, node == nil) 13 | 14 | node = l.head.Next() 15 | assert.True(t, node == nil) 16 | } 17 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "sync/atomic" 5 | "unsafe" 6 | ) 7 | 8 | type store[Key comparable, Value any] struct { 9 | keyShifts uintptr // Pointer size - log2 of array size, to be used as index in the data array 10 | count atomic.Uintptr // count of filled elements in the slice 11 | array unsafe.Pointer // pointer to slice data array 12 | index []*ListElement[Key, Value] // storage for the slice for the garbage collector to not clean it up 13 | } 14 | 15 | // item returns the item for the given hashed key. 16 | func (s *store[Key, Value]) item(hashedKey uintptr) *ListElement[Key, Value] { 17 | index := hashedKey >> s.keyShifts 18 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(s.array) + index*intSizeBytes)) 19 | item := (*ListElement[Key, Value])(atomic.LoadPointer(ptr)) 20 | return item 21 | } 22 | 23 | // adds an item to the index if needed and returns the new item counter if it changed, otherwise 0. 24 | func (s *store[Key, Value]) addItem(item *ListElement[Key, Value]) uintptr { 25 | index := item.keyHash >> s.keyShifts 26 | ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(s.array) + index*intSizeBytes)) 27 | 28 | for { // loop until the smallest key hash is in the index 29 | element := (*ListElement[Key, Value])(atomic.LoadPointer(ptr)) // get the current item in the index 30 | if element == nil { // no item yet at this index 31 | if atomic.CompareAndSwapPointer(ptr, nil, unsafe.Pointer(item)) { 32 | return s.count.Add(1) 33 | } 34 | continue // a new item was inserted concurrently, retry 35 | } 36 | 37 | if item.keyHash < element.keyHash { 38 | // the new item is the smallest for this index? 39 | if !atomic.CompareAndSwapPointer(ptr, unsafe.Pointer(element), unsafe.Pointer(item)) { 40 | continue // a new item was inserted concurrently, retry 41 | } 42 | } 43 | return 0 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | const ( 8 | // intSizeBytes is the size in byte of an int or uint value. 9 | intSizeBytes = strconv.IntSize >> 3 10 | ) 11 | 12 | // roundUpPower2 rounds a number to the next power of 2. 13 | func roundUpPower2(i uintptr) uintptr { 14 | i-- 15 | i |= i >> 1 16 | i |= i >> 2 17 | i |= i >> 4 18 | i |= i >> 8 19 | i |= i >> 16 20 | i |= i >> 32 21 | i++ 22 | return i 23 | } 24 | 25 | // log2 computes the binary logarithm of x, rounded up to the next integer. 26 | func log2(i uintptr) uintptr { 27 | var n, p uintptr 28 | for p = 1; p < i; p += p { 29 | n++ 30 | } 31 | return n 32 | } 33 | -------------------------------------------------------------------------------- /util_hash.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math/bits" 7 | "reflect" 8 | "unsafe" 9 | ) 10 | 11 | const ( 12 | prime1 uint64 = 11400714785074694791 13 | prime2 uint64 = 14029467366897019727 14 | prime3 uint64 = 1609587929392839161 15 | prime4 uint64 = 9650029242287828579 16 | prime5 uint64 = 2870177450012600261 17 | ) 18 | 19 | var prime1v = prime1 20 | 21 | /* 22 | Copyright (c) 2016 Caleb Spare 23 | 24 | MIT License 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining 27 | a copy of this software and associated documentation files (the 28 | "Software"), to deal in the Software without restriction, including 29 | without limitation the rights to use, copy, modify, merge, publish, 30 | distribute, sublicense, and/or sell copies of the Software, and to 31 | permit persons to whom the Software is furnished to do so, subject to 32 | the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be 35 | included in all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 38 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 39 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 40 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 41 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 42 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 43 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 44 | */ 45 | 46 | // setDefaultHasher sets the default hasher depending on the key type. 47 | // Inlines hashing as anonymous functions for performance improvements, other options like 48 | // returning an anonymous functions from another function turned out to not be as performant. 49 | func (m *Map[Key, Value]) setDefaultHasher() { 50 | var key Key 51 | kind := reflect.ValueOf(&key).Elem().Type().Kind() 52 | 53 | switch kind { 54 | case reflect.Int, reflect.Uint, reflect.Uintptr: 55 | switch intSizeBytes { 56 | case 2: 57 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashWord)) 58 | case 4: 59 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashDword)) 60 | case 8: 61 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashQword)) 62 | 63 | default: 64 | panic(fmt.Errorf("unsupported integer byte size %d", intSizeBytes)) 65 | } 66 | 67 | case reflect.Int8, reflect.Uint8: 68 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashByte)) 69 | case reflect.Int16, reflect.Uint16: 70 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashWord)) 71 | case reflect.Int32, reflect.Uint32: 72 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashDword)) 73 | case reflect.Int64, reflect.Uint64: 74 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashQword)) 75 | case reflect.Float32: 76 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashFloat32)) 77 | case reflect.Float64: 78 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashFloat64)) 79 | case reflect.String: 80 | m.hasher = *(*func(Key) uintptr)(unsafe.Pointer(&xxHashString)) 81 | 82 | default: 83 | panic(fmt.Errorf("unsupported key type %T of kind %v", key, kind)) 84 | } 85 | } 86 | 87 | // Specialized xxhash hash functions, optimized for the bit size of the key where available, 88 | // for all supported types beside string. 89 | 90 | var xxHashByte = func(key uint8) uintptr { 91 | h := prime5 + 1 92 | h ^= uint64(key) * prime5 93 | h = bits.RotateLeft64(h, 11) * prime1 94 | 95 | h ^= h >> 33 96 | h *= prime2 97 | h ^= h >> 29 98 | h *= prime3 99 | h ^= h >> 32 100 | 101 | return uintptr(h) 102 | } 103 | 104 | var xxHashWord = func(key uint16) uintptr { 105 | h := prime5 + 2 106 | h ^= (uint64(key) & 0xff) * prime5 107 | h = bits.RotateLeft64(h, 11) * prime1 108 | h ^= ((uint64(key) >> 8) & 0xff) * prime5 109 | h = bits.RotateLeft64(h, 11) * prime1 110 | 111 | h ^= h >> 33 112 | h *= prime2 113 | h ^= h >> 29 114 | h *= prime3 115 | h ^= h >> 32 116 | 117 | return uintptr(h) 118 | } 119 | 120 | var xxHashDword = func(key uint32) uintptr { 121 | h := prime5 + 4 122 | h ^= uint64(key) * prime1 123 | h = bits.RotateLeft64(h, 23)*prime2 + prime3 124 | 125 | h ^= h >> 33 126 | h *= prime2 127 | h ^= h >> 29 128 | h *= prime3 129 | h ^= h >> 32 130 | 131 | return uintptr(h) 132 | } 133 | 134 | var xxHashFloat32 = func(key float32) uintptr { 135 | h := prime5 + 4 136 | h ^= uint64(key) * prime1 137 | h = bits.RotateLeft64(h, 23)*prime2 + prime3 138 | 139 | h ^= h >> 33 140 | h *= prime2 141 | h ^= h >> 29 142 | h *= prime3 143 | h ^= h >> 32 144 | 145 | return uintptr(h) 146 | } 147 | 148 | var xxHashFloat64 = func(key float64) uintptr { 149 | h := prime5 + 4 150 | h ^= uint64(key) * prime1 151 | h = bits.RotateLeft64(h, 23)*prime2 + prime3 152 | 153 | h ^= h >> 33 154 | h *= prime2 155 | h ^= h >> 29 156 | h *= prime3 157 | h ^= h >> 32 158 | 159 | return uintptr(h) 160 | } 161 | 162 | var xxHashQword = func(key uint64) uintptr { 163 | k1 := key * prime2 164 | k1 = bits.RotateLeft64(k1, 31) 165 | k1 *= prime1 166 | h := (prime5 + 8) ^ k1 167 | h = bits.RotateLeft64(h, 27)*prime1 + prime4 168 | 169 | h ^= h >> 33 170 | h *= prime2 171 | h ^= h >> 29 172 | h *= prime3 173 | h ^= h >> 32 174 | 175 | return uintptr(h) 176 | } 177 | 178 | var xxHashString = func(key string) uintptr { 179 | sh := (*reflect.StringHeader)(unsafe.Pointer(&key)) 180 | bh := reflect.SliceHeader{ 181 | Data: sh.Data, 182 | Len: sh.Len, 183 | Cap: sh.Len, // cap needs to be set, otherwise xxhash fails on ARM Macs 184 | } 185 | 186 | b := *(*[]byte)(unsafe.Pointer(&bh)) 187 | var h uint64 188 | 189 | if sh.Len >= 32 { 190 | v1 := prime1v + prime2 191 | v2 := prime2 192 | v3 := uint64(0) 193 | v4 := -prime1v 194 | for len(b) >= 32 { 195 | v1 = round(v1, binary.LittleEndian.Uint64(b[0:8:len(b)])) 196 | v2 = round(v2, binary.LittleEndian.Uint64(b[8:16:len(b)])) 197 | v3 = round(v3, binary.LittleEndian.Uint64(b[16:24:len(b)])) 198 | v4 = round(v4, binary.LittleEndian.Uint64(b[24:32:len(b)])) 199 | b = b[32:len(b):len(b)] 200 | } 201 | h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) 202 | h = mergeRound(h, v1) 203 | h = mergeRound(h, v2) 204 | h = mergeRound(h, v3) 205 | h = mergeRound(h, v4) 206 | } else { 207 | h = prime5 208 | } 209 | 210 | h += uint64(sh.Len) 211 | 212 | i, end := 0, len(b) 213 | for ; i+8 <= end; i += 8 { 214 | k1 := round(0, binary.LittleEndian.Uint64(b[i:i+8:len(b)])) 215 | h ^= k1 216 | h = rol27(h)*prime1 + prime4 217 | } 218 | if i+4 <= end { 219 | h ^= uint64(binary.LittleEndian.Uint32(b[i:i+4:len(b)])) * prime1 220 | h = rol23(h)*prime2 + prime3 221 | i += 4 222 | } 223 | for ; i < end; i++ { 224 | h ^= uint64(b[i]) * prime5 225 | h = rol11(h) * prime1 226 | } 227 | 228 | h ^= h >> 33 229 | h *= prime2 230 | h ^= h >> 29 231 | h *= prime3 232 | h ^= h >> 32 233 | 234 | return uintptr(h) 235 | } 236 | 237 | func round(acc, input uint64) uint64 { 238 | acc += input * prime2 239 | acc = rol31(acc) 240 | acc *= prime1 241 | return acc 242 | } 243 | 244 | func mergeRound(acc, val uint64) uint64 { 245 | val = round(0, val) 246 | acc ^= val 247 | acc = acc*prime1 + prime4 248 | return acc 249 | } 250 | 251 | func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } 252 | func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } 253 | func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } 254 | func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } 255 | func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } 256 | func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } 257 | func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } 258 | func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } 259 | -------------------------------------------------------------------------------- /util_hash_test.go: -------------------------------------------------------------------------------- 1 | //go:build !386 2 | 3 | package hashmap 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/cornelk/hashmap/assert" 9 | ) 10 | 11 | func TestHashingUintptr(t *testing.T) { 12 | m := New[uintptr, uintptr]() 13 | assert.Equal(t, uintptr(0x9f29cb17a2a49995), m.hasher(1)) 14 | assert.Equal(t, uintptr(0xeac73e4044e82db0), m.hasher(2)) 15 | } 16 | 17 | func TestHashingUint64(t *testing.T) { 18 | m := New[uint64, uint64]() 19 | assert.Equal(t, uintptr(0x9f29cb17a2a49995), m.hasher(1)) 20 | assert.Equal(t, uintptr(0xeac73e4044e82db0), m.hasher(2)) 21 | } 22 | 23 | func TestHashingUint32(t *testing.T) { 24 | m := New[uint32, uint32]() 25 | assert.Equal(t, uintptr(0xf42f94001fcb5351), m.hasher(1)) 26 | assert.Equal(t, uintptr(0x277af360cedcb29e), m.hasher(2)) 27 | } 28 | 29 | func TestHashingUint16(t *testing.T) { 30 | m := New[uint16, uint16]() 31 | assert.Equal(t, uintptr(0xdd8f621dbf7f57f1), m.hasher(1)) 32 | assert.Equal(t, uintptr(0xfc2f33e9edde6f4a), m.hasher(0x102)) 33 | } 34 | 35 | func TestHashingUint8(t *testing.T) { 36 | m := New[uint8, uint8]() 37 | assert.Equal(t, uintptr(0x8a4127811b21e730), m.hasher(1)) 38 | assert.Equal(t, uintptr(0x4b79b8c95732b0e7), m.hasher(2)) 39 | } 40 | 41 | func TestHashingString(t *testing.T) { 42 | m := New[string, uint8]() 43 | assert.Equal(t, uintptr(0x6a1faf26e7da4cb9), m.hasher("properunittesting")) 44 | assert.Equal(t, uintptr(0x2d4ff7e12135f1f3), m.hasher("longstringlongstringlongstringlongstring")) 45 | } 46 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package hashmap 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cornelk/hashmap/assert" 7 | ) 8 | 9 | func TestLog2(t *testing.T) { 10 | var fixtures = map[uintptr]uintptr{ 11 | 0: 0, 12 | 1: 0, 13 | 2: 1, 14 | 3: 2, 15 | 4: 2, 16 | 5: 3, 17 | } 18 | 19 | for input, result := range fixtures { 20 | output := log2(input) 21 | assert.Equal(t, output, result) 22 | } 23 | } 24 | 25 | func TestHashCollision(t *testing.T) { 26 | m := New[string, int]() 27 | 28 | staticHasher := func(_ string) uintptr { 29 | return 4 // chosen by fair dice roll. guaranteed to be random. 30 | } 31 | 32 | m.SetHasher(staticHasher) 33 | 34 | inserted := m.Insert("1", 1) 35 | assert.True(t, inserted) 36 | inserted = m.Insert("2", 2) 37 | assert.True(t, inserted) 38 | 39 | value, ok := m.Get("1") 40 | assert.True(t, ok) 41 | assert.Equal(t, 1, value) 42 | 43 | value, ok = m.Get("2") 44 | assert.True(t, ok) 45 | assert.Equal(t, 2, value) 46 | } 47 | 48 | func TestAliasTypeSupport(t *testing.T) { 49 | type alias uintptr 50 | 51 | m := New[alias, alias]() 52 | 53 | inserted := m.Insert(1, 1) 54 | assert.True(t, inserted) 55 | 56 | value, ok := m.Get(1) 57 | assert.True(t, ok) 58 | assert.Equal(t, 1, value) 59 | } 60 | --------------------------------------------------------------------------------