A Golang lock-free thread-safe HashMap optimized for fastest read access.

hashmap Build Status GoDoc Go Report Card codecov

Overview

A Golang lock-free thread-safe HashMap optimized for fastest read access.

Usage

Set a value for a key in the map:

m := &HashMap{}
m.Set("amount", 123)

Read a value for a key from the map:

amount, ok := m.Get("amount")

Use the map to count URL requests:

var i int64
actual, _ := m.GetOrInsert("api/123", &i)
counter := (actual).(*int64)
atomic.AddInt64(counter, 1) // increase counter
...
count := atomic.LoadInt64(counter) // read counter

Benchmarks

Reading from the hash map in a thread-safe way is nearly as fast as reading from a standard Golang map in an unsafe way and twice as fast as Go's sync.Map:

BenchmarkReadHashMapUint-8                	  200000	      6830 ns/op
BenchmarkReadGoMapUintUnsafe-8            	  300000	      4280 ns/op
BenchmarkReadGoMapUintMutex-8             	   30000	     51294 ns/op
BenchmarkReadGoSyncMapUint-8              	  200000	     10351 ns/op

If your keys for the map are already hashes, no extra hashing needs to be done by the map:

BenchmarkReadHashMapHashedKey-8           	 1000000	      1692 ns/op

Reading from the map while writes are happening:

BenchmarkReadHashMapWithWritesUint-8      	  200000	      8395 ns/op
BenchmarkReadGoMapWithWritesUintMutex-8   	   10000	    143793 ns/op
BenchmarkReadGoSyncMapWithWritesUint-8    	  100000	     12221 ns/op

Write performance without any concurrent reads:

BenchmarkWriteHashMapUint-8               	   10000	    210383 ns/op
BenchmarkWriteGoMapMutexUint-8            	   30000	     53331 ns/op
BenchmarkWriteGoSyncMapUint-8             	   10000	    176903 ns/op

The benchmarks were run with Golang 1.10.1 on MacOS.

Benefits over Golangs builtin map

  • Faster

  • thread-safe access without need of a(n extra) mutex

  • Compare-and-swap access for values

  • Access functions for keys that are hashes and do not need to be hashed again

Benefits over Golangs sync.Map

  • Faster

  • Access functions for keys that are hashes and do not need to be hashed again

Technical details

  • Technical design decisions have been made based on benchmarks that are stored in an external repository: go-benchmark

  • The library uses a sorted doubly linked list and a slice as an index into that list.

  • The Get() function contains helper functions that have been inlined manually until the Golang compiler will inline them automatically.

  • It optimizes the slice access by circumventing the Golang size check when reading from the slice. Once a slice is allocated, the size of it does not change. The library limits the index into the slice, therefore the Golang size check is obsolete. When the slice reaches a defined fill rate, a bigger slice is allocated and all keys are recalculated and transferred into the new slice.

Owner
Cornel
Golang | Distributed systems | Kubernetes | AWS | Google Cloud
Cornel
Comments
  • Use uintptr instead of uint64 to support 32 bit systems with same cod…

    Use uintptr instead of uint64 to support 32 bit systems with same cod…

    This should allow to use the hashmap on 32 bit systems without need of copying the codebase to files that use 32 bit instead of 64 bit variables.

    @edhemphill please test and review

  • Race detector warnings

    Race detector warnings

    Hi, I'm getting race detector warnings when accessing a hashmap from multiple goroutines:

    WARNING: DATA RACE
    Read at 0x00c001b7dbd0 by goroutine 64:
      sync/atomic.LoadInt64()
          /usr/local/Cellar/go/1.11.1/libexec/src/runtime/race_amd64.s:211 +0xb
      .../vendor/github.com/cornelk/hashmap.(*ListElement).Next()
          /.../vendor/github.com/cornelk/hashmap/listelement.go:25 +0x3e
      .../vendor/github.com/cornelk/hashmap.(*HashMap).fillIndexItems()
          /.../vendor/github.com/cornelk/hashmap/hashmap.go:373 +0x77
      .../vendor/github.com/cornelk/hashmap.(*HashMap).grow()
          /.../vendor/github.com/cornelk/hashmap/hashmap.go:339 +0x292
    
    Previous write at 0x00c001b7dbd0 by main goroutine:
      .../vendor/github.com/cornelk/hashmap.(*List).insertAt()
          /.../vendor/github.com/cornelk/hashmap/list.go:128 +0x234
      .../vendor/github.com/cornelk/hashmap.(*List).AddOrUpdate()
          /.../vendor/github.com/cornelk/hashmap/list.go:65 +0xcf
      .../vendor/github.com/cornelk/hashmap.(*HashMap).insertListElement()
          /.../vendor/github.com/cornelk/hashmap/hashmap.go:237 +0xc8
      core-next/vendor/github.com/cornelk/hashmap.(*HashMap).Set()
          /.../vendor/github.com/cornelk/hashmap/hashmap.go:211 +0x172
          /.../main.go:45 +0xd3
    

    But the strange part is, this is code running in a single goroutine - just iterating over set of data and calling .Set() on the hashmap.Hashmap.

    Example:

    var dataMap hashmap.HashMap
    for _, data := range listOfData {
    	var innerMap hashmap.HashMap
    	for _, innerData := range data.InnerList {
    		innerMap.Set(innerData, struct{}{})
    	}
    	dataMap.Set(data.ID, innerMap)
    }
    

    Any ideas what to do here? Am I using the hashmap.Hashmap wrong?

  • swap order of List fields for 64 + 32-bit version

    swap order of List fields for 64 + 32-bit version

    I just swapped the order of field in List. This partially fixes problems on 32-bit ARM. It passes all tests, but fails on a benchmark. Probably due to a AddInt64 call.

    root@wigwagrelay:~/go-workspace/src/github.com/cornelk/hashmap# go test
    PASS
    ok  	github.com/cornelk/hashmap	0.023s
    root@wigwagrelay:~/go-workspace/src/github.com/cornelk/hashmap# go test -bench=.
    BenchmarkReadHashMapUint-2                	    2000	    846979 ns/op
    BenchmarkReadHashMapWithWritesUint-2      	--- FAIL: BenchmarkReadHashMapWithWritesUint-2
    BenchmarkReadHashMapString-2              	     500	   2481451 ns/op
    BenchmarkReadHashMapInterface-2           	    1000	   2514878 ns/op
    BenchmarkReadHashMapHashedKey-2           	    2000	    744152 ns/op
    BenchmarkReadGoMapUintUnsafe-2            	    5000	    231738 ns/op
    BenchmarkReadGoMapUintMutex-2             	    5000	    414895 ns/op
    BenchmarkReadGoMapWithWritesUintMutex-2   	    1000	   2226309 ns/op
    BenchmarkReadGoSyncMapUint-2              	    1000	   1633804 ns/op
    BenchmarkReadGoSyncMapWithWritesUint-2    	    1000	   2079039 ns/op
    BenchmarkReadGoMapStringUnsafe-2          	    2000	   1549876 ns/op
    BenchmarkReadGoMapStringMutex-2           	    1000	   1802993 ns/op
    BenchmarkWriteHashMapUint-2               	     300	   4837673 ns/op
    BenchmarkWriteHashMapHashedKey-2          	     300	   3929762 ns/op
    BenchmarkWriteGoMapMutexUint-2            	    1000	   1134647 ns/op
    BenchmarkWriteGoSyncMapUint-2             	     300	   5139583 ns/op
    FAIL
    exit status 1
    FAIL	github.com/cornelk/hashmap	34.995s
    root@wigwagrelay:~/go-workspace/src/github.com/cornelk/hashmap# 
    
  • How about use as counter map ?

    How about use as counter map ?

    I use a map for recording api invoke count in high conccurrency QPS which key is api , value is uint64,what's the efficient way to use this hashmap to implements 'increase value atomiclly as value++' which actually don't need change unsafe.Pointer ?

  • Question: is possible the values in `left` and `right` are change when inserting a new element?

    Question: is possible the values in `left` and `right` are change when inserting a new element?

    When inserting a new element into the double-linked sorted list, is there a chance that just before line 139, the node left and node right are replaced by another thread, but the address of left and right are still the same as before being replaced?

    If possible, the list looks the same but node left and node right may already have different values.

    Because the following CAS operations are based on pointer addresses, they can succeed. But the element may be placed in an incorrect place since the values in left and right are already changed.

    Is such a case prevented somewhere else if I missed something?

    https://github.com/cornelk/hashmap/blob/c93d96ce6b8aa530423b5e10d4a56194284b47f6/list.go#L136-L148

  • Lost add when deleting concurrently

    Lost add when deleting concurrently

    List implementation has potentional deadlock. I've made a branch with random Sleeps to reproduce. Please run TestList_insertAt() few times to get deadlock. https://github.com/coraxster/hashmap/tree/list-deadlock

  • Lost writes on table growth

    Lost writes on table growth

    The map loses writes and it looks a lot (I didn't verify that) that the problem is in missing happens-before edges between growth and concurrent writes.

    Most like this issue is the same as #46, but I didn't check their reproducer.

    Here is a simple reproducer:

    func TestMapParallelStores(t *testing.T) {
    	const (
    		numStorers = 8
    		numEntries = 10000
    	)
    	m := &HashMap{}
    	cdone := make(chan bool)
    	// Write to the map in parallel.
    	for i := 0; i < numStorers; i++ {
    		go func(id int) {
    			for j := 0; j < numEntries; j++ {
    				m.Set(strconv.Itoa(id)+":"+strconv.Itoa(j), j)
    			}
    			cdone <- true
    		}(i)
    	}
    	// Wait for the goroutines to finish.
    	for i := 0; i < numStorers; i++ {
    		<-cdone
    	}
    	// Verify map contents.
    	l := m.Len()
    	if l != numStorers*numEntries {
    		t.Errorf("invalid map length: got %d, expected %d", l, numStorers*numEntries)
    	}
    	for i := 0; i < numStorers; i++ {
    		for j := 0; j < numEntries; j++ {
    			k := strconv.Itoa(i) + ":" + strconv.Itoa(j)
    			v, ok := m.GetStringKey(k)
    			if !ok {
    				t.Errorf("value not found for %s", k)
    			}
    			if vi, ok := v.(int); ok && vi != j {
    				t.Errorf("values do not match for %s: %v", k, v)
    			}
    		}
    	}
    }
    

    Environment: Ubuntu 20.04, Go 1.16.7.

    Expected result: the test passes. Note: the test may pass with a certain probability. For instance, the test always succeeds if numEntries set to 1 since no rehashing happens in such scenario.

    Actual result: the test fails.

    $ go test -count=10 -run=TestMapParallelStores
    --- FAIL: TestMapParallelStores (3.79s)
        hashmap_test.go:700: value not found for 5:6070
    ...
    --- FAIL: TestMapParallelStores (3.46s)
        hashmap_test.go:700: value not found for 4:3905
        hashmap_test.go:700: value not found for 7:4212
    FAIL
    exit status 1
    FAIL	github.com/cornelk/hashmap	36.795s
    
  • invalid recursive type atomic.Pointer

    invalid recursive type atomic.Pointer

    Describe the bug when I compile my project which indirectly depend this package github.com\cornelk\[email protected]\list_element.go:17:13: invalid recursive type atomic.Pointer image

    System (please complete the following information):

    • OS: windows10
    • Version / Commit: GO1.19.4
  • Hang when using GetOrInsert and DEL

    Hang when using GetOrInsert and DEL

    Describe the bug Hang when using GetOrInsert and DEL

    To Reproduce

    func TestDebug(t *testing.T) {
        m := hashmap.New[string, int]()
    
        var wg sync.WaitGroup
        key := "key"
    
        wg.Add(1)
        go func() {
            defer wg.Done()
            m.GetOrInsert(key, 9)
            m.Del(key)
        }()
    
        wg.Add(1)
        go func() {
            defer wg.Done()
            m.GetOrInsert(key, 9)
            m.Del(key)
        }()
    
        wg.Wait()
    }
    

    Expected behavior not hang

    System (please complete the following information):

    • OS: macos
    • Version / Commit: latest
  • List.Delete stucks in infinite loop

    List.Delete stucks in infinite loop

    List.Delete stucks in infinite loop: The following is my test case:

    package main import ( "github.com/cornelk/hashmap" ) func main() { num := 128 map1 := hashmap.New(uintptr(num)) map1.Set("aa1", true) map1.Set("aa2", true) map1.Del("aa1") }

    //////////////////////////////////////////////////////////

    1. But if I delete aa2 it work fine. map1.Set("aa1", true) map1.Set("aa2", true) map1.Del("aa2")

    ////////////////////////////////////////////////////////// 2. And the following is also work fine. map1.Set("a1", true) map1.Set("a2", true) map1.Del("a2")

  • Possible concurrency problem in `grow`

    Possible concurrency problem in `grow`

    https://github.com/cornelk/hashmap/blob/master/hashmap.go#L329-L333

    func (m *HashMap) grow(newSize uintptr, loop bool) {
        // ...
    
        m.fillIndexItems(newdata) // initialize new index slice with longer keys
    
        atomic.StorePointer(&m.datamap, unsafe.Pointer(newdata))
    
        m.fillIndexItems(newdata) // make sure that the new index is up to date with the current state of the linked list
    
        // ...
    }
    

    If there is any data added while grow is running at first m.fillIndexItems(newdata), such data may not be visible when newdata is set to m.datamap and second m.fillIndexItems(newdata) isn't done.

  • Unexpected behaviors under large number of data

    Unexpected behaviors under large number of data

    Describe the bug Under certain cases of large data it seems that the map performs unexpectedly, meaning it performs differently from sync.Map, a normal map with a RWMutex, and what's expected. I don't know whether these behaviors are expected or whether I'm using the map wrong, but I think I used it correctly.

    To Reproduce Code 1:

    func BenchmarkHashMap_Case1(b *testing.B) {
    	b.StopTimer()
    	wg := sync.WaitGroup{}
    	for i := 0; i < b.N; i++ {
    		M := hashmap.New[int, int]()
    		b.StartTimer()
    		for k := 0; k < iter0; k++ {
    			wg.Add(1)
    			go func(l, h int) {
    				for j := l; j < h; j++ {
    					M.Insert(j, j)
    				}
    				for j := l; j < h; j++ {
    					_, a := M.Get(j)
    					if !a {
    						b.Error("key doesn't exist", j)
    					}
    				}
    				for j := l; j < h; j++ {
    					x, _ := M.Get(j)
    					if x != j {
    						b.Error("incorrect value", j, x)
    					}
    				}
    				wg.Done()
    			}(k*elementNum0, (k+1)*elementNum0)
    		}
    		wg.Wait()
    		b.StopTimer()
    	}
    }
    

    Code 2:

    func BenchmarkHashMap_Case3(b *testing.B) {
    	b.StopTimer()
    	wg := &sync.WaitGroup{}
    	for a := 0; a < b.N; a++ {
    		M := hashmap.New[int, int]()
    		b.StartTimer()
    		for j := 0; j < iter0; j++ {
    			wg.Add(1)
    			go func(l, h int) {
    				defer wg.Done()
    				for i := l; i < h; i++ {
    					M.Insert(i, i)
    				}
    
    				for i := l; i < h; i++ {
    					_, x := M.Get(i)
    					if !x {
    						b.Errorf("not put: %v\n", O(i))
    					}
    				}
    				for i := l; i < h; i++ {
    					M.Del(i)
    
    				}
    				for i := l; i < h; i++ {
    					_, x := M.Get(i)
    					if x {
    						b.Errorf("not removed: %v\n", O(i))
    					}
    				}
    
    			}(j*elementNum0, (j+1)*elementNum0)
    		}
    		wg.Wait()
    	
    ```	b.StopTimer()
    	}
    
    }
    Set `elementNum0=1024; iter0=8`. You can remove the benchmark part and all those timing stuffs.
    
    **Expected behavior**
    What these 2 functions are doing is that each thread is performing operations(read/write/delete) on different set of keys. Since different threads aren't interfering with each other, so operations are performed sequentially with respect to each thread; therefore, no errors should occur. This is the behavior for sync.Map and a default map with RWMutex. However, errors occur for this implementation. 
    
    **System (please complete the following information): AMD **
     - OS: win10 64 bit
     - Version / Commit: newest
     - Go 1.19.2
    
    **Additional context**
    I didn't thoroughly read the code of this hashmap, but I assume that this behavior is potentially caused by concurrent modifications during resizing?
    I also have a case2 benchmark which involves using M.set(), but I don't know whether is M.set() really slow or performing unexpectedly, the benchmark usually results in a timeout. 
    Thanks.
    
  • 1.19.4

    1.19.4

    Describe the bug After upgrade Go to 1.19.4 always got error on build

    github.com/cornelk/hashmap

    ../../GOLANG/pkg/mod/github.com/cornelk/[email protected]/list_element.go:17:13: invalid recursive type atomic.Pointer ../../GOLANG/pkg/mod/github.com/cornelk/[email protected]/list_element.go:17:13: atomic.Pointer refers to ../../GOLANG/pkg/mod/github.com/cornelk/[email protected]/list_element.go:17:22: ListElement refers to ../../GOLANG/pkg/mod/github.com/cornelk/[email protected]/list_element.go:17:13: atomic.Pointer

    To Reproduce install go 1.19.4 use hash map

    Expected behavior compile without errors

    System (please complete the following information):

    • Mac OS 13.0.1:
    • hash map 1.0.8:

    Additional context

  • Support compound keys

    Support compound keys

    We would like to use struct keys, but do not need a general "supports all structs" approach being asked for in #53 . We can chain hashmaps together, but that is not a good solution for us.

    In particular, we were wondering if a few new key types could be added.

    type Key128 struct {
        One, Two uint64
    }
    

    This would be generally useful for a number of things such as IPv6 address lookups.

    These next two would be useful for IPv6 + some other tuple information or two IPv6 addresses.

    type Key192 struct {
        One, Two, Three uint64
    }
    
    type Key256 struct {
        One, Two, Three, Four uint64
    }
    
  • panic: unsupported key type *foo

    panic: unsupported key type *foo

    Apparently it is not currently possible to add a reference to a struct as a key in the hashmap. This is valid in the native map, sync.Map, etc.

    In this example, i create an "Attribute" and "Value" struct, then try to create a map of key: *Attribute, value:*Value... https://play.golang.org/p/m2RkogzD-82

    Error returned is: panic: unsupported key type *main.Attribute

Go concurrent-safe, goroutine-safe, thread-safe queue
Go concurrent-safe, goroutine-safe, thread-safe queue

goconcurrentqueue - Concurrent safe queues The package goconcurrentqueue offers a public interface Queue with methods for a queue. It comes with multi

Dec 31, 2022
High-performance minimalist queue implemented using a stripped-down lock-free ringbuffer, written in Go (golang.org)

This project is no longer maintained - feel free to fork the project! gringo A high-performance minimalist queue implemented using a stripped-down loc

Oct 24, 2022
Package ring provides a high performance and thread safe Go implementation of a bloom filter.

ring - high performance bloom filter Package ring provides a high performance and thread safe Go implementation of a bloom filter. Usage Please see th

Nov 20, 2022
A thread safe map which has expiring key-value pairs

~ timedmap ~ A map which has expiring key-value pairs. go get github.com/zekroTJA/timedmap Intro This package allows to set values to a map which will

Dec 29, 2022
go.fifo provides a simple fifo thread-safe queue for the Go programming language

go.fifo Description go.fifo provides a simple FIFO thread-safe queue. *fifo.Queue supports pushing an item at the end with Add(), and popping an item

Aug 29, 2022
A highly optimized double-ended queue

Overview Deque is a highly optimized double-ended queue. Benchmark Benchmark_PushBack/Deque<harden> 100000000 10.3 ns/op 9 B/op

Dec 13, 2022
A fast (5x) string keyed read-only map for Go - particularly good for keys using a small set of nearby runes.

faststringmap faststringmap is a fast read-only string keyed map for Go (golang). For our use case it is approximately 5 times faster than using Go's

Jan 8, 2023
Access LeetCode problems via id, Golang implementation

LCid-Go Introduction This naive toy is based on bunnyxt/lcid, and implemented in Golang for practice. They are same in program logic and static files.

Jan 15, 2022
Null Types, Safe primitive type conversion and fetching value from complex structures.

Typ Typ is a library providing a powerful interface to impressive user experience with conversion and fetching data from built-in types in Golang Feat

Sep 26, 2022
Type-safe, zero-allocation sets for Go

Set Package set is a type-safe, zero-allocation port of the excellent package fatih/set. It contains sets for most of the basic types and you can gene

Jan 5, 2023
dagger is a fast, concurrency safe, mutable, in-memory directed graph library with zero dependencies
dagger is a fast, concurrency safe, mutable, in-memory directed graph library with zero dependencies

dagger is a blazing fast, concurrency safe, mutable, in-memory directed graph implementation with zero dependencies

Dec 19, 2022
Graphoscope: a solution to access multiple independent data sources from a common UI and show data relations as a graph
Graphoscope: a solution to access multiple independent data sources from a common UI and show data relations as a graph

Graphoscope A solution to access multiple independent data sources from a common UI and show data relations as a graph: Contains a list of by default

May 26, 2022
CLRS study. Codes are written with golang.

algorithms CLRS study. Codes are written with golang. go version: 1.11 Heap BinaryHeap on array BinaryHeap on linkedlist LeftistHeap FibonacciHeap Tre

Dec 26, 2022
Golang string comparison and edit distance algorithms library, featuring : Levenshtein, LCS, Hamming, Damerau levenshtein (OSA and Adjacent transpositions algorithms), Jaro-Winkler, Cosine, etc...

Go-edlib : Edit distance and string comparison library Golang string comparison and edit distance algorithms library featuring : Levenshtein, LCS, Ham

Dec 20, 2022
Gota: DataFrames and data wrangling in Go (Golang)

Gota: DataFrames, Series and Data Wrangling for Go This is an implementation of DataFrames, Series and data wrangling methods for the Go programming l

Jan 6, 2023
Roaring bitmaps in Go (golang)

roaring This is a go version of the Roaring bitmap data structure. Roaring bitmaps are used by several major systems such as Apache Lucene and derivat

Jan 8, 2023
A simple Set data structure implementation in Go (Golang) using LinkedHashMap.

Set Set is a simple Set data structure implementation in Go (Golang) using LinkedHashMap. This library allow you to get a set of int64 or string witho

Sep 26, 2022
skiplist for golang

skiplist reference from redis zskiplist Usage package main import ( "fmt" "github.com/gansidui/skiplist" "log" ) type User struct { score float6

Dec 6, 2022