Skip to content

Commit

Permalink
improve hashing and avoid dedicated MapString type
Browse files Browse the repository at this point in the history
  • Loading branch information
cornelk committed Sep 3, 2022
1 parent 26a941b commit 199c5e7
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 624 deletions.
8 changes: 0 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,3 @@ test-coverage-web: test-coverage ## run unit tests and show test coverage in bro

install-linters: ## install all used linters
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v1.49.0

alternatives: ## generate alternative non numeric hashmap versions
cp hashmap.go hashmap_string.go
sed -i 's,Map,MapString,' hashmap_string.go
sed -i 's,New\[,NewString\[,' hashmap_string.go
sed -i 's,// New,// NewString,' hashmap_string.go
sed -i 's,NewSized,NewStringSized,' hashmap_string.go
sed -i 's,numeric,string,' hashmap_string.go
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ The minimal supported Golang version is 1.19 as it makes use of Generics and the

## Usage

For `New()` only Go numeric types are supported. For string keyed maps `NewString` has to be used.

Example uint8 key map uses:

```
Expand All @@ -28,14 +26,14 @@ value, ok := m.Get(1)
Example string key map uses:

```
m := NewString[string, int]()
m := New[string, int]()
m.Set("amount", 123)
value, ok := m.Get("amount")
```

Using the map to count URL requests:
```
m := NewString[string, *int64]()
m := New[string, *int64]()
var i int64
counter, _ := m.GetOrInsert("api/123", &i)
atomic.AddInt64(counter, 1) // increase counter
Expand All @@ -49,18 +47,18 @@ Reading from the hash map for numeric key types in a thread-safe way is faster t
in an unsafe way and four times faster than Golang's `sync.Map`:

```
BenchmarkReadHashMapUint-8 1788601 668.4 ns/op
BenchmarkReadHaxMapUint-8 1691654 709.6 ns/op
BenchmarkReadGoMapUintUnsafe-8 1516452 784.4 ns/op
BenchmarkReadGoMapUintMutex-8 39429 27978 ns/op
BenchmarkReadGoSyncMapUint-8 446930 2544 ns/op
BenchmarkReadHashMapUint-8 1774460 677.3 ns/op
BenchmarkReadHaxMapUint-8 1758708 679.0 ns/op
BenchmarkReadGoMapUintUnsafe-8 1497732 790.9 ns/op
BenchmarkReadGoMapUintMutex-8 41562 28672 ns/op
BenchmarkReadGoSyncMapUint-8 454401 2646 ns/op
```

Reading from the map while writes are happening:
```
BenchmarkReadHashMapWithWritesUint-8 1418299 856.4 ns/op
BenchmarkReadHaxMapWithWritesUint-8 1262414 948.5 ns/op
BenchmarkReadGoSyncMapWithWritesUint-8 382785 3240 ns/op
BenchmarkReadHashMapWithWritesUint-8 1388560 859.1 ns/op
BenchmarkReadHaxMapWithWritesUint-8 1306671 914.5 ns/op
BenchmarkReadGoSyncMapWithWritesUint-8 335732 3113 ns/op
```

Write performance without any concurrent reads:
Expand Down Expand Up @@ -88,5 +86,3 @@ The benchmarks were run with Golang 1.19.0 on Linux and AMD64 using `make benchm
When the slice reaches a defined fill rate, a bigger slice is allocated and all keys are recalculated and transferred into the new slice.

* For hashing, specialized xxhash implementations are used that match the size of the key type where available

* A specialized String version of the map exists due to a limitation of type switches of parametric types - see https://github.com/golang/go/issues/45380 for more info.
4 changes: 2 additions & 2 deletions benchmarks/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ func setupHaxMap(b *testing.B) *haxmap.HashMap[uintptr, uintptr] {
return m
}

func setupHashMapString(b *testing.B) (*hashmap.MapString[string, string], []string) {
func setupHashMapString(b *testing.B) (*hashmap.Map[string, string], []string) {
b.Helper()

m := hashmap.NewString[string, string]()
m := hashmap.New[string, string]()
keys := make([]string, benchmarkItemCount)
for i := 0; i < benchmarkItemCount; i++ {
s := strconv.Itoa(i)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ go 1.19
replace github.com/cornelk/hashmap => ../

require (
github.com/alphadose/haxmap v0.3.1-0.20220831135524-f7fd3700af2e
github.com/alphadose/haxmap v0.3.1
github.com/cornelk/hashmap v1.0.7-0.20220831150614-2c244e4098a0
)
2 changes: 2 additions & 0 deletions benchmarks/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ github.com/alphadose/haxmap v0.3.1-0.20220831034947-0d601bb44159 h1:Vy5DvT2YgH2N
github.com/alphadose/haxmap v0.3.1-0.20220831034947-0d601bb44159/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A=
github.com/alphadose/haxmap v0.3.1-0.20220831135524-f7fd3700af2e h1:wNcWJlc0StruM5i6yPyrDYQDO3pxrxm1b/U2boezeVI=
github.com/alphadose/haxmap v0.3.1-0.20220831135524-f7fd3700af2e/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A=
github.com/alphadose/haxmap v0.3.1 h1:P02INS0xIwY0R3E+KRJTPtrlwTvRPYoU2GgoeRk2KbU=
github.com/alphadose/haxmap v0.3.1/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A=
6 changes: 3 additions & 3 deletions defines.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const defaultSize = 8
// maxFillRate is the maximum fill rate for the slice before a resize will happen.
const maxFillRate = 50

// support all numeric types and types whose underlying type is also numeric.
type numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64
// support all numeric and string types and aliases of those.
type hashable interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
}
4 changes: 2 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
// TestAPICounter shows how to use the hashmap to count REST server API calls.
func TestAPICounter(t *testing.T) {
t.Parallel()
m := NewString[string, *int64]()
m := New[string, *int64]()

for i := 0; i < 100; i++ {
s := fmt.Sprintf("/api%d/", i%4)
Expand All @@ -28,7 +28,7 @@ func TestAPICounter(t *testing.T) {
}

func TestExample(t *testing.T) {
m := NewString[string, int]()
m := New[string, int]()
m.Set("amount", 123)
value, ok := m.Get("amount")
assert.True(t, ok)
Expand Down
6 changes: 3 additions & 3 deletions hashmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// Map implements a read optimized hash map.
type Map[Key numeric, Value any] struct {
type Map[Key hashable, Value any] struct {
hasher func(Key) uintptr
store atomic.Pointer[store[Key, Value]] // pointer to a map instance that gets replaced if the map resizes
linkedList *List[Key, Value] // key sorted linked list of elements
Expand All @@ -21,12 +21,12 @@ type Map[Key numeric, Value any] struct {
}

// New returns a new map instance.
func New[Key numeric, Value any]() *Map[Key, Value] {
func New[Key hashable, Value any]() *Map[Key, Value] {
return NewSized[Key, Value](defaultSize)
}

// NewSized returns a new map instance with a specific initialization size.
func NewSized[Key numeric, Value any](size uintptr) *Map[Key, Value] {
func NewSized[Key hashable, Value any](size uintptr) *Map[Key, Value] {
m := &Map[Key, Value]{}
m.allocate(size)
m.setDefaultHasher()
Expand Down
Loading

0 comments on commit 199c5e7

Please sign in to comment.