Browse Source

lite: memStoreProvider GetHeightBinarySearch method + fix ValKeys.signHeaders

Updates #1021

* Implement a GetHeightBinarySearch method that looks for
the height using the binary search algorithm guaranteeing
worst case iteration time of O(log2(n))
whereas
worst case iteration time of O(n) for the current linear search

So if n we had 500 commits stored by height and sorted, to
trigger the worst case scenario for each, pass in
the most negative height you can find e.g. -1
Linear search: 500 iterations
Binary search: 9 iterations

with n=1000, qHeight = -1
Linear search: 1000 iterations
Binary search: 10 iterations

with n=1e6, qHeight = -1
Linear search: 1e6 iterations
Binary search: 20 iterations

Of course there are realistic expectations e.g. a max of
commits that may be saved so linear search might be useful
for very small size set because it has less preparing overhead
and only ~2 types of comparisons, but nonetheless binary search
shines as soon as we start to hit say 50 commits to search from
as you can see below:

```shell
$ go test -v -run=^$ -bench=MemStore
goos: darwin
goarch: amd64
pkg: github.com/tendermint/tendermint/lite
BenchmarkMemStoreProviderGetByHeightLinearSearch5-4     300000        6491 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch50-4      200000       12064 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch100-4      50000       32987 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch500-4       5000      395521 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch1000-4	       500     2940724 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch5-4     300000        6281 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch50-4      200000       10117 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch100-4     100000       18447 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch500-4      20000       89029 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch1000-4	      5000      265719 ns/op      1600 B/op       15 allocs/op
PASS
ok    github.com/tendermint/tendermint/lite 86.614s
$ go test -v -run=^$ -bench=MemStore
goos: darwin
goarch: amd64
pkg: github.com/tendermint/tendermint/lite
BenchmarkMemStoreProviderGetByHeightLinearSearch5-4     300000        6779 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch50-4      100000       12980 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch100-4      30000       43598 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch500-4       5000      377462 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightLinearSearch1000-4	       500     3278122 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch5-4     300000        7084 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch50-4      200000        9852 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch100-4     100000       19020 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch500-4      20000       99463 ns/op      1600 B/op       15 allocs/op
BenchmarkMemStoreProviderGetByHeightBinarySearch1000-4	      5000      259293 ns/op      1600 B/op       15 allocs/op
PASS
ok    github.com/tendermint/tendermint/lite 86.204s
```

which gives
```shell
$ benchstat old.txt new.txt
name                               old time/op    new time/op    delta
MemStoreProviderGetByHeight5-4       6.63µs ± 2%    6.68µs ± 6%   ~             (p=1.000 n=2+2)
MemStoreProviderGetByHeight50-4      12.5µs ± 4%    10.0µs ± 1%   ~             (p=0.333 n=2+2)
MemStoreProviderGetByHeight100-4     38.3µs ±14%    18.7µs ± 2%   ~             (p=0.333 n=2+2)
MemStoreProviderGetByHeight500-4      386µs ± 2%      94µs ± 6%   ~             (p=0.333 n=2+2)
MemStoreProviderGetByHeight1000-4    3.11ms ± 5%    0.26ms ± 1%   ~             (p=0.333 n=2+2)
```

If need be we can make a hybrid algorithm that switches between the
linear and binary search depending on the number of items.
This is reminiscent of Python's TimSort algorithm.
pull/1043/head
Emmanuel Odeke 7 years ago
parent
commit
14eaba9ec3
3 changed files with 158 additions and 4 deletions
  1. +3
    -0
      lite/helpers.go
  2. +45
    -4
      lite/memprovider.go
  3. +110
    -0
      lite/performance_test.go

+ 3
- 0
lite/helpers.go View File

@ -78,6 +78,9 @@ func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit
// fill in the votes we want
for i := first; i < last; i++ {
if i >= len(v) {
break
}
vote := makeVote(header, vset, v[i])
votes[vote.ValidatorIndex] = vote
}


+ 45
- 4
lite/memprovider.go View File

@ -14,6 +14,8 @@ type memStoreProvider struct {
// btree would be more efficient for larger sets
byHeight fullCommits
byHash map[string]FullCommit
sorted bool
}
// fullCommits just exists to allow easy sorting
@ -52,7 +54,7 @@ func (m *memStoreProvider) StoreCommit(fc FullCommit) error {
defer m.mtx.Unlock()
m.byHash[key] = fc
m.byHeight = append(m.byHeight, fc)
sort.Sort(m.byHeight)
m.sorted = false
return nil
}
@ -60,17 +62,56 @@ func (m *memStoreProvider) StoreCommit(fc FullCommit) error {
func (m *memStoreProvider) GetByHeight(h int64) (FullCommit, error) {
m.mtx.RLock()
defer m.mtx.RUnlock()
if !m.sorted {
sort.Sort(m.byHeight)
m.sorted = true
}
// search from highest to lowest
for i := len(m.byHeight) - 1; i >= 0; i-- {
fc := m.byHeight[i]
if fc.Height() <= h {
if fc := m.byHeight[i]; fc.Height() <= h {
return fc, nil
}
}
return FullCommit{}, liteErr.ErrCommitNotFound()
}
// GetByHeight returns the FullCommit for height h or an error if the commit is not found.
func (m *memStoreProvider) GetByHeightBinarySearch(h int64) (FullCommit, error) {
m.mtx.RLock()
defer m.mtx.RUnlock()
if !m.sorted {
sort.Sort(m.byHeight)
m.sorted = true
}
low, high := 0, len(m.byHeight)-1
var mid int
var hmid int64
var midFC FullCommit
// Our goal is to either find:
// * item ByHeight with the query
// * heighest height with a height <= query
for low <= high {
mid = int(uint(low+high) >> 1) // Avoid an overflow
midFC = m.byHeight[mid]
hmid = midFC.Height()
switch {
case hmid == h:
return midFC, nil
case hmid < h:
low = mid + 1
case hmid > h:
high = mid - 1
}
}
if high >= 0 {
if highFC := m.byHeight[high]; highFC.Height() < h {
return highFC, nil
}
}
return FullCommit{}, liteErr.ErrCommitNotFound()
}
// GetByHash returns the FullCommit for the hash or an error if the commit is not found.
func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) {
m.mtx.RLock()


+ 110
- 0
lite/performance_test.go View File

@ -2,6 +2,7 @@ package lite_test
import (
"fmt"
"math/rand"
"testing"
"github.com/tendermint/tendermint/lite"
@ -115,3 +116,112 @@ func benchmarkCertifyCommit(b *testing.B, keys lite.ValKeys) {
}
}
type algo bool
const (
linearSearch = true
binarySearch = false
)
var (
fcs5, h5 = genFullCommits(nil, nil, 5)
fcs50, h50 = genFullCommits(fcs5, h5, 50)
fcs100, h100 = genFullCommits(fcs50, h50, 100)
fcs500, h500 = genFullCommits(fcs100, h100, 500)
fcs1000, h1000 = genFullCommits(fcs500, h500, 1000)
)
func BenchmarkMemStoreProviderGetByHeightLinearSearch5(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs5, h5, linearSearch)
}
func BenchmarkMemStoreProviderGetByHeightLinearSearch50(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs50, h50, linearSearch)
}
func BenchmarkMemStoreProviderGetByHeightLinearSearch100(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs100, h100, linearSearch)
}
func BenchmarkMemStoreProviderGetByHeightLinearSearch500(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs500, h500, linearSearch)
}
func BenchmarkMemStoreProviderGetByHeightLinearSearch1000(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs1000, h1000, linearSearch)
}
func BenchmarkMemStoreProviderGetByHeightBinarySearch5(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs5, h5, binarySearch)
}
func BenchmarkMemStoreProviderGetByHeightBinarySearch50(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs50, h50, binarySearch)
}
func BenchmarkMemStoreProviderGetByHeightBinarySearch100(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs100, h100, binarySearch)
}
func BenchmarkMemStoreProviderGetByHeightBinarySearch500(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs500, h500, binarySearch)
}
func BenchmarkMemStoreProviderGetByHeightBinarySearch1000(b *testing.B) {
benchmarkMemStoreProviderGetByHeight(b, fcs1000, h1000, binarySearch)
}
var rng = rand.New(rand.NewSource(10))
func benchmarkMemStoreProviderGetByHeight(b *testing.B, fcs []lite.FullCommit, fHeights []int64, algo algo) {
b.StopTimer()
mp := lite.NewMemStoreProvider()
for i, fc := range fcs {
if err := mp.StoreCommit(fc); err != nil {
b.Fatalf("FullCommit #%d: err: %v", i, err)
}
}
qHeights := make([]int64, len(fHeights))
copy(qHeights, fHeights)
// Append some non-existent heights to trigger the worst cases.
qHeights = append(qHeights, 19, -100, -10000, 1e7, -17, 31, -1e9)
searchFn := mp.GetByHeight
if algo == binarySearch {
searchFn = mp.(interface {
GetByHeightBinarySearch(h int64) (lite.FullCommit, error)
}).GetByHeightBinarySearch
}
hPerm := rng.Perm(len(qHeights))
b.StartTimer()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, j := range hPerm {
h := qHeights[j]
if _, err := searchFn(h); err != nil {
}
}
}
b.ReportAllocs()
}
func genFullCommits(prevFC []lite.FullCommit, prevH []int64, want int) ([]lite.FullCommit, []int64) {
fcs := make([]lite.FullCommit, len(prevFC))
copy(fcs, prevFC)
heights := make([]int64, len(prevH))
copy(heights, prevH)
appHash := []byte("benchmarks")
chainID := "benchmarks-gen-full-commits"
n := want
keys := lite.GenValKeys(2 + (n / 3))
for i := 0; i < n; i++ {
vals := keys.ToValidators(10, int64(n/2))
h := int64(20 + 10*i)
fcs = append(fcs, keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5))
heights = append(heights, h)
}
return fcs, heights
}

Loading…
Cancel
Save