diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0d2a8d650e..2cbe04cdda50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +* [#12886](https://github.com/cosmos/cosmos-sdk/pull/12886) Amortize cost of processing cache KV store. * (events) [#12850](https://github.com/cosmos/cosmos-sdk/pull/12850) Add a new `fee_payer` attribute to the `tx` event that is emitted from the `DeductFeeDecorator` AnteHandler decorator. * (x/params) [#12615](https://github.com/cosmos/cosmos-sdk/pull/12615) Add `GetParamSetIfExists` function to params `Subspace` to prevent panics on breaking changes. * (x/bank) [#12674](https://github.com/cosmos/cosmos-sdk/pull/12674) Add convenience function `CreatePrefixedAccountStoreKey()` to construct key to access account's balance for a given denom. 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 28063504b208..a70becf13587 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. @@ -273,6 +274,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) @@ -289,7 +292,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 { if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) { cacheValue := store.cache[key] @@ -320,6 +323,17 @@ func (store *Store) dirtyItems(start, end []byte) { startIndex = 0 } + // 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]