diff --git a/server/v2/stf/branch/bench_test.go b/server/v2/stf/branch/bench_test.go index db46846c1677..d1aa56a2d5ab 100644 --- a/server/v2/stf/branch/bench_test.go +++ b/server/v2/stf/branch/bench_test.go @@ -1,6 +1,7 @@ package branch import ( + "encoding/binary" "fmt" "testing" @@ -14,29 +15,61 @@ var ( ) func Benchmark_CacheStack_Set(b *testing.B) { + var sink any for _, stackSize := range stackSizes { b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { bs := makeBranchStack(b, stackSize) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - _ = bs.Set([]byte{0}, []byte{0}) + sink = bs.Set([]byte{0}, []byte{0}) } }) } + if sink != nil { + b.Fatal("prevent compiler optimization") + } } func Benchmark_Get(b *testing.B) { + var sink any for _, stackSize := range stackSizes { b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { bs := makeBranchStack(b, stackSize) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = bs.Get([]byte{0}) + sink, _ = bs.Get([]byte{0}) + } + }) + } + if sink == nil { + b.Fatal("prevent compiler optimization") + } +} + +func Benchmark_GetSparse(b *testing.B) { + var sink any + for _, stackSize := range stackSizes { + b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { + bs := makeBranchStack(b, stackSize) + keys := func() [][]byte { + var keys [][]byte + for i := 0; i < b.N; i++ { + keys = append(keys, numToBytes(i)) + } + return keys + }() + b.ResetTimer() + b.ReportAllocs() + for _, key := range keys { + sink, _ = bs.Get(key) } }) } + if sink == nil { + b.Fatal("prevent compiler optimization") + } } func Benchmark_Iterate(b *testing.B) { @@ -71,7 +104,7 @@ func makeBranchStack(b *testing.B, stackSize int) Store[store.KVStore] { branch = NewStore[store.KVStore](branch) for j := 0; j < elemsInStack; j++ { // create unique keys by including the branch index. - key := []byte{byte(i), byte(j)} + key := append(numToBytes(i), numToBytes(j)...) value := []byte{byte(j)} err := branch.Set(key, value) if err != nil { @@ -81,3 +114,7 @@ func makeBranchStack(b *testing.B, stackSize int) Store[store.KVStore] { } return branch } + +func numToBytes[T ~int](n T) []byte { + return binary.BigEndian.AppendUint64(nil, uint64(n)) +} diff --git a/server/v2/stf/branch/store.go b/server/v2/stf/branch/store.go index 19e32e8392a3..16358d22f30f 100644 --- a/server/v2/stf/branch/store.go +++ b/server/v2/stf/branch/store.go @@ -29,8 +29,7 @@ func (s Store[T]) Get(key []byte) (value []byte, err error) { if found { return } - // after we get it from parent store, we cache it. - // if it is not found in parent store, we still cache it as nil. + // if not found in the changeset, then check the parent. value, err = s.parent.Get(key) if err != nil { return nil, err diff --git a/store/cachekv/branch_bench_test.go b/store/cachekv/branch_bench_test.go new file mode 100644 index 000000000000..cb1064e7c5c5 --- /dev/null +++ b/store/cachekv/branch_bench_test.go @@ -0,0 +1,116 @@ +package cachekv_test + +import ( + "encoding/binary" + "fmt" + "testing" + + coretesting "cosmossdk.io/core/testing" + "cosmossdk.io/store/cachekv" + "cosmossdk.io/store/dbadapter" +) + +var ( + stackSizes = []int{1, 10, 100} + elemsInStack = 10 +) + +func Benchmark_CacheStack_Set(b *testing.B) { + for _, stackSize := range stackSizes { + b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { + bs := makeBranchStack(b, stackSize) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + bs.Set([]byte{0}, []byte{0}) + } + }) + } +} + +// Gets the same key from the branch store. +func Benchmark_Get(b *testing.B) { + var sink any + for _, stackSize := range stackSizes { + b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { + bs := makeBranchStack(b, stackSize) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + sink = bs.Get([]byte{0}) + } + }) + } + if sink == nil { + b.Fatal("prevent compiler optimization") + } +} + +// Gets always different keys. +func Benchmark_GetSparse(b *testing.B) { + var sink any + for _, stackSize := range stackSizes { + b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { + bs := makeBranchStack(b, stackSize) + keys := func() [][]byte { + var keys [][]byte + for i := 0; i < b.N; i++ { + keys = append(keys, numToBytes(i)) + } + return keys + }() + b.ResetTimer() + b.ReportAllocs() + for _, key := range keys { + sink = bs.Get(key) + } + }) + } + if sink == nil { + b.Fatal("prevent compiler optimization") + } +} + +func Benchmark_Iterate(b *testing.B) { + var keySink, valueSink any + + for _, stackSize := range stackSizes { + b.Run(fmt.Sprintf("StackSize%d", stackSize), func(b *testing.B) { + bs := makeBranchStack(b, stackSize) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + iter := bs.Iterator(nil, nil) + for iter.Valid() { + keySink = iter.Key() + valueSink = iter.Value() + iter.Next() + } + _ = iter.Close() + } + }) + } + + _ = keySink + _ = valueSink +} + +// makeBranchStack creates a branch stack of the given size and initializes it with unique key-value pairs. +func makeBranchStack(_ *testing.B, stackSize int) *cachekv.Store { + parent := dbadapter.Store{DB: coretesting.NewMemDB()} + branch := cachekv.NewStore(parent) + for i := 1; i < stackSize; i++ { + branch = cachekv.NewStore(branch) + for j := 0; j < elemsInStack; j++ { + // create unique keys by including the branch index. + key := append(numToBytes(i), numToBytes(j)...) + value := []byte{byte(j)} + branch.Set(key, value) + } + } + return branch +} + +func numToBytes[T ~int](n T) []byte { + return binary.BigEndian.AppendUint64(nil, uint64(n)) +}