Skip to content

Commit 22a4f50

Browse files
mergify[bot]ValarDragonrobert-zaremba
authored
perf: Make CacheKV store interleaved iterator and insertion not O(n^2) (backport #10026) (#10167)
* perf: Make CacheKV store interleaved iterator and insertion not O(n^2) (#10026) (cherry picked from commit 28bf2c1) # Conflicts: # CHANGELOG.md # store/cachekv/store.go * fix changelog conflict * Update store.go Co-authored-by: Dev Ojha <[email protected]> Co-authored-by: Robert Zaremba <[email protected]>
1 parent b4d7c1f commit 22a4f50

File tree

3 files changed

+79
-116
lines changed

3 files changed

+79
-116
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ Ref: https://keepachangelog.com/en/1.0.0/
3838
## [Unreleased]
3939

4040
### Improvements
41+
4142
* (types) [\#10021](https://github.com/cosmos/cosmos-sdk/pull/10021) Speedup coins.AmountOf(), by removing many intermittent regex calls.
43+
* (store) [\#10026](https://github.com/cosmos/cosmos-sdk/pull/10026) Improve CacheKVStore datastructures / algorithms, to no longer take O(N^2) time when interleaving iterators and insertions.
4244

4345
### Bug Fixes
4446

store/cachekv/memiterator.go

Lines changed: 22 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,50 @@
11
package cachekv
22

33
import (
4-
"errors"
5-
64
dbm "github.com/tendermint/tm-db"
75

8-
"github.com/cosmos/cosmos-sdk/types/kv"
6+
"github.com/cosmos/cosmos-sdk/store/types"
97
)
108

119
// Iterates over iterKVCache items.
1210
// if key is nil, means it was deleted.
1311
// Implements Iterator.
1412
type memIterator struct {
15-
start, end []byte
16-
items []*kv.Pair
17-
ascending bool
18-
}
19-
20-
func newMemIterator(start, end []byte, items *kv.List, ascending bool) *memIterator {
21-
itemsInDomain := make([]*kv.Pair, 0, items.Len())
22-
23-
var entered bool
24-
25-
for e := items.Front(); e != nil; e = e.Next() {
26-
item := e.Value
27-
if !dbm.IsKeyInDomain(item.Key, start, end) {
28-
if entered {
29-
break
30-
}
31-
32-
continue
33-
}
34-
35-
itemsInDomain = append(itemsInDomain, item)
36-
entered = true
37-
}
13+
types.Iterator
3814

39-
return &memIterator{
40-
start: start,
41-
end: end,
42-
items: itemsInDomain,
43-
ascending: ascending,
44-
}
15+
deleted map[string]struct{}
4516
}
4617

47-
func (mi *memIterator) Domain() ([]byte, []byte) {
48-
return mi.start, mi.end
49-
}
18+
func newMemIterator(start, end []byte, items *dbm.MemDB, deleted map[string]struct{}, ascending bool) *memIterator {
19+
var iter types.Iterator
20+
var err error
5021

51-
func (mi *memIterator) Valid() bool {
52-
return len(mi.items) > 0
53-
}
22+
if ascending {
23+
iter, err = items.Iterator(start, end)
24+
} else {
25+
iter, err = items.ReverseIterator(start, end)
26+
}
5427

55-
func (mi *memIterator) assertValid() {
56-
if err := mi.Error(); err != nil {
28+
if err != nil {
5729
panic(err)
5830
}
59-
}
6031

61-
func (mi *memIterator) Next() {
62-
mi.assertValid()
63-
64-
if mi.ascending {
65-
mi.items = mi.items[1:]
66-
} else {
67-
mi.items = mi.items[:len(mi.items)-1]
32+
newDeleted := make(map[string]struct{})
33+
for k, v := range deleted {
34+
newDeleted[k] = v
6835
}
69-
}
7036

71-
func (mi *memIterator) Key() []byte {
72-
mi.assertValid()
37+
return &memIterator{
38+
Iterator: iter,
7339

74-
if mi.ascending {
75-
return mi.items[0].Key
40+
deleted: newDeleted,
7641
}
77-
78-
return mi.items[len(mi.items)-1].Key
7942
}
8043

8144
func (mi *memIterator) Value() []byte {
82-
mi.assertValid()
83-
84-
if mi.ascending {
85-
return mi.items[0].Value
86-
}
87-
88-
return mi.items[len(mi.items)-1].Value
89-
}
90-
91-
func (mi *memIterator) Close() error {
92-
mi.start = nil
93-
mi.end = nil
94-
mi.items = nil
95-
96-
return nil
97-
}
98-
99-
// Error returns an error if the memIterator is invalid defined by the Valid
100-
// method.
101-
func (mi *memIterator) Error() error {
102-
if !mi.Valid() {
103-
return errors.New("invalid memIterator")
45+
key := mi.Iterator.Key()
46+
if _, ok := mi.deleted[string(key)]; ok {
47+
return nil
10448
}
105-
106-
return nil
49+
return mi.Iterator.Value()
10750
}

store/cachekv/store.go

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ import (
2020
// If value is nil but deleted is false, it means the parent doesn't have the
2121
// key. (No need to delete upon Write())
2222
type cValue struct {
23-
value []byte
24-
deleted bool
25-
dirty bool
23+
value []byte
24+
dirty bool
2625
}
2726

2827
// Store wraps an in-memory cache around an underlying types.KVStore.
2928
type Store struct {
3029
mtx sync.Mutex
3130
cache map[string]*cValue
31+
deleted map[string]struct{}
3232
unsortedCache map[string]struct{}
33-
sortedCache *kv.List // always ascending sorted
33+
sortedCache *dbm.MemDB // always ascending sorted
3434
parent types.KVStore
3535
}
3636

@@ -40,8 +40,9 @@ var _ types.CacheKVStore = (*Store)(nil)
4040
func NewStore(parent types.KVStore) *Store {
4141
return &Store{
4242
cache: make(map[string]*cValue),
43+
deleted: make(map[string]struct{}),
4344
unsortedCache: make(map[string]struct{}),
44-
sortedCache: kv.NewList(),
45+
sortedCache: dbm.NewMemDB(),
4546
parent: parent,
4647
}
4748
}
@@ -122,7 +123,7 @@ func (store *Store) Write() {
122123
cacheValue := store.cache[key]
123124

124125
switch {
125-
case cacheValue.deleted:
126+
case store.isDeleted(key):
126127
store.parent.Delete([]byte(key))
127128
case cacheValue.value == nil:
128129
// Skip, it already doesn't exist in parent.
@@ -133,8 +134,9 @@ func (store *Store) Write() {
133134

134135
// Clear the cache
135136
store.cache = make(map[string]*cValue)
137+
store.deleted = make(map[string]struct{})
136138
store.unsortedCache = make(map[string]struct{})
137-
store.sortedCache = kv.NewList()
139+
store.sortedCache = dbm.NewMemDB()
138140
}
139141

140142
// CacheWrap implements CacheWrapper.
@@ -178,23 +180,40 @@ func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator {
178180
}
179181

180182
store.dirtyItems(start, end)
181-
cache = newMemIterator(start, end, store.sortedCache, ascending)
183+
cache = newMemIterator(start, end, store.sortedCache, store.deleted, ascending)
182184

183185
return newCacheMergeIterator(parent, cache, ascending)
184186
}
185187

186188
// Constructs a slice of dirty items, to use w/ memIterator.
187189
func (store *Store) dirtyItems(start, end []byte) {
188-
unsorted := make([]*kv.Pair, 0)
189-
190190
n := len(store.unsortedCache)
191-
for key := range store.unsortedCache {
192-
if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) {
191+
unsorted := make([]*kv.Pair, 0)
192+
// If the unsortedCache is too big, its costs too much to determine
193+
// whats in the subset we are concerned about.
194+
// If you are interleaving iterator calls with writes, this can easily become an
195+
// O(N^2) overhead.
196+
// Even without that, too many range checks eventually becomes more expensive
197+
// than just not having the cache.
198+
if n >= 1024 {
199+
for key := range store.unsortedCache {
193200
cacheValue := store.cache[key]
194201
unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value})
195202
}
203+
} else {
204+
// else do a linear scan to determine if the unsorted pairs are in the pool.
205+
for key := range store.unsortedCache {
206+
if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) {
207+
cacheValue := store.cache[key]
208+
unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value})
209+
}
210+
}
196211
}
212+
store.clearUnsortedCacheSubset(unsorted)
213+
}
197214

215+
func (store *Store) clearUnsortedCacheSubset(unsorted []*kv.Pair) {
216+
n := len(store.unsortedCache)
198217
if len(unsorted) == n { // This pattern allows the Go compiler to emit the map clearing idiom for the entire map.
199218
for key := range store.unsortedCache {
200219
delete(store.unsortedCache, key)
@@ -204,32 +223,21 @@ func (store *Store) dirtyItems(start, end []byte) {
204223
delete(store.unsortedCache, conv.UnsafeBytesToStr(kv.Key))
205224
}
206225
}
207-
208226
sort.Slice(unsorted, func(i, j int) bool {
209227
return bytes.Compare(unsorted[i].Key, unsorted[j].Key) < 0
210228
})
211229

212-
for e := store.sortedCache.Front(); e != nil && len(unsorted) != 0; {
213-
uitem := unsorted[0]
214-
sitem := e.Value
215-
comp := bytes.Compare(uitem.Key, sitem.Key)
216-
217-
switch comp {
218-
case -1:
219-
unsorted = unsorted[1:]
220-
221-
store.sortedCache.InsertBefore(uitem, e)
222-
case 1:
223-
e = e.Next()
224-
case 0:
225-
unsorted = unsorted[1:]
226-
e.Value = uitem
227-
e = e.Next()
230+
for _, item := range unsorted {
231+
if item.Value == nil {
232+
// deleted element, tracked by store.deleted
233+
// setting arbitrary value
234+
store.sortedCache.Set(item.Key, []byte{})
235+
continue
236+
}
237+
err := store.sortedCache.Set(item.Key, item.Value)
238+
if err != nil {
239+
panic(err)
228240
}
229-
}
230-
231-
for _, kvp := range unsorted {
232-
store.sortedCache.PushBack(kvp)
233241
}
234242
}
235243

@@ -238,12 +246,22 @@ func (store *Store) dirtyItems(start, end []byte) {
238246

239247
// Only entrypoint to mutate store.cache.
240248
func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) {
241-
store.cache[conv.UnsafeBytesToStr(key)] = &cValue{
242-
value: value,
243-
deleted: deleted,
244-
dirty: dirty,
249+
keyStr := conv.UnsafeBytesToStr(key)
250+
store.cache[keyStr] = &cValue{
251+
value: value,
252+
dirty: dirty,
253+
}
254+
if deleted {
255+
store.deleted[keyStr] = struct{}{}
256+
} else {
257+
delete(store.deleted, keyStr)
245258
}
246259
if dirty {
247260
store.unsortedCache[conv.UnsafeBytesToStr(key)] = struct{}{}
248261
}
249262
}
263+
264+
func (store *Store) isDeleted(key string) bool {
265+
_, ok := store.deleted[key]
266+
return ok
267+
}

0 commit comments

Comments
 (0)