storage: use two maps for MemoryStore

Simple and dumb as it is, this allows to separate contract storage from other
things and dramatically improve Seek() time over storage (even though it's
still unordered!) which in turn improves block processing speed.

        LevelDB             LevelDB (KeepOnlyLatest)  BoltDB              BoltDB (KeepOnlyLatest)
Master  real    16m27,936s  real    10m9,440s         real    16m39,369s  real    8m1,227s
        user    20m12,619s  user    26m13,925s        user    18m9,162s   user    18m5,846s
        sys     2m56,377s   sys     1m32,051s         sys     9m52,576s   sys     2m9,455s

2 maps  real    10m49,495s  real    8m53,342s         real    11m46,204s  real    5m56,043s
        user    14m19,922s  user    24m6,225s         user    13m25,691s  user    15m4,694s
        sys     1m53,021s   sys     1m23,006s         sys     4m31,735s   sys     2m8,714s

neo-bench performance is mostly unaffected, ~0.5% for 1-1 test and 4% for
10K-10K test both fall within regular test error range.
This commit is contained in:
Roman Khimov 2022-02-15 19:07:59 +03:00
parent 0767120e8a
commit 35bdfc5eca
7 changed files with 86 additions and 158 deletions

View file

@ -84,23 +84,25 @@ func (s *BoltDBStore) Delete(key []byte) error {
// PutBatch implements the Store interface. // PutBatch implements the Store interface.
func (s *BoltDBStore) PutBatch(batch Batch) error { func (s *BoltDBStore) PutBatch(batch Batch) error {
memBatch := batch.(*MemoryBatch) memBatch := batch.(*MemoryBatch)
return s.PutChangeSet(memBatch.mem) return s.PutChangeSet(memBatch.mem, memBatch.stor)
} }
// PutChangeSet implements the Store interface. // PutChangeSet implements the Store interface.
func (s *BoltDBStore) PutChangeSet(puts map[string][]byte) error { func (s *BoltDBStore) PutChangeSet(puts map[string][]byte, stores map[string][]byte) error {
var err error var err error
return s.db.Update(func(tx *bbolt.Tx) error { return s.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(Bucket) b := tx.Bucket(Bucket)
for k, v := range puts { for _, m := range []map[string][]byte{puts, stores} {
if v != nil { for k, v := range m {
err = b.Put([]byte(k), v) if v != nil {
} else { err = b.Put([]byte(k), v)
err = b.Delete([]byte(k)) } else {
} err = b.Delete([]byte(k))
if err != nil { }
return err if err != nil {
return err
}
} }
} }
return nil return nil

View file

@ -62,20 +62,22 @@ func (s *LevelDBStore) PutBatch(batch Batch) error {
} }
// PutChangeSet implements the Store interface. // PutChangeSet implements the Store interface.
func (s *LevelDBStore) PutChangeSet(puts map[string][]byte) error { func (s *LevelDBStore) PutChangeSet(puts map[string][]byte, stores map[string][]byte) error {
tx, err := s.db.OpenTransaction() tx, err := s.db.OpenTransaction()
if err != nil { if err != nil {
return err return err
} }
for k := range puts { for _, m := range []map[string][]byte{puts, stores} {
if puts[k] != nil { for k := range m {
err = tx.Put([]byte(k), puts[k], nil) if m[k] != nil {
} else { err = tx.Put([]byte(k), m[k], nil)
err = tx.Delete([]byte(k), nil) } else {
} err = tx.Delete([]byte(k), nil)
if err != nil { }
tx.Discard() if err != nil {
return err tx.Discard()
return err
}
} }
} }
return tx.Commit() return tx.Commit()

View file

@ -55,7 +55,8 @@ func NewMemCachedStore(lower Store) *MemCachedStore {
func (s *MemCachedStore) Get(key []byte) ([]byte, error) { func (s *MemCachedStore) Get(key []byte) ([]byte, error) {
s.mut.RLock() s.mut.RLock()
defer s.mut.RUnlock() defer s.mut.RUnlock()
if val, ok := s.mem[string(key)]; ok { m := s.chooseMap(key)
if val, ok := m[string(key)]; ok {
if val == nil { if val == nil {
return nil, ErrKeyNotFound return nil, ErrKeyNotFound
} }
@ -71,15 +72,17 @@ func (s *MemCachedStore) GetBatch() *MemBatch {
var b MemBatch var b MemBatch
b.Put = make([]KeyValueExists, 0, len(s.mem)) b.Put = make([]KeyValueExists, 0, len(s.mem)+len(s.stor))
b.Deleted = make([]KeyValueExists, 0) b.Deleted = make([]KeyValueExists, 0)
for k, v := range s.mem { for _, m := range []map[string][]byte{s.mem, s.stor} {
key := []byte(k) for k, v := range m {
_, err := s.ps.Get(key) key := []byte(k)
if v == nil { _, err := s.ps.Get(key)
b.Deleted = append(b.Deleted, KeyValueExists{KeyValue: KeyValue{Key: key}, Exists: err == nil}) if v == nil {
} else { b.Deleted = append(b.Deleted, KeyValueExists{KeyValue: KeyValue{Key: key}, Exists: err == nil})
b.Put = append(b.Put, KeyValueExists{KeyValue: KeyValue{Key: key, Value: v}, Exists: err == nil}) } else {
b.Put = append(b.Put, KeyValueExists{KeyValue: KeyValue{Key: key, Value: v}, Exists: err == nil})
}
} }
} }
return &b return &b
@ -131,7 +134,8 @@ func (s *MemCachedStore) seek(ctx context.Context, rng SeekRange, cutPrefix bool
} }
} }
s.mut.RLock() s.mut.RLock()
for k, v := range s.MemoryStore.mem { m := s.MemoryStore.chooseMap(rng.Prefix)
for k, v := range m {
if isKeyOK(k) { if isKeyOK(k) {
memRes = append(memRes, KeyValueExists{ memRes = append(memRes, KeyValueExists{
KeyValue: KeyValue{ KeyValue: KeyValue{
@ -259,7 +263,7 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) {
defer s.plock.Unlock() defer s.plock.Unlock()
s.mut.Lock() s.mut.Lock()
keys = len(s.mem) keys = len(s.mem) + len(s.stor)
if keys == 0 { if keys == 0 {
s.mut.Unlock() s.mut.Unlock()
return 0, nil return 0, nil
@ -269,14 +273,15 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) {
// starts using fresh new maps. This tempstore is only known here and // starts using fresh new maps. This tempstore is only known here and
// nothing ever changes it, therefore accesses to it (reads) can go // nothing ever changes it, therefore accesses to it (reads) can go
// unprotected while writes are handled by s proper. // unprotected while writes are handled by s proper.
var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem}, ps: s.ps} var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem, stor: s.stor}, ps: s.ps}
s.ps = tempstore s.ps = tempstore
s.mem = make(map[string][]byte, len(s.mem)) s.mem = make(map[string][]byte, len(s.mem))
s.stor = make(map[string][]byte, len(s.stor))
if !isSync { if !isSync {
s.mut.Unlock() s.mut.Unlock()
} }
err = tempstore.ps.PutChangeSet(tempstore.mem) err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor)
if !isSync { if !isSync {
s.mut.Lock() s.mut.Lock()
@ -290,10 +295,14 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) {
// We're toast. We'll try to still keep proper state, but OOM // We're toast. We'll try to still keep proper state, but OOM
// killer will get to us eventually. // killer will get to us eventually.
for k := range s.mem { for k := range s.mem {
tempstore.put(k, s.mem[k]) put(tempstore.mem, k, s.mem[k])
}
for k := range s.stor {
put(tempstore.stor, k, s.stor[k])
} }
s.ps = tempstore.ps s.ps = tempstore.ps
s.mem = tempstore.mem s.mem = tempstore.mem
s.stor = tempstore.stor
} }
s.mut.Unlock() s.mut.Unlock()
return keys, err return keys, err

View file

@ -287,7 +287,7 @@ func (b *BadStore) Put(k, v []byte) error {
func (b *BadStore) PutBatch(Batch) error { func (b *BadStore) PutBatch(Batch) error {
return nil return nil
} }
func (b *BadStore) PutChangeSet(_ map[string][]byte) error { func (b *BadStore) PutChangeSet(_ map[string][]byte, _ map[string][]byte) error {
b.onPutBatch() b.onPutBatch()
return ErrKeyNotFound return ErrKeyNotFound
} }

View file

@ -12,8 +12,9 @@ import (
// MemoryStore is an in-memory implementation of a Store, mainly // MemoryStore is an in-memory implementation of a Store, mainly
// used for testing. Do not use MemoryStore in production. // used for testing. Do not use MemoryStore in production.
type MemoryStore struct { type MemoryStore struct {
mut sync.RWMutex mut sync.RWMutex
mem map[string][]byte mem map[string][]byte
stor map[string][]byte
} }
// MemoryBatch is an in-memory batch compatible with MemoryStore. // MemoryBatch is an in-memory batch compatible with MemoryStore.
@ -23,18 +24,19 @@ type MemoryBatch struct {
// Put implements the Batch interface. // Put implements the Batch interface.
func (b *MemoryBatch) Put(k, v []byte) { func (b *MemoryBatch) Put(k, v []byte) {
b.MemoryStore.put(string(k), slice.Copy(v)) put(b.MemoryStore.chooseMap(k), string(k), slice.Copy(v))
} }
// Delete implements Batch interface. // Delete implements Batch interface.
func (b *MemoryBatch) Delete(k []byte) { func (b *MemoryBatch) Delete(k []byte) {
b.MemoryStore.drop(string(k)) drop(b.MemoryStore.chooseMap(k), string(k))
} }
// NewMemoryStore creates a new MemoryStore object. // NewMemoryStore creates a new MemoryStore object.
func NewMemoryStore() *MemoryStore { func NewMemoryStore() *MemoryStore {
return &MemoryStore{ return &MemoryStore{
mem: make(map[string][]byte), mem: make(map[string][]byte),
stor: make(map[string][]byte),
} }
} }
@ -42,16 +44,26 @@ func NewMemoryStore() *MemoryStore {
func (s *MemoryStore) Get(key []byte) ([]byte, error) { func (s *MemoryStore) Get(key []byte) ([]byte, error) {
s.mut.RLock() s.mut.RLock()
defer s.mut.RUnlock() defer s.mut.RUnlock()
if val, ok := s.mem[string(key)]; ok && val != nil { m := s.chooseMap(key)
if val, ok := m[string(key)]; ok && val != nil {
return val, nil return val, nil
} }
return nil, ErrKeyNotFound return nil, ErrKeyNotFound
} }
func (s *MemoryStore) chooseMap(key []byte) map[string][]byte {
switch KeyPrefix(key[0]) {
case STStorage, STTempStorage:
return s.stor
default:
return s.mem
}
}
// put puts a key-value pair into the store, it's supposed to be called // put puts a key-value pair into the store, it's supposed to be called
// with mutex locked. // with mutex locked.
func (s *MemoryStore) put(key string, value []byte) { func put(m map[string][]byte, key string, value []byte) {
s.mem[key] = value m[key] = value
} }
// Put implements the Store interface. Never returns an error. // Put implements the Store interface. Never returns an error.
@ -59,22 +71,22 @@ func (s *MemoryStore) Put(key, value []byte) error {
newKey := string(key) newKey := string(key)
vcopy := slice.Copy(value) vcopy := slice.Copy(value)
s.mut.Lock() s.mut.Lock()
s.put(newKey, vcopy) put(s.chooseMap(key), newKey, vcopy)
s.mut.Unlock() s.mut.Unlock()
return nil return nil
} }
// drop deletes a key-value pair from the store, it's supposed to be called // drop deletes a key-value pair from the store, it's supposed to be called
// with mutex locked. // with mutex locked.
func (s *MemoryStore) drop(key string) { func drop(m map[string][]byte, key string) {
s.mem[key] = nil m[key] = nil
} }
// Delete implements Store interface. Never returns an error. // Delete implements Store interface. Never returns an error.
func (s *MemoryStore) Delete(key []byte) error { func (s *MemoryStore) Delete(key []byte) error {
newKey := string(key) newKey := string(key)
s.mut.Lock() s.mut.Lock()
s.drop(newKey) drop(s.chooseMap(key), newKey)
s.mut.Unlock() s.mut.Unlock()
return nil return nil
} }
@ -82,14 +94,17 @@ func (s *MemoryStore) Delete(key []byte) error {
// PutBatch implements the Store interface. Never returns an error. // PutBatch implements the Store interface. Never returns an error.
func (s *MemoryStore) PutBatch(batch Batch) error { func (s *MemoryStore) PutBatch(batch Batch) error {
b := batch.(*MemoryBatch) b := batch.(*MemoryBatch)
return s.PutChangeSet(b.mem) return s.PutChangeSet(b.mem, b.stor)
} }
// PutChangeSet implements the Store interface. Never returns an error. // PutChangeSet implements the Store interface. Never returns an error.
func (s *MemoryStore) PutChangeSet(puts map[string][]byte) error { func (s *MemoryStore) PutChangeSet(puts map[string][]byte, stores map[string][]byte) error {
s.mut.Lock() s.mut.Lock()
for k := range puts { for k := range puts {
s.put(k, puts[k]) put(s.mem, k, puts[k])
}
for k := range stores {
put(s.stor, k, stores[k])
} }
s.mut.Unlock() s.mut.Unlock()
return nil return nil
@ -109,7 +124,7 @@ func (s *MemoryStore) SeekGC(rng SeekRange, keep func(k, v []byte) bool) error {
// sensitive to the order of KV pairs. // sensitive to the order of KV pairs.
s.seek(rng, func(k, v []byte) bool { s.seek(rng, func(k, v []byte) bool {
if !keep(k, v) { if !keep(k, v) {
s.drop(string(k)) drop(s.chooseMap(k), string(k))
} }
return true return true
}) })
@ -122,7 +137,8 @@ func (s *MemoryStore) SeekAll(key []byte, f func(k, v []byte)) {
s.mut.RLock() s.mut.RLock()
defer s.mut.RUnlock() defer s.mut.RUnlock()
sk := string(key) sk := string(key)
for k, v := range s.mem { m := s.chooseMap(key)
for k, v := range m {
if strings.HasPrefix(k, sk) { if strings.HasPrefix(k, sk) {
f([]byte(k), v) f([]byte(k), v)
} }
@ -152,7 +168,8 @@ func (s *MemoryStore) seek(rng SeekRange, f func(k, v []byte) bool) {
return res != 0 && rng.Backwards == (res > 0) return res != 0 && rng.Backwards == (res > 0)
} }
for k, v := range s.mem { m := s.chooseMap(rng.Prefix)
for k, v := range m {
if v != nil && isKeyOK(k) { if v != nil && isKeyOK(k) {
memList = append(memList, KeyValue{ memList = append(memList, KeyValue{
Key: []byte(k), Key: []byte(k),
@ -185,6 +202,7 @@ func newMemoryBatch() *MemoryBatch {
func (s *MemoryStore) Close() error { func (s *MemoryStore) Close() error {
s.mut.Lock() s.mut.Lock()
s.mem = nil s.mem = nil
s.stor = nil
s.mut.Unlock() s.mut.Unlock()
return nil return nil
} }

View file

@ -61,8 +61,7 @@ type Operation struct {
// SeekRange represents options for Store.Seek operation. // SeekRange represents options for Store.Seek operation.
type SeekRange struct { type SeekRange struct {
// Prefix denotes the Seek's lookup key. // Prefix denotes the Seek's lookup key.
// Empty Prefix means seeking through all keys in the DB starting from // Empty Prefix is not supported.
// the Start if specified.
Prefix []byte Prefix []byte
// Start denotes value appended to the Prefix to start Seek from. // Start denotes value appended to the Prefix to start Seek from.
// Seeking starting from some key includes this key to the result; // Seeking starting from some key includes this key to the result;
@ -92,7 +91,7 @@ type (
Put(k, v []byte) error Put(k, v []byte) error
PutBatch(Batch) error PutBatch(Batch) error
// PutChangeSet allows to push prepared changeset to the Store. // PutChangeSet allows to push prepared changeset to the Store.
PutChangeSet(puts map[string][]byte) error PutChangeSet(puts map[string][]byte, stor map[string][]byte) error
// Seek can guarantee that provided key (k) and value (v) are the only valid until the next call to f. // Seek can guarantee that provided key (k) and value (v) are the only valid until the next call to f.
// Seek continues iteration until false is returned from f. // Seek continues iteration until false is returned from f.
// Key and value slices should not be modified. // Key and value slices should not be modified.

View file

@ -228,108 +228,6 @@ func testStoreSeek(t *testing.T, s Store) {
}) })
}) })
}) })
t.Run("empty prefix, non-empty start", func(t *testing.T) {
t.Run("forwards", func(t *testing.T) {
t.Run("good", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("21")
goodkvs := []KeyValue{
kvs[3], // key = "21"
kvs[4], // key = "22"
kvs[5], // key = "30"
kvs[6], // key = "31"
}
check(t, goodprefix, start, goodkvs, false, nil)
})
t.Run("no matching items", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("32") // start is more than all keys.
check(t, goodprefix, start, []KeyValue{}, false, nil)
})
t.Run("early stop", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("21")
goodkvs := []KeyValue{
kvs[3], // key = "21"
kvs[4], // key = "22"
kvs[5], // key = "30"
}
check(t, goodprefix, start, goodkvs, false, func(k, v []byte) bool {
return string(k) < "30"
})
})
})
t.Run("backwards", func(t *testing.T) {
t.Run("good", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("21")
goodkvs := []KeyValue{
kvs[3], // key = "21"
kvs[2], // key = "20"
kvs[1], // key = "11"
kvs[0], // key = "10"
}
check(t, goodprefix, start, goodkvs, true, nil)
})
t.Run("no matching items", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("0") // start is less than all keys.
check(t, goodprefix, start, []KeyValue{}, true, nil)
})
t.Run("early stop", func(t *testing.T) {
goodprefix := []byte{}
start := []byte("21")
goodkvs := []KeyValue{
kvs[3], // key = "21"
kvs[2], // key = "20"
kvs[1], // key = "11"
}
check(t, goodprefix, start, goodkvs, true, func(k, v []byte) bool {
return string(k) > "11"
})
})
})
})
t.Run("empty prefix, empty start", func(t *testing.T) {
goodprefix := []byte{}
start := []byte{}
goodkvs := make([]KeyValue, len(kvs))
copy(goodkvs, kvs)
t.Run("forwards", func(t *testing.T) {
t.Run("good", func(t *testing.T) {
check(t, goodprefix, start, goodkvs, false, nil)
})
t.Run("early stop", func(t *testing.T) {
goodkvs := []KeyValue{
kvs[0], // key = "10"
kvs[1], // key = "11"
kvs[2], // key = "20"
kvs[3], // key = "21"
}
check(t, goodprefix, start, goodkvs, false, func(k, v []byte) bool {
return string(k) < "21"
})
})
})
t.Run("backwards", func(t *testing.T) {
t.Run("good", func(t *testing.T) {
check(t, goodprefix, start, goodkvs, true, nil)
})
t.Run("early stop", func(t *testing.T) {
goodkvs := []KeyValue{
kvs[6], // key = "31"
kvs[5], // key = "30"
kvs[4], // key = "22"
kvs[3], // key = "21"
}
check(t, goodprefix, start, goodkvs, true, func(k, v []byte) bool {
return string(k) > "21"
})
})
})
})
} }
func testStoreDeleteNonExistent(t *testing.T, s Store) { func testStoreDeleteNonExistent(t *testing.T, s Store) {