Skip to content

Commit 939a36e

Browse files
authored
Merge pull request #1 from lispad/feature/add_sharded_lock_map
Add sharded rwlock map
2 parents 0005070 + df02a1b commit 939a36e

File tree

10 files changed

+759
-61
lines changed

10 files changed

+759
-61
lines changed

README.md

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,19 @@
1-
# Heap structure, using go generics
2-
[![Go Report Card](https://goreportcard.com/badge/github.com/lispad/go-generics-tools)](https://goreportcard.com/report/github.com/lispad/go-generics-tools)
1+
# GoLang Generics tools: Heap structure, sharded rw-locked map.
2+
[![Go Report Card](https://goreportcard.com/badge/github.com/lispad/go-generics-tools)](https://goreportcard.com/report/github.com/lispad/go-generics-tools)
33
[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
44

55
Introduction
66
------------
77

8-
The Heap package contains simple [binary heap](https://en.wikipedia.org/wiki/Binary_heap) implementation, using Golang
8+
The [Heap](binheap/README.md) package contains simple [binary heap](https://en.wikipedia.org/wiki/Binary_heap) implementation, using Golang
99
generics. There are several heap implementations
10+
[Details](binheap/README.md).
1011

11-
- generic Heap implementation, that could be used for `any` type,
12-
- `ComparableHeap` for [comparable](https://go.dev/ref/spec#Comparison_operators) types. Additional `Search`
13-
and `Delete` are implemented,
14-
- for [`constraints.Ordered`](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered) there are
15-
constructors for min, max heaps;
1612

17-
Also use-cases provided:
18-
19-
- `TopN` that allows getting N top elements from slice.
20-
`TopN` swaps top N elements to first N elements of slice, no additional allocations are done. All slice elements are
21-
kept, only order is changed.
22-
- `TopNHeap` allows to get N top, pushing elements from stream without allocation slice for all elements. Only O(N)
23-
memory is used.
24-
- `TopNImmutable` allocated new slice for heap, input slice is not mutated.
25-
26-
Both TopN and TopNImmutable has methods for creating min and max tops for `constraints.Ordered`.
27-
28-
Usage Example
29-
-----------------
30-
31-
package main
32-
33-
import (
34-
"fmt"
35-
36-
"github.com/lispad/go-generics-tools/binheap"
37-
)
38-
39-
func main() {
40-
someData := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
41-
mins := binheap.MinN[float64](someData, 3)
42-
fmt.Printf("--- top 3 min elements: %v\n", mins)
43-
maxs := binheap.MaxN[float64](someData, 3)
44-
fmt.Printf("--- top 3 max elements: %v\n\n", maxs)
45-
46-
heap := binheap.EmptyMaxHeap[string]()
47-
heap.Push("foo")
48-
heap.Push("zzz")
49-
heap.Push("bar")
50-
heap.Push("baz")
51-
heap.Push("foobar")
52-
heap.Push("foobaz")
53-
fmt.Printf("--- heap has %d elements, max element:\n%s\n\n", heap.Len(), heap.Peak())
54-
}
55-
56-
A bit more examples could be found in `examples` directory
57-
58-
Benchmark
59-
-----------------
60-
Theoretical complexity for getting TopN from slice with size M, N <= M: O(N*ln(M)). When N << M, the heap-based TopN
61-
could be much faster than sorting slice and getting top. E.g. For top-3 from 10k elements approach is ln(10^5)/ln(3) ~=
62-
8.38 times faster.
63-
64-
#### Benchmark
65-
66-
BenchmarkSortedMaxN-8 10303648 136.0 ns/op 0 B/op 0 allocs/op
67-
BenchmarkMaxNImmutable-8 398996316 3.029 ns/op 0 B/op 0 allocs/op
68-
BenchmarkMaxN-8 804041455 1.819 ns/op 0 B/op 0 allocs/op
13+
The [ShardedLockMap](smap/README.md) package contains implementation of sharded lock map.
14+
Interface is similar to sync.map, but sharded lock map is faster on scenarios with huge read load with rare updates,
15+
and uses less memory, doing less allocations.
16+
[Details](smap/README.md)
6917

7018
Compatibility
7119
-------------
@@ -77,6 +25,10 @@ Installation
7725
To install package, run:
7826

7927
go get github.com/lispad/go-generics-tools/binheap
28+
or
29+
30+
go get github.com/lispad/go-generics-tools/smap
31+
8032

8133
License
8234
-------

binheap/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Heap structure, using go generics
2+
[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
3+
4+
Introduction
5+
------------
6+
7+
The Heap package contains simple [binary heap](https://en.wikipedia.org/wiki/Binary_heap) implementation, using Golang
8+
generics. There are several heap implementations
9+
10+
- generic Heap implementation, that could be used for `any` type,
11+
- `ComparableHeap` for [comparable](https://go.dev/ref/spec#Comparison_operators) types. Additional `Search`
12+
and `Delete` are implemented,
13+
- for [`constraints.Ordered`](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered) there are
14+
constructors for min, max heaps;
15+
16+
Also use-cases provided:
17+
18+
- `TopN` that allows getting N top elements from slice.
19+
`TopN` swaps top N elements to first N elements of slice, no additional allocations are done. All slice elements are
20+
kept, only order is changed.
21+
- `TopNHeap` allows to get N top, pushing elements from stream without allocation slice for all elements. Only O(N)
22+
memory is used.
23+
- `TopNImmutable` allocated new slice for heap, input slice is not mutated.
24+
25+
Both TopN and TopNImmutable has methods for creating min and max tops for `constraints.Ordered`.
26+
27+
Usage Example
28+
-----------------
29+
30+
package main
31+
32+
import (
33+
"fmt"
34+
35+
"github.com/lispad/go-generics-tools/binheap"
36+
)
37+
38+
func main() {
39+
someData := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
40+
mins := binheap.MinN[float64](someData, 3)
41+
fmt.Printf("--- top 3 min elements: %v\n", mins)
42+
maxs := binheap.MaxN[float64](someData, 3)
43+
fmt.Printf("--- top 3 max elements: %v\n\n", maxs)
44+
45+
heap := binheap.EmptyMaxHeap[string]()
46+
heap.Push("foo")
47+
heap.Push("zzz")
48+
heap.Push("bar")
49+
heap.Push("baz")
50+
heap.Push("foobar")
51+
heap.Push("foobaz")
52+
fmt.Printf("--- heap has %d elements, max element:\n%s\n\n", heap.Len(), heap.Peak())
53+
}
54+
55+
A bit more examples could be found in `examples` directory
56+
57+
Benchmark
58+
-----------------
59+
Theoretical complexity for getting TopN from slice with size M, N <= M: O(N*ln(M)). When N << M, the heap-based TopN
60+
could be much faster than sorting slice and getting top. E.g. For top-3 from 10k elements approach is ln(10^5)/ln(3) ~=
61+
8.38 times faster.
62+
63+
#### Benchmark
64+
65+
BenchmarkSortedMaxN-8 10303648 136.0 ns/op 0 B/op 0 allocs/op
66+
BenchmarkMaxNImmutable-8 398996316 3.029 ns/op 0 B/op 0 allocs/op
67+
BenchmarkMaxN-8 804041455 1.819 ns/op 0 B/op 0 allocs/op
68+
69+
Compatibility
70+
-------------
71+
Minimal Golang version is 1.18. Generics and fuzz testing are used.
72+
73+
Installation
74+
----------------------
75+
76+
To install package, run:
77+
78+
go get github.com/lispad/go-generics-tools/binheap
79+
80+
License
81+
-------
82+
83+
The binheap package is licensed under the MIT license. Please see the LICENSE file for details.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.18
44

55
require (
66
github.com/stretchr/testify v1.7.1
7-
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb
7+
golang.org/x/exp v0.0.0-20220609121020-a51bd0440498
88
)
99

1010
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
77
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
88
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb h1:pC9Okm6BVmxEw76PUu0XUbOTQ92JX11hfvqTjAV3qxM=
99
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
10+
golang.org/x/exp v0.0.0-20220609121020-a51bd0440498 h1:TF0FvLUGEq/8wOt/9AV1nj6D4ViZGUIGCMQfCv7VRXY=
11+
golang.org/x/exp v0.0.0-20220609121020-a51bd0440498/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
1012
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1113
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1214
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

smap/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Sharded RWLocked Map, using go generics
2+
[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
3+
4+
Introduction
5+
------------
6+
7+
In some scenarios sync.Map could allocate large space for dirty-read copies, or even heavily use locks (when getting
8+
missing values, that should be rechecked in map with lock). In such scenarios go internal map with rw-mutex, divided to
9+
several shards could perform much better, with cost of memory for additional locks storage. But this amount could be
10+
much less, than sync.map uses.
11+
12+
Interface incompatibility
13+
------------
14+
15+
- `LoadOrStore` method changed to `LoadOrCreate`, with callback that generates value. Could be used to avoid
16+
unnecessary creating huge values, in case if key already exists.
17+
18+
Usage Example
19+
-----------------
20+
21+
package main
22+
23+
import (
24+
"fmt"
25+
26+
"github.com/lispad/go-generics-tools/smap"
27+
)
28+
29+
func main() {
30+
m := NewIntegerComparable[int, int](8, 128)
31+
m.Store(123, 456)
32+
33+
value, ok := m.Load(123)
34+
fmt.Printf("%d, %t", value, ok)
35+
}
36+
37+
A bit more examples could be found in tests.
38+
39+
Benchmark
40+
-----------------
41+
42+
43+
#### Benchmark
44+
45+
Benchmark performed on Lenovo Ideapad laptop with AMD Ryzen 7 4700U, Linux Mint 20.3 with 5.13.0 kernel
46+
47+
BenchmarkIntegerSMap_ConcurrentGet-8 82540485 12.95 ns/op 0 B/op 0 allocs/op
48+
BenchmarkSyncMap_ConcurrentGet-8 73431339 18.35 ns/op 0 B/op 0 allocs/op
49+
BenchmarkLockMap_ConcurrentGet-8 19327282 54.03 ns/op 0 B/op 0 allocs/op
50+
BenchmarkIntegerShardedMap_ConcurrentSet-8 25605380 42.33 ns/op 0 B/op 0 allocs/op
51+
BenchmarkSyncMap_ConcurrentSet-8 2138496 536.1 ns/op 36 B/op 3 allocs/op
52+
BenchmarkLockMap_ConcurrentSet-8 3827476 302.9 ns/op 0 B/op 0 allocs/op
53+
BenchmarkIntegerShardedMap_ConcurrentGetSet5-8 3377473 357.1 ns/op 0 B/op 0 allocs/op
54+
BenchmarkSyncMap_ConcurrentGetSet5-8 323318 3540 ns/op 266 B/op 3 allocs/op
55+
BenchmarkLockMap_ConcurrentGetSet5-8 204633 6176 ns/op 0 B/op 0 allocs/op
56+
BenchmarkIntegerShardedMap_ConcurrentGetSet50-8 18236305 65.03 ns/op 0 B/op 0 allocs/op
57+
BenchmarkSyncMap_ConcurrentGetSet50-8 18337423 56.55 ns/op 32 B/op 2 allocs/op
58+
BenchmarkLockMap_ConcurrentGetSet50-8 2697315 431.2 ns/op 0 B/op 0 allocs/op
59+
BenchmarkIntegerShardedMap_ConcurrentGetSet1-8 1003506 1076 ns/op 0 B/op 0 allocs/op
60+
BenchmarkSyncMap_ConcurrentGetSet1-8 32668 44423 ns/op 3508 B/op 5 allocs/op
61+
BenchmarkLockMap_ConcurrentGetSet1-8 78486 16019 ns/op 0 B/op 0 allocs/op
62+
63+
Sharded Lock map is approximately equal to sync.Map on 50% read + 50% concurrent writes, and is much faster on
64+
5% writes+95% reads, and 1% writes+99% reads.
65+
Also sharded map allocated about 40x less memory in 5% writes+95% reads scenario, than sync.Map does.
66+
67+
Compatibility
68+
-------------
69+
Minimal Golang version is 1.18. Generics are used.
70+
71+
Installation
72+
----------------------
73+
74+
To install package, run:
75+
76+
go get github.com/lispad/go-generics-tools/smap
77+
78+
License
79+
-------
80+
81+
The smap package is licensed under the MIT license. Please see the LICENSE file for details.

smap/comparable.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package smap
2+
3+
// GenericComparable stores data in N shards, with rw mutex for each.
4+
// Additional CompareAndSwap method added for comparable values.
5+
type GenericComparable[K comparable, V comparable] struct {
6+
Generic[K, V]
7+
}
8+
9+
// NewGenericComparable creates generic RWLocked Sharded map for comparable values.
10+
// shardDetector should be idempotent function.
11+
func NewGenericComparable[K comparable, V comparable](shardsCount, defaultSize int, shardDetector func(key K) int) GenericComparable[K, V] {
12+
return GenericComparable[K, V]{
13+
Generic: NewGeneric[K, V](shardsCount, defaultSize, shardDetector),
14+
}
15+
}
16+
17+
// CompareAndSwap executes the compare-and-swap operation for the Key & Value pair.
18+
// If and only if key exists, and value for key equals old, value will be changed to new.
19+
// Otherwise, returns current value.
20+
// The ok result indicates whether value was changed to new in the map.
21+
func (sm GenericComparable[K, V]) CompareAndSwap(key K, old, new V) (V, bool) {
22+
shardID := sm.shardDetector(key)
23+
sm.locks[shardID].Lock()
24+
if current, ok := sm.shards[shardID][key]; ok && current == old {
25+
sm.shards[shardID][key] = new
26+
sm.locks[shardID].Unlock()
27+
return new, true
28+
} else {
29+
sm.locks[shardID].Unlock()
30+
return current, false
31+
}
32+
}

0 commit comments

Comments
 (0)