From dc76584da878597e6b1447db9849352abdfa871b Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Thu, 30 Aug 2018 16:01:47 +0100 Subject: [PATCH] Introduce KeyFormat that uses a full 8 bytes for int64 values and avoids string manipulatio/scanning Signed-off-by: Silas Davis --- CHANGELOG.md | 11 ++++ key_format.go | 127 +++++++++++++++++++++++++++++++++++++++++++++ key_format_test.go | 37 +++++++++++++ nodedb.go | 59 +++++++++++---------- nodedb_test.go | 7 +-- proof_test.go | 2 +- testutils_test.go | 7 ++- tree_test.go | 37 +++++++------ 8 files changed, 230 insertions(+), 57 deletions(-) create mode 100644 key_format.go create mode 100644 key_format_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e3f5ca4..69d6933dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +BREAKING CHANGES + +- Changed internal database key format to store int64 key components in a full 8-byte fixed width. + + +IMPROVEMENTS + +- Database key format avoids use of fmt.Sprintf fmt.Sscanf leading to ~10% speedup in benchmark BenchmarkTreeLoadAndDelete + ## 0.10.0 BREAKING CHANGES diff --git a/key_format.go b/key_format.go new file mode 100644 index 000000000..597c0efbf --- /dev/null +++ b/key_format.go @@ -0,0 +1,127 @@ +package iavl + +import ( + "encoding/binary" + "fmt" +) + +// Provides a fixed-width lexicographically sortable []byte key format +type KeyFormat struct { + prefix byte + layout []int + length int +} + +// Create a []byte key format based on a single byte prefix and fixed width key segments each of whose length is +// specified by by the corresponding element of layout +func NewKeyFormat(prefix byte, layout ...int) *KeyFormat { + // For prefix + length := 1 + for _, l := range layout { + length += int(l) + } + return &KeyFormat{ + prefix: prefix, + layout: layout, + length: length, + } +} + +// Format the byte segments into the key format - will panic if the segment lengths to do match the layout. +func (kf *KeyFormat) KeyBytes(segments ...[]byte) []byte { + key := make([]byte, kf.length) + key[0] = kf.prefix + n := 1 + for i, s := range segments { + l := kf.layout[i] + if len(s) > l { + panic(fmt.Errorf("length of segment %X provided to KeyFormat.Key() is longer than the %d bytes "+ + "required by layout for segment %d", s, l, i)) + } + copy(key[n+(l-len(s)):n+l], s) + n += l + } + return key[:n] +} + +// Format the args passed into the key format - will panic if the arguments passed to not match the length +// of the segment to which they correspond. When called with no arguments returns the raw prefix (useful as a start +// element of the entire keys space when sorted lexicographically) +func (kf *KeyFormat) Key(args ...interface{}) []byte { + if len(args) > len(kf.layout) { + panic(fmt.Errorf("KeyFormat.Key() is provided with %d args but format only has %d segments", + len(args), len(kf.layout))) + } + segments := make([][]byte, len(args)) + for i, a := range args { + segments[i] = format(a) + } + return kf.KeyBytes(segments...) +} + +// Reads out the bytes associated with each segment of the key format from key +func (kf *KeyFormat) ScanBytes(key []byte) [][]byte { + segments := make([][]byte, len(kf.layout)) + n := 1 + for i, l := range kf.layout { + end := n + l + if end > len(key) { + return segments[:i] + } + segments[i] = key[n:end] + n = end + } + return segments +} + +// Extracts the segments into the values pointed to by each of args. Each arg must be a pointer to int64, uint64, or +// []byte, and the width of the args must match layout. +func (kf *KeyFormat) Scan(key []byte, args ...interface{}) { + segments := kf.ScanBytes(key) + if len(args) > len(segments) { + panic(fmt.Errorf("KeyFormat.Scan() is provided with %d args but format only has %d segments", + len(args), len(segments))) + } + for i, a := range args { + scan(a, segments[i]) + } +} + +// Return the prefix as a string +func (kf *KeyFormat) Prefix() string { + return string([]byte{kf.prefix}) +} + +func scan(a interface{}, value []byte) { + switch v := a.(type) { + case *int64: + *v = int64(binary.BigEndian.Uint64(value)) + case *uint64: + *v = binary.BigEndian.Uint64(value) + case *[]byte: + *v = value + default: + panic(fmt.Errorf("KeyFormat.scan() does not support scanning value of type %T: %v", a, a)) + } +} + +func format(a interface{}) []byte { + switch v := a.(type) { + case uint64: + bs := make([]byte, 8) + binary.BigEndian.PutUint64(bs, v) + return bs + case uint: + return format(uint64(v)) + case int64: + bs := make([]byte, 8) + binary.BigEndian.PutUint64(bs, uint64(v)) + return bs + case int: + return format(int64(v)) + case []byte: + return v + default: + panic(fmt.Errorf("KeyFormat.format() does not support formatting value of type %T: %v", a, a)) + } +} diff --git a/key_format_test.go b/key_format_test.go new file mode 100644 index 000000000..dd6668aec --- /dev/null +++ b/key_format_test.go @@ -0,0 +1,37 @@ +package iavl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeyFormatBytes(t *testing.T) { + kf := NewKeyFormat(byte('e'), 8, 8, 8) + assert.Equal(t, []byte{'e', 0, 0, 0, 0, 0, 1, 2, 3}, kf.KeyBytes([]byte{1, 2, 3})) + assert.Equal(t, []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8}, kf.KeyBytes([]byte{1, 2, 3, 4, 5, 6, 7, 8})) + assert.Equal(t, []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 1, 1, 2, 2, 3, 3}, + kf.KeyBytes([]byte{1, 2, 3, 4, 5, 6, 7, 8}, []byte{1, 2, 3, 4, 5, 6, 7, 8}, []byte{1, 1, 2, 2, 3, 3})) + assert.Equal(t, []byte{'e'}, kf.KeyBytes()) +} + +func TestKeyFormat(t *testing.T) { + kf := NewKeyFormat(byte('e'), 8, 8, 8) + key := []byte{'e', 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 200, 0, 0, 0, 0, 0, 0, 1, 144} + var a, b, c int64 = 100, 200, 400 + assert.Equal(t, key, kf.Key(a, b, c)) + + var ao, bo, co = new(int64), new(int64), new(int64) + kf.Scan(key, ao, bo, co) + assert.Equal(t, a, *ao) + assert.Equal(t, b, *bo) + assert.Equal(t, c, *co) + + bs := new([]byte) + kf.Scan(key, ao, bo, bs) + assert.Equal(t, a, *ao) + assert.Equal(t, b, *bo) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 1, 144}, *bs) + + assert.Equal(t, []byte{'e', 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 200}, kf.Key(a, b)) +} diff --git a/nodedb.go b/nodedb.go index a933b2662..a589a5fc2 100644 --- a/nodedb.go +++ b/nodedb.go @@ -7,27 +7,29 @@ import ( "sort" "sync" + "github.com/tendermint/tendermint/crypto/tmhash" dbm "github.com/tendermint/tendermint/libs/db" ) +const ( + int64Size = 8 + hashSize = tmhash.Size +) + var ( // All node keys are prefixed with this. This ensures no collision is // possible with the other keys, and makes them easier to traverse. - nodePrefix = "n/" - nodeKeyFmt = "n/%X" + nodeKeyFormat = NewKeyFormat('n', hashSize) // Orphans are keyed in the database by their expected lifetime. // The first number represents the *last* version at which the orphan needs // to exist, while the second number represents the *earliest* version at // which it is expected to exist - which starts out by being the version // of the node being orphaned. - orphanPrefix = "o/" - orphanPrefixFmt = "o/%010d/" // o// - orphanKeyFmt = "o/%010d/%010d/%X" // o/// + orphanKeyFormat = NewKeyFormat('o', int64Size, int64Size, hashSize) // r/ - rootPrefix = "r/" - rootPrefixFmt = "r/%010d" + rootKeyFormat = NewKeyFormat('r', int64Size) ) type nodeDB struct { @@ -196,7 +198,7 @@ func (ndb *nodeDB) deleteOrphans(version int64) { // See comment on `orphanKeyFmt`. Note that here, `version` and // `toVersion` are always equal. - fmt.Sscanf(string(key), orphanKeyFmt, &toVersion, &fromVersion) + orphanKeyFormat.Scan(key, &toVersion, &fromVersion) // Delete orphan key and reverse-lookup key. ndb.batch.Delete(key) @@ -218,15 +220,15 @@ func (ndb *nodeDB) deleteOrphans(version int64) { } func (ndb *nodeDB) nodeKey(hash []byte) []byte { - return []byte(fmt.Sprintf(nodeKeyFmt, hash)) + return nodeKeyFormat.KeyBytes(hash) } func (ndb *nodeDB) orphanKey(fromVersion, toVersion int64, hash []byte) []byte { - return []byte(fmt.Sprintf(orphanKeyFmt, toVersion, fromVersion, hash)) + return orphanKeyFormat.Key(toVersion, fromVersion, hash) } func (ndb *nodeDB) rootKey(version int64) []byte { - return []byte(fmt.Sprintf(rootPrefixFmt, version)) + return rootKeyFormat.Key(version) } func (ndb *nodeDB) getLatestVersion() int64 { @@ -244,20 +246,16 @@ func (ndb *nodeDB) updateLatestVersion(version int64) { func (ndb *nodeDB) getPreviousVersion(version int64) int64 { itr := ndb.db.ReverseIterator( - []byte(fmt.Sprintf(rootPrefixFmt, version-1)), - []byte(fmt.Sprintf(rootPrefixFmt, 0)), + rootKeyFormat.Key(version-1), + rootKeyFormat.Key(0), ) defer itr.Close() pversion := int64(-1) for ; itr.Valid(); itr.Next() { k := itr.Key() - _, err := fmt.Sscanf(string(k), rootPrefixFmt, &pversion) - if err != nil { - panic(err) - } else { - return pversion - } + rootKeyFormat.Scan(k, &pversion) + return pversion } return 0 @@ -274,13 +272,12 @@ func (ndb *nodeDB) deleteRoot(version int64) { } func (ndb *nodeDB) traverseOrphans(fn func(k, v []byte)) { - ndb.traversePrefix([]byte(orphanPrefix), fn) + ndb.traversePrefix(orphanKeyFormat.Key(), fn) } // Traverse orphans ending at a certain version. func (ndb *nodeDB) traverseOrphansVersion(version int64, fn func(k, v []byte)) { - prefix := fmt.Sprintf(orphanPrefixFmt, version) - ndb.traversePrefix([]byte(prefix), fn) + ndb.traversePrefix(orphanKeyFormat.Key(version), fn) } // Traverse all keys. @@ -339,9 +336,9 @@ func (ndb *nodeDB) getRoot(version int64) []byte { func (ndb *nodeDB) getRoots() (map[int64][]byte, error) { roots := map[int64][]byte{} - ndb.traversePrefix([]byte(rootPrefix), func(k, v []byte) { + ndb.traversePrefix(rootKeyFormat.Key(), func(k, v []byte) { var version int64 - fmt.Sscanf(string(k), rootPrefixFmt, &version) + rootKeyFormat.Scan(k, &version) roots[version] = v }) return roots, nil @@ -426,12 +423,12 @@ func (ndb *nodeDB) size() int { func (ndb *nodeDB) traverseNodes(fn func(hash []byte, node *Node)) { nodes := []*Node{} - ndb.traversePrefix([]byte(nodePrefix), func(key, value []byte) { + ndb.traversePrefix(nodeKeyFormat.Key(), func(key, value []byte) { node, err := MakeNode(value) if err != nil { panic(fmt.Sprintf("Couldn't decode node from database: %v", err)) } - fmt.Sscanf(string(key), nodeKeyFmt, &node.hash) + nodeKeyFormat.Scan(key, &node.hash) nodes = append(nodes, node) }) @@ -448,7 +445,7 @@ func (ndb *nodeDB) String() string { var str string index := 0 - ndb.traversePrefix([]byte(rootPrefix), func(key, value []byte) { + ndb.traversePrefix(rootKeyFormat.Key(), func(key, value []byte) { str += fmt.Sprintf("%s: %x\n", string(key), value) }) str += "\n" @@ -462,11 +459,13 @@ func (ndb *nodeDB) String() string { if len(hash) == 0 { str += fmt.Sprintf("\n") } else if node == nil { - str += fmt.Sprintf("%s%40x: \n", nodePrefix, hash) + str += fmt.Sprintf("%s%40x: \n", nodeKeyFormat.Prefix(), hash) } else if node.value == nil && node.height > 0 { - str += fmt.Sprintf("%s%40x: %s %-16s h=%d version=%d\n", nodePrefix, hash, node.key, "", node.height, node.version) + str += fmt.Sprintf("%s%40x: %s %-16s h=%d version=%d\n", + nodeKeyFormat.Prefix(), hash, node.key, "", node.height, node.version) } else { - str += fmt.Sprintf("%s%40x: %s = %-16s h=%d version=%d\n", nodePrefix, hash, node.key, node.value, node.height, node.version) + str += fmt.Sprintf("%s%40x: %s = %-16s h=%d version=%d\n", + nodeKeyFormat.Prefix(), hash, node.key, node.value, node.height, node.version) } index++ }) diff --git a/nodedb_test.go b/nodedb_test.go index d800faefa..eee0ce16b 100644 --- a/nodedb_test.go +++ b/nodedb_test.go @@ -26,12 +26,13 @@ func makeHashes(b *testing.B, seed int64) [][]byte { b.StopTimer() rnd := rand.NewSource(seed) hashes := make([][]byte, b.N) + hashBytes := 8*((hashSize+7)/8) for i := 0; i < b.N; i++ { - hashes[i] = make([]byte, 32) - for b := 0; b < 32; b += 8 { + hashes[i] = make([]byte, hashBytes) + for b := 0; b < hashBytes; b += 8 { binary.BigEndian.PutUint64(hashes[i][b:b+8], uint64(rnd.Int63())) } - hashes[i] = hashes[i][:20] + hashes[i] = hashes[i][:hashSize] } b.StartTimer() return hashes diff --git a/proof_test.go b/proof_test.go index b408a0d9f..b9e82a2b2 100644 --- a/proof_test.go +++ b/proof_test.go @@ -17,7 +17,7 @@ func TestTreeGetWithProof(t *testing.T) { require := require.New(t) for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { key := []byte{ikey} - tree.Set(key, []byte(rand.Str(8))) + tree.Set(key, []byte(random.Str(8))) } root := tree.WorkingHash() diff --git a/testutils_test.go b/testutils_test.go index 1a76d60b9..9f810a79b 100644 --- a/testutils_test.go +++ b/testutils_test.go @@ -115,8 +115,9 @@ func benchmarkImmutableAvlTreeWithDB(b *testing.B, db db.DB) { b.StopTimer() t := NewMutableTree(db, 100000) + value := []byte{} for i := 0; i < 1000000; i++ { - t.Set(i2b(int(cmn.RandInt32())), nil) + t.Set(i2b(int(cmn.RandInt32())), value) if i > 990000 && i%1000 == 999 { t.SaveVersion() } @@ -124,14 +125,12 @@ func benchmarkImmutableAvlTreeWithDB(b *testing.B, db db.DB) { b.ReportAllocs() t.SaveVersion() - fmt.Println("ok, starting") - runtime.GC() b.StartTimer() for i := 0; i < b.N; i++ { ri := i2b(int(cmn.RandInt32())) - t.Set(ri, nil) + t.Set(ri, value) t.Remove(ri) if i%100 == 99 { t.SaveVersion() diff --git a/tree_test.go b/tree_test.go index c12eb7116..613977eec 100644 --- a/tree_test.go +++ b/tree_test.go @@ -3,7 +3,6 @@ package iavl import ( "bytes" "flag" - "fmt" "os" "runtime" "testing" @@ -18,11 +17,11 @@ import ( var testLevelDB bool var testFuzzIterations int -var rand *cmn.Rand +var random *cmn.Rand func init() { - rand = cmn.NewRand() - rand.Seed(0) // for determinism + random = cmn.NewRand() + random.Seed(0) // for determinism flag.BoolVar(&testLevelDB, "test.leveldb", false, "test leveldb backend") flag.IntVar(&testFuzzIterations, "test.fuzz-iterations", 100000, "number of fuzz testing iterations") flag.Parse() @@ -55,8 +54,8 @@ func TestVersionedRandomTree(t *testing.T) { // Create a tree of size 1000 with 100 versions. for i := 1; i <= versions; i++ { for j := 0; j < keysPerVersion; j++ { - k := []byte(rand.Str(8)) - v := []byte(rand.Str(8)) + k := []byte(random.Str(8)) + v := []byte(random.Str(8)) tree.Set(k, v) } tree.SaveVersion() @@ -97,8 +96,8 @@ func TestVersionedRandomTreeSmallKeys(t *testing.T) { for i := 1; i <= versions; i++ { for j := 0; j < keysPerVersion; j++ { // Keys of size one are likely to be overwritten. - k := []byte(rand.Str(1)) - v := []byte(rand.Str(8)) + k := []byte(random.Str(1)) + v := []byte(random.Str(8)) tree.Set(k, v) singleVersionTree.Set(k, v) } @@ -119,7 +118,7 @@ func TestVersionedRandomTreeSmallKeys(t *testing.T) { // Try getting random keys. for i := 0; i < keysPerVersion; i++ { - _, val := tree.Get([]byte(rand.Str(1))) + _, val := tree.Get([]byte(random.Str(1))) require.NotNil(val) require.NotEmpty(val) } @@ -138,8 +137,8 @@ func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { for i := 1; i <= versions; i++ { for j := 0; j < keysPerVersion; j++ { // Keys of size one are likely to be overwritten. - k := []byte(rand.Str(1)) - v := []byte(rand.Str(8)) + k := []byte(random.Str(1)) + v := []byte(random.Str(8)) tree.Set(k, v) singleVersionTree.Set(k, v) } @@ -147,7 +146,7 @@ func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { } singleVersionTree.SaveVersion() - for _, i := range rand.Perm(versions - 1) { + for _, i := range random.Perm(versions - 1) { tree.DeleteVersion(int64(i + 1)) } @@ -160,7 +159,7 @@ func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { // Try getting random keys. for i := 0; i < keysPerVersion; i++ { - _, val := tree.Get([]byte(rand.Str(1))) + _, val := tree.Get([]byte(random.Str(1))) require.NotNil(val) require.NotEmpty(val) } @@ -697,8 +696,8 @@ func TestVersionedCheckpoints(t *testing.T) { for i := 1; i <= versions; i++ { for j := 0; j < keysPerVersion; j++ { - k := []byte(rand.Str(1)) - v := []byte(rand.Str(8)) + k := []byte(random.Str(1)) + v := []byte(random.Str(8)) keys[int64(i)] = append(keys[int64(i)], k) tree.Set(k, v) } @@ -931,7 +930,7 @@ func TestVersionedTreeEfficiency(t *testing.T) { for i := 1; i <= versions; i++ { for j := 0; j < keysPerVersion; j++ { // Keys of size one are likely to be overwritten. - tree.Set([]byte(rand.Str(1)), []byte(rand.Str(8))) + tree.Set([]byte(random.Str(1)), []byte(random.Str(8))) } sizeBefore := len(tree.ndb.nodes()) tree.SaveVersion() @@ -1051,7 +1050,7 @@ func TestOrphans(t *testing.T) { tree.ndb.traverseOrphans(func(k, v []byte) { var fromVersion, toVersion int64 - fmt.Sscanf(string(k), orphanKeyFmt, &toVersion, &fromVersion) + orphanKeyFormat.Scan(k, &toVersion, &fromVersion) require.Equal(fromVersion, int64(1), "fromVersion should be 1") require.Equal(toVersion, int64(1), "toVersion should be 1") }) @@ -1182,7 +1181,7 @@ func BenchmarkTreeLoadAndDelete(b *testing.B) { tree := NewMutableTree(d, 0) for v := 1; v < numVersions; v++ { for i := 0; i < numKeysPerVersion; i++ { - tree.Set([]byte(rand.Str(16)), rand.Bytes(32)) + tree.Set([]byte(random.Str(16)), random.Bytes(32)) } tree.SaveVersion() } @@ -1203,7 +1202,7 @@ func BenchmarkTreeLoadAndDelete(b *testing.B) { // If we can load quickly into a data-structure that allows for // efficient deletes, we are golden. for v := 0; v < numVersions/10; v++ { - version := (rand.Int() % numVersions) + 1 + version := (random.Int() % numVersions) + 1 tree.DeleteVersion(int64(version)) } }