diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b61ffb61f7..baeb39a8f979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#12187](https://github.com/cosmos/cosmos-sdk/pull/12187) Add batch operation for x/nft module. * [#12693](https://github.com/cosmos/cosmos-sdk/pull/12693) Make sure the order of each node is consistent when emitting proto events. * [#12455](https://github.com/cosmos/cosmos-sdk/pull/12455) Show attempts count in error for signing. +* [#12886](https://github.com/cosmos/cosmos-sdk/pull/12886) Amortize cost of processing cache KV store ### State Machine Breaking diff --git a/store/cachekv/search_benchmark_test.go b/store/cachekv/search_benchmark_test.go new file mode 100644 index 000000000000..d7f1dcb8d4f1 --- /dev/null +++ b/store/cachekv/search_benchmark_test.go @@ -0,0 +1,43 @@ +package cachekv + +import ( + db "github.com/tendermint/tm-db" + "strconv" + "testing" +) + +func BenchmarkLargeUnsortedMisses(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + store := generateStore() + b.StartTimer() + + for k := 0; k < 10000; k++ { + // cache has A + Z values + // these are within range, but match nothing + store.dirtyItems([]byte("B1"), []byte("B2")) + } + } +} + +func generateStore() *Store { + cache := map[string]*cValue{} + unsorted := map[string]struct{}{} + for i := 0; i < 5000; i++ { + key := "A" + strconv.Itoa(i) + unsorted[key] = struct{}{} + cache[key] = &cValue{} + } + + for i := 0; i < 5000; i++ { + key := "Z" + strconv.Itoa(i) + unsorted[key] = struct{}{} + cache[key] = &cValue{} + } + + return &Store{ + cache: cache, + unsortedCache: unsorted, + sortedCache: db.NewMemDB(), + } +} diff --git a/store/cachekv/store.go b/store/cachekv/store.go index 29e297f4cf67..bd467c418f91 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -13,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/tracekv" "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/tendermint/tendermint/libs/math" ) // cValue represents a cached value. @@ -278,6 +279,8 @@ const ( stateAlreadySorted ) +const minSortSize = 1024 + // Constructs a slice of dirty items, to use w/ memIterator. func (store *Store) dirtyItems(start, end []byte) { startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end) @@ -294,7 +297,7 @@ func (store *Store) dirtyItems(start, end []byte) { // O(N^2) overhead. // Even without that, too many range checks eventually becomes more expensive // than just not having the cache. - if n < 1024 { + if n < minSortSize { for key := range store.unsortedCache { // dbm.IsKeyInDomain is nil safe and returns true iff key is greater than start if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) { @@ -331,6 +334,17 @@ func (store *Store) dirtyItems(start, end []byte) { endIndex = len(strL) - 1 } + // Since we spent cycles to sort the values, we should process and remove a reasonable amount + // ensure start to end is at least minSortSize in size + // if below minSortSize, expand it to cover additional values + // this amortizes the cost of processing elements across multiple calls + if endIndex-startIndex < minSortSize { + endIndex = math.MinInt(startIndex+minSortSize, len(strL)-1) + if endIndex-startIndex < minSortSize { + startIndex = math.MaxInt(endIndex-minSortSize, 0) + } + } + kvL := make([]*kv.Pair, 0) for i := startIndex; i <= endIndex; i++ { key := strL[i]