Skip to content

Commit

Permalink
Introduce KeyFormat that uses a full 8 bytes for int64 values and avoids
Browse files Browse the repository at this point in the history
string manipulatio/scanning

Signed-off-by: Silas Davis <[email protected]>
  • Loading branch information
Silas Davis committed Aug 30, 2018
1 parent 17e8f36 commit dc76584
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 57 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
127 changes: 127 additions & 0 deletions key_format.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
37 changes: 37 additions & 0 deletions key_format_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
59 changes: 29 additions & 30 deletions nodedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/<last-version>/
orphanKeyFmt = "o/%010d/%010d/%X" // o/<last-version>/<first-version>/<hash>
orphanKeyFormat = NewKeyFormat('o', int64Size, int64Size, hashSize)

// r/<version>
rootPrefix = "r/"
rootPrefixFmt = "r/%010d"
rootKeyFormat = NewKeyFormat('r', int64Size)
)

type nodeDB struct {
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
})

Expand All @@ -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"
Expand All @@ -462,11 +459,13 @@ func (ndb *nodeDB) String() string {
if len(hash) == 0 {
str += fmt.Sprintf("<nil>\n")
} else if node == nil {
str += fmt.Sprintf("%s%40x: <nil>\n", nodePrefix, hash)
str += fmt.Sprintf("%s%40x: <nil>\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++
})
Expand Down
7 changes: 4 additions & 3 deletions nodedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
7 changes: 3 additions & 4 deletions testutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,23 +115,22 @@ 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()
}
}
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()
Expand Down
Loading

0 comments on commit dc76584

Please sign in to comment.