2019-02-01 17:03:09 -08:00
|
|
|
package cachekv
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"io"
|
|
|
|
"sort"
|
|
|
|
"sync"
|
|
|
|
|
2019-08-02 06:20:39 -07:00
|
|
|
dbm "github.com/tendermint/tm-db"
|
2019-02-01 17:03:09 -08:00
|
|
|
|
2021-03-01 07:10:22 -08:00
|
|
|
"github.com/cosmos/cosmos-sdk/internal/conv"
|
2021-03-30 13:13:51 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/store/listenkv"
|
2019-02-01 17:03:09 -08:00
|
|
|
"github.com/cosmos/cosmos-sdk/store/tracekv"
|
2020-01-16 13:46:51 -08:00
|
|
|
"github.com/cosmos/cosmos-sdk/store/types"
|
2020-07-30 07:53:02 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/types/kv"
|
2019-02-01 17:03:09 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
// If value is nil but deleted is false, it means the parent doesn't have the
|
|
|
|
// key. (No need to delete upon Write())
|
|
|
|
type cValue struct {
|
2021-09-10 12:42:56 -07:00
|
|
|
value []byte
|
|
|
|
dirty bool
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Store wraps an in-memory cache around an underlying types.KVStore.
|
|
|
|
type Store struct {
|
2019-05-15 07:42:06 -07:00
|
|
|
mtx sync.Mutex
|
|
|
|
cache map[string]*cValue
|
2021-09-10 12:42:56 -07:00
|
|
|
deleted map[string]struct{}
|
2019-05-15 07:42:06 -07:00
|
|
|
unsortedCache map[string]struct{}
|
2021-09-10 12:42:56 -07:00
|
|
|
sortedCache *dbm.MemDB // always ascending sorted
|
2019-05-15 07:42:06 -07:00
|
|
|
parent types.KVStore
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
var _ types.CacheKVStore = (*Store)(nil)
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// NewStore creates a new Store object
|
2019-02-01 17:03:09 -08:00
|
|
|
func NewStore(parent types.KVStore) *Store {
|
|
|
|
return &Store{
|
2019-05-15 07:42:06 -07:00
|
|
|
cache: make(map[string]*cValue),
|
2021-09-10 12:42:56 -07:00
|
|
|
deleted: make(map[string]struct{}),
|
2019-05-15 07:42:06 -07:00
|
|
|
unsortedCache: make(map[string]struct{}),
|
2021-09-10 12:42:56 -07:00
|
|
|
sortedCache: dbm.NewMemDB(),
|
2019-05-15 07:42:06 -07:00
|
|
|
parent: parent,
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// GetStoreType implements Store.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) GetStoreType() types.StoreType {
|
|
|
|
return store.parent.GetStoreType()
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// Get implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) Get(key []byte) (value []byte) {
|
|
|
|
store.mtx.Lock()
|
|
|
|
defer store.mtx.Unlock()
|
2019-09-04 10:33:32 -07:00
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
types.AssertValidKey(key)
|
|
|
|
|
2021-05-06 06:33:01 -07:00
|
|
|
cacheValue, ok := store.cache[conv.UnsafeBytesToStr(key)]
|
2019-02-01 17:03:09 -08:00
|
|
|
if !ok {
|
|
|
|
value = store.parent.Get(key)
|
|
|
|
store.setCacheValue(key, value, false, false)
|
|
|
|
} else {
|
|
|
|
value = cacheValue.value
|
|
|
|
}
|
|
|
|
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// Set implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) Set(key []byte, value []byte) {
|
|
|
|
store.mtx.Lock()
|
|
|
|
defer store.mtx.Unlock()
|
2019-09-04 10:33:32 -07:00
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
types.AssertValidKey(key)
|
|
|
|
types.AssertValidValue(value)
|
|
|
|
|
|
|
|
store.setCacheValue(key, value, false, true)
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// Has implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) Has(key []byte) bool {
|
|
|
|
value := store.Get(key)
|
|
|
|
return value != nil
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// Delete implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) Delete(key []byte) {
|
|
|
|
store.mtx.Lock()
|
|
|
|
defer store.mtx.Unlock()
|
2019-09-04 10:33:32 -07:00
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
types.AssertValidKey(key)
|
|
|
|
store.setCacheValue(key, nil, true, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implements Cachetypes.KVStore.
|
|
|
|
func (store *Store) Write() {
|
|
|
|
store.mtx.Lock()
|
|
|
|
defer store.mtx.Unlock()
|
|
|
|
|
|
|
|
// We need a copy of all of the keys.
|
|
|
|
// Not the best, but probably not a bottleneck depending.
|
|
|
|
keys := make([]string, 0, len(store.cache))
|
2020-04-29 19:36:34 -07:00
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
for key, dbValue := range store.cache {
|
|
|
|
if dbValue.dirty {
|
|
|
|
keys = append(keys, key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
// TODO: Consider allowing usage of Batch, which would allow the write to
|
|
|
|
// at least happen atomically.
|
|
|
|
for _, key := range keys {
|
|
|
|
cacheValue := store.cache[key]
|
2020-04-29 19:36:34 -07:00
|
|
|
|
2019-08-19 09:06:27 -07:00
|
|
|
switch {
|
2021-09-10 12:42:56 -07:00
|
|
|
case store.isDeleted(key):
|
2019-02-01 17:03:09 -08:00
|
|
|
store.parent.Delete([]byte(key))
|
2019-08-19 09:06:27 -07:00
|
|
|
case cacheValue.value == nil:
|
2019-02-01 17:03:09 -08:00
|
|
|
// Skip, it already doesn't exist in parent.
|
2019-08-19 09:06:27 -07:00
|
|
|
default:
|
2019-02-01 17:03:09 -08:00
|
|
|
store.parent.Set([]byte(key), cacheValue.value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear the cache
|
2019-05-15 07:42:06 -07:00
|
|
|
store.cache = make(map[string]*cValue)
|
2021-09-10 12:42:56 -07:00
|
|
|
store.deleted = make(map[string]struct{})
|
2019-05-15 07:42:06 -07:00
|
|
|
store.unsortedCache = make(map[string]struct{})
|
2021-09-10 12:42:56 -07:00
|
|
|
store.sortedCache = dbm.NewMemDB()
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// CacheWrap implements CacheWrapper.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) CacheWrap() types.CacheWrap {
|
|
|
|
return NewStore(store)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CacheWrapWithTrace implements the CacheWrapper interface.
|
|
|
|
func (store *Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap {
|
|
|
|
return NewStore(tracekv.NewStore(store, w, tc))
|
|
|
|
}
|
|
|
|
|
2021-03-30 13:13:51 -07:00
|
|
|
// CacheWrapWithListeners implements the CacheWrapper interface.
|
|
|
|
func (store *Store) CacheWrapWithListeners(storeKey types.StoreKey, listeners []types.WriteListener) types.CacheWrap {
|
|
|
|
return NewStore(listenkv.NewStore(store, storeKey, listeners))
|
|
|
|
}
|
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
//----------------------------------------
|
|
|
|
// Iteration
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// Iterator implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) Iterator(start, end []byte) types.Iterator {
|
|
|
|
return store.iterator(start, end, true)
|
|
|
|
}
|
|
|
|
|
2021-01-05 07:57:33 -08:00
|
|
|
// ReverseIterator implements types.KVStore.
|
2019-02-01 17:03:09 -08:00
|
|
|
func (store *Store) ReverseIterator(start, end []byte) types.Iterator {
|
|
|
|
return store.iterator(start, end, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator {
|
2019-05-15 07:42:06 -07:00
|
|
|
store.mtx.Lock()
|
|
|
|
defer store.mtx.Unlock()
|
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
var parent, cache types.Iterator
|
|
|
|
|
|
|
|
if ascending {
|
|
|
|
parent = store.parent.Iterator(start, end)
|
|
|
|
} else {
|
|
|
|
parent = store.parent.ReverseIterator(start, end)
|
|
|
|
}
|
|
|
|
|
2019-05-15 07:42:06 -07:00
|
|
|
store.dirtyItems(start, end)
|
2021-09-10 12:42:56 -07:00
|
|
|
cache = newMemIterator(start, end, store.sortedCache, store.deleted, ascending)
|
2019-02-01 17:03:09 -08:00
|
|
|
|
|
|
|
return newCacheMergeIterator(parent, cache, ascending)
|
|
|
|
}
|
|
|
|
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
func findStartIndex(strL []string, startQ string) int {
|
|
|
|
// Modified binary search to find the very first element in >=startQ.
|
|
|
|
if len(strL) == 0 {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
var left, right, mid int
|
|
|
|
right = len(strL) - 1
|
|
|
|
for left <= right {
|
|
|
|
mid = (left + right) >> 1
|
|
|
|
midStr := strL[mid]
|
|
|
|
if midStr == startQ {
|
|
|
|
// Handle condition where there might be multiple values equal to startQ.
|
|
|
|
// We are looking for the very first value < midStL, that i+1 will be the first
|
|
|
|
// element >= midStr.
|
|
|
|
for i := mid - 1; i >= 0; i-- {
|
|
|
|
if strL[i] != midStr {
|
|
|
|
return i + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if midStr < startQ {
|
|
|
|
left = mid + 1
|
|
|
|
} else { // midStrL > startQ
|
|
|
|
right = mid - 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if left >= 0 && left < len(strL) && strL[left] >= startQ {
|
|
|
|
return left
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
func findEndIndex(strL []string, endQ string) int {
|
|
|
|
if len(strL) == 0 {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Modified binary search to find the very first element <endQ.
|
|
|
|
var left, right, mid int
|
|
|
|
right = len(strL) - 1
|
|
|
|
for left <= right {
|
|
|
|
mid = (left + right) >> 1
|
|
|
|
midStr := strL[mid]
|
|
|
|
if midStr == endQ {
|
|
|
|
// Handle condition where there might be multiple values equal to startQ.
|
|
|
|
// We are looking for the very first value < midStL, that i+1 will be the first
|
|
|
|
// element >= midStr.
|
|
|
|
for i := mid - 1; i >= 0; i-- {
|
|
|
|
if strL[i] < midStr {
|
|
|
|
return i + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if midStr < endQ {
|
|
|
|
left = mid + 1
|
|
|
|
} else { // midStrL > startQ
|
|
|
|
right = mid - 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Binary search failed, now let's find a value less than endQ.
|
|
|
|
for i := right; i >= 0; i-- {
|
|
|
|
if strL[i] < endQ {
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
type sortState int
|
|
|
|
|
|
|
|
const (
|
|
|
|
stateUnsorted sortState = iota
|
|
|
|
stateAlreadySorted
|
|
|
|
)
|
|
|
|
|
2019-02-01 17:03:09 -08:00
|
|
|
// Constructs a slice of dirty items, to use w/ memIterator.
|
2019-05-15 07:42:06 -07:00
|
|
|
func (store *Store) dirtyItems(start, end []byte) {
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end)
|
|
|
|
if startStr > endStr {
|
|
|
|
// Nothing to do here.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-27 07:26:22 -08:00
|
|
|
n := len(store.unsortedCache)
|
2021-09-10 12:42:56 -07:00
|
|
|
unsorted := make([]*kv.Pair, 0)
|
|
|
|
// If the unsortedCache is too big, its costs too much to determine
|
|
|
|
// whats in the subset we are concerned about.
|
|
|
|
// If you are interleaving iterator calls with writes, this can easily become an
|
|
|
|
// O(N^2) overhead.
|
|
|
|
// Even without that, too many range checks eventually becomes more expensive
|
|
|
|
// than just not having the cache.
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
if n < 1024 {
|
2021-09-10 12:42:56 -07:00
|
|
|
for key := range store.unsortedCache {
|
|
|
|
if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) {
|
|
|
|
cacheValue := store.cache[key]
|
|
|
|
unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value})
|
|
|
|
}
|
|
|
|
}
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
store.clearUnsortedCacheSubset(unsorted, stateUnsorted)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise it is large so perform a modified binary search to find
|
|
|
|
// the target ranges for the keys that we should be looking for.
|
|
|
|
strL := make([]string, 0, n)
|
|
|
|
for key := range store.unsortedCache {
|
|
|
|
strL = append(strL, key)
|
2021-02-27 07:26:22 -08:00
|
|
|
}
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
sort.Strings(strL)
|
|
|
|
|
|
|
|
// Now find the values within the domain
|
|
|
|
// [start, end)
|
|
|
|
startIndex := findStartIndex(strL, startStr)
|
|
|
|
endIndex := findEndIndex(strL, endStr)
|
|
|
|
|
|
|
|
if endIndex < 0 {
|
|
|
|
endIndex = len(strL) - 1
|
|
|
|
}
|
|
|
|
if startIndex < 0 {
|
|
|
|
startIndex = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
kvL := make([]*kv.Pair, 0)
|
|
|
|
for i := startIndex; i <= endIndex; i++ {
|
|
|
|
key := strL[i]
|
|
|
|
cacheValue := store.cache[key]
|
|
|
|
kvL = append(kvL, &kv.Pair{Key: []byte(key), Value: cacheValue.value})
|
|
|
|
}
|
|
|
|
|
|
|
|
// kvL was already sorted so pass it in as is.
|
|
|
|
store.clearUnsortedCacheSubset(kvL, stateAlreadySorted)
|
2021-09-10 12:42:56 -07:00
|
|
|
}
|
2020-04-29 19:36:34 -07:00
|
|
|
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
func (store *Store) clearUnsortedCacheSubset(unsorted []*kv.Pair, sortState sortState) {
|
2021-09-10 12:42:56 -07:00
|
|
|
n := len(store.unsortedCache)
|
2021-02-27 07:26:22 -08:00
|
|
|
if len(unsorted) == n { // This pattern allows the Go compiler to emit the map clearing idiom for the entire map.
|
|
|
|
for key := range store.unsortedCache {
|
2019-05-15 07:42:06 -07:00
|
|
|
delete(store.unsortedCache, key)
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
2021-02-27 07:26:22 -08:00
|
|
|
} else { // Otherwise, normally delete the unsorted keys from the map.
|
|
|
|
for _, kv := range unsorted {
|
2021-03-01 07:10:22 -08:00
|
|
|
delete(store.unsortedCache, conv.UnsafeBytesToStr(kv.Key))
|
2021-02-27 07:26:22 -08:00
|
|
|
}
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
fix!: store/cachekv: reduce growth factor for iterator ranging using binary searches (#10024)
This change takes the observation that previous dbm.IsKeyInDomain
which searches for [start, end) was performing too many byteslice
comparisons. Instead we start off by sorting all the values in the
store.unsortedCache, and then apply a modified binary search to
look for values that fall within the domain [start, end)
The procedure involves:
* iterating over all items to build a list of all keys -- O(n)
* invoking sort.Strings immediately, of which
we anyways eventually invoke sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
* invoking modified binary search which is O(log(n)) * 2 ~ O(log(n))
to search for the [start, end) range indices
for a total approximate complexity of:
Best case: O(n) + O(n(log(n))) + O(log(n)) ~= O(nlog(n))
Worst case: O(n) + O(n^2) + O(log(n)) ~= O(n^2)
instead of previously:
* iterating over all the unsorted items and invoking dbm.IsKeyInDomain:
bytes.Compare ~ O(n) + O(n*s*e) where s -- len(start), e -- len(end)
for overall complexity of O(n*s*e)
* invoking sort.Slice(unsorted, ...) which uses
Quicksort -- O(nlog(n)) or O(n^2) worst case
for a total approximate complexity of:
Best case: O(n) + O(n*s*e) + O(nlog(n)) ~= O(n*s*e) ~ O(n^2)
Worst case: O(n) + O(n*s*e) + O(n^2) ~= O(n*s*e) ~ O(n^2)
Ordinarily we'd combine the n*s*e to be n*m, but really the comparisons
between (start & key, end & key) are profound that it makes sense to
keep them as factors. The overall benchmark results vindicate our choice
of isolating the factors (n*s*e)
The benchmarks show that as the number of keys to iterate grows, the
new code grows gracefully in a somewhat linear growth, notice for
CAcheKVStoreIterator*, when we go from:
* 1,000 to 10,000 keys: 120us->1,600us (13X) old vs 95us->900us (9.47X) new
* 50,000 to 100,000 keys: 19ms->100ms (5.3X) old vs 5.5ms->17ms (3X) new
```shell
time/op
GetValidator-8 5.8ms ± 2% 4.7ms ± 1% -17.69% (p=0.000 n=10+10)
OneBankSendTxPerBlock-8 3.2ms ± 2% 2.8ms ± 1% -10.80% (p=0.000 n=7+10)
OneBankMultiSendTxPerBlock-8 3.1ms ± 3% 2.9ms ± 2% -8.36% (p=0.000 n=10+10)
AccountMapperSetAccount-8 8.6µs ± 1% 7.8µs ± 1% -9.74% (p=0.000 n=10+10)
CacheKVStoreIterator500-8 64µs ± 6% 51µs ± 6% -19.22% (p=0.000 n=10+9)
CacheKVStoreIterator1000-8 0.12ms ± 4% 95µs ± 4% -19.55% (p=0.000 n=10+10)
CacheKVStoreIterator10000-8 1.6ms ± 4% 0.90ms ± 1% -42.11% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 19ms ± 5% 5.5ms ± 1% -71.35% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10s ± 23% 17ms ± 7% -83.44% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 1.3µs ± 6% 0.90µs ± 3% -31.19% (p=0.000 n=9+9)
CacheKVStoreGetKeyFound-8 0.66µs ± 6% 0.56µs ± 2% -14.81% (p=0.000 n=10+9)
alloc/op
B/op
BlockProvision-8 0.11kB ± 0% 0.10kB ± 0% -7.14% (p=0.000 n=10+10)
CacheKVStoreIterator50000-8 0.89MB ± 6% 0.53MB ± 1% -40.85% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 6.3MB ± 23% 1.6MB ± 6% -74.17% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 0.26kB ± 0% 0.23kB ± 1% -11.53% (p=0.000 n=10+8)
allocs/op (count)
AccountMapperSetAccount-8 42 ± 0% 38 ± 0% -9.52% (p=0.000 n=10+10)
BlockProvision-8 6.0 ± 0% 5.0 ± 0% -16.67% (p=0.000 n=10+10)
CacheKVStoreIterator1000-8 14 ± 0% 13 ± 0% -7.14% (p=0.002 n=8+10)
CacheKVStoreIterator10000-8 0.15k ± 2% 76 ± 1% -49.00% (p=0.000 n=7+10)
CacheKVStoreIterator50000-8 8.9k ± 11% 2.0k ± 2% -77.60% (p=0.000 n=10+10)
CacheKVStoreIterator100000-8 0.10M ± 26% 13k ± 12% -86.89% (p=0.000 n=10+10)
CacheKVStoreGetNoKeyFound-8 5.0 ± 0% 4.0 ± 0% -20.00% (p=0.000 n=10+10)
```
Note: Purposefully using a commit off master that doesn't
include the buggy code that caused x/bank.BenchmarkOneBank* to fail
per issue https://github.com/cosmos/cosmos-sdk/issues/10023
Updates #9876
/cc @cuonglm @kirbyquerby
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #XXXX
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-14 14:58:25 -07:00
|
|
|
|
|
|
|
if sortState == stateUnsorted {
|
|
|
|
sort.Slice(unsorted, func(i, j int) bool {
|
|
|
|
return bytes.Compare(unsorted[i].Key, unsorted[j].Key) < 0
|
|
|
|
})
|
|
|
|
}
|
2019-02-01 17:03:09 -08:00
|
|
|
|
2021-09-10 12:42:56 -07:00
|
|
|
for _, item := range unsorted {
|
|
|
|
if item.Value == nil {
|
|
|
|
// deleted element, tracked by store.deleted
|
|
|
|
// setting arbitrary value
|
|
|
|
store.sortedCache.Set(item.Key, []byte{})
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err := store.sortedCache.Set(item.Key, item.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2019-05-15 07:42:06 -07:00
|
|
|
}
|
|
|
|
}
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
//----------------------------------------
|
|
|
|
// etc
|
|
|
|
|
|
|
|
// Only entrypoint to mutate store.cache.
|
|
|
|
func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) {
|
2021-08-24 01:59:38 -07:00
|
|
|
keyStr := conv.UnsafeBytesToStr(key)
|
|
|
|
store.cache[keyStr] = &cValue{
|
2021-09-10 12:42:56 -07:00
|
|
|
value: value,
|
|
|
|
dirty: dirty,
|
|
|
|
}
|
|
|
|
if deleted {
|
|
|
|
store.deleted[keyStr] = struct{}{}
|
|
|
|
} else {
|
|
|
|
delete(store.deleted, keyStr)
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
2019-05-15 07:42:06 -07:00
|
|
|
if dirty {
|
2021-08-24 01:59:38 -07:00
|
|
|
store.unsortedCache[keyStr] = struct{}{}
|
2019-05-15 07:42:06 -07:00
|
|
|
}
|
2019-02-01 17:03:09 -08:00
|
|
|
}
|
2021-09-10 12:42:56 -07:00
|
|
|
|
|
|
|
func (store *Store) isDeleted(key string) bool {
|
|
|
|
_, ok := store.deleted[key]
|
|
|
|
return ok
|
|
|
|
}
|