From a6b922cb7ecf09273c5b77fe2e247dbc4df1394e Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Thu, 16 Dec 2021 08:02:14 -0800 Subject: [PATCH] perf: use a reusable bytes.Buffer to speed up MutableTree.String (#456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior code used a naive string concatenation str += "..." which is very slow and inefficient especially when iterating over all the keys, but also it performed an unnecessary byteslice->string conversion for all arguments inside (nodeDB).traverse* using str += fmt.Sprintf("%s: %x\n", string(key), value) notice the `string(key)` passed into fmt.Sprintf("%s"? At bare minimum that code should just simply be: str += fmt.Sprintf("%s: %x\n", key, value) per https://twitter.com/orijtech/status/1462100381803102213?s=20 which saves a whole lot of cycles and RAM. This change uses: * (*bytes.Buffer) in combination with fmt.Fprintf * A sync.Pool with (*bytes.Buffer) values to reuse buffers and the results are profound: ```shell $ benchstat before.txt after.txt name old time/op new time/op delta TreeString-8 111ms ± 8% 4ms ± 4% -96.01% (p=0.000 n=20+20) name old alloc/op new alloc/op delta TreeString-8 734MB ± 0% 2MB ± 1% -99.72% (p=0.000 n=20+20) name old allocs/op new allocs/op delta TreeString-8 37.0k ± 0% 28.7k ± 0% -22.40% (p=0.000 n=20+20) ``` Fixes #455 --- nodedb.go | 23 +++++++++++++---------- nodedb_test.go | 35 +++++++++++++++++++++++++++++++++++ with_gcc_test.go | 1 + 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/nodedb.go b/nodedb.go index ea26fc821..0dddc9dca 100644 --- a/nodedb.go +++ b/nodedb.go @@ -698,33 +698,36 @@ func (ndb *nodeDB) traverseNodes(fn func(hash []byte, node *Node)) { } func (ndb *nodeDB) String() string { - var str string + buf := bufPool.Get().(*bytes.Buffer) + defer bufPool.Put(buf) + buf.Reset() + index := 0 ndb.traversePrefix(rootKeyFormat.Key(), func(key, value []byte) { - str += fmt.Sprintf("%s: %x\n", string(key), value) + fmt.Fprintf(buf, "%s: %x\n", key, value) }) - str += "\n" + buf.WriteByte('\n') ndb.traverseOrphans(func(key, value []byte) { - str += fmt.Sprintf("%s: %x\n", string(key), value) + fmt.Fprintf(buf, "%s: %x\n", key, value) }) - str += "\n" + buf.WriteByte('\n') ndb.traverseNodes(func(hash []byte, node *Node) { switch { case len(hash) == 0: - str += "\n" + buf.WriteByte('\n') case node == nil: - str += fmt.Sprintf("%s%40x: \n", nodeKeyFormat.Prefix(), hash) + fmt.Fprintf(buf, "%s%40x: \n", nodeKeyFormat.Prefix(), hash) case node.value == nil && node.height > 0: - str += fmt.Sprintf("%s%40x: %s %-16s h=%d version=%d\n", + fmt.Fprintf(buf, "%s%40x: %s %-16s h=%d version=%d\n", nodeKeyFormat.Prefix(), hash, node.key, "", node.height, node.version) default: - str += fmt.Sprintf("%s%40x: %s = %-16s h=%d version=%d\n", + fmt.Fprintf(buf, "%s%40x: %s = %-16s h=%d version=%d\n", nodeKeyFormat.Prefix(), hash, node.key, node.value, node.height, node.version) } index++ }) - return "-" + "\n" + str + "-" + return "-" + "\n" + buf.String() + "-" } diff --git a/nodedb_test.go b/nodedb_test.go index 8e891a15e..e5dadf446 100644 --- a/nodedb_test.go +++ b/nodedb_test.go @@ -4,6 +4,10 @@ import ( "encoding/binary" "math/rand" "testing" + + "github.com/stretchr/testify/require" + + dbm "github.com/tendermint/tm-db" ) func BenchmarkNodeKey(b *testing.B) { @@ -22,6 +26,37 @@ func BenchmarkOrphanKey(b *testing.B) { } } +func BenchmarkTreeString(b *testing.B) { + tree := makeAndPopulateMutableTree(b) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink = tree.String() + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func makeAndPopulateMutableTree(tb testing.TB) *MutableTree { + memDB := dbm.NewMemDB() + tree, err := NewMutableTreeWithOpts(memDB, 0, &Options{InitialVersion: 9}) + require.NoError(tb, err) + + for i := 0; i < 1e4; i++ { + buf := make([]byte, 0, (i/255)+1) + for j := 0; 1<>j)&0xff)) + } + tree.Set(buf, buf) + } + _, _, err = tree.SaveVersion() + require.Nil(tb, err, "Expected .SaveVersion to succeed") + return tree +} + func makeHashes(b *testing.B, seed int64) [][]byte { b.StopTimer() rnd := rand.NewSource(seed) diff --git a/with_gcc_test.go b/with_gcc_test.go index 70dc791a5..433ab3acb 100644 --- a/with_gcc_test.go +++ b/with_gcc_test.go @@ -1,3 +1,4 @@ +//go:build gcc // +build gcc // This file exists because some of the DBs e.g CLevelDB