Skip to content

Commit 8361dba

Browse files
committed
Add block serialization benchmark
1 parent 3b7bffc commit 8361dba

File tree

8 files changed

+134
-2
lines changed

8 files changed

+134
-2
lines changed

bench/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Benchmarks
2+
3+
## Block Serialization Benchmark
4+
5+
**File:** `block_bytes_bench_test.go`
6+
7+
**What it compares:**
8+
Two different approaches to serializing blocks to bytes:
9+
10+
1. **Bytes()** - Uses growing slice with dynamic allocations
11+
2. **PreAllocBytes()** - Pre-allocates slice using `GetSerializeSize()` API
12+
13+
### Running the Benchmark
14+
15+
```bash
16+
go test -bench=BenchmarkComparison -benchmem -benchtime=10x ./bench/
17+
```
18+
19+
### Sample Results
20+
21+
```
22+
BenchmarkComparison/Bytes 10 5755508 ns/op 10589998 B/op 42 allocs/op
23+
BenchmarkComparison/PreAllocBytes 10 5592246 ns/op 1999252 B/op 9 allocs/op
24+
```
25+
26+
- **Memory efficiency**: ~81% reduction in total allocations (10.6MB → 2MB)
27+
- **Allocation count**: ~79% reduction (42 → 9 allocations)
28+
- **CPU performance**: ~2.8% improvement (5.76ms → 5.59ms)

bench/block_bytes_bench_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package bench
2+
3+
import (
4+
"encoding/hex"
5+
"github.com/stringintech/go-bitcoinkernel/kernel"
6+
"os"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func loadTestBlock(tb testing.TB) *kernel.Block {
12+
tb.Helper()
13+
14+
// Load a recent mainnet block
15+
hexBytes, err := os.ReadFile("../data/mainnet.txt")
16+
if err != nil {
17+
tb.Fatal(err)
18+
}
19+
hexString := strings.TrimSpace(string(hexBytes))
20+
21+
bytes, err := hex.DecodeString(hexString)
22+
if err != nil {
23+
tb.Fatal(err)
24+
}
25+
26+
block, err := kernel.NewBlockFromRaw(bytes)
27+
if err != nil {
28+
tb.Fatal(err)
29+
}
30+
31+
return block
32+
}
33+
34+
func BenchmarkComparison(b *testing.B) {
35+
block := loadTestBlock(b)
36+
defer block.Destroy()
37+
38+
b.Run("Bytes", func(b *testing.B) {
39+
b.ReportAllocs()
40+
for i := 0; i < b.N; i++ {
41+
if _, err := block.Bytes(); err != nil {
42+
b.Fatal(err)
43+
}
44+
}
45+
})
46+
47+
b.Run("PreAllocBytes", func(b *testing.B) {
48+
b.ReportAllocs()
49+
for i := 0; i < b.N; i++ {
50+
if _, err := block.PreAllocBytes(); err != nil {
51+
b.Fatal(err)
52+
}
53+
}
54+
})
55+
}

data/mainnet.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

depend/bitcoin/src/kernel/bitcoinkernel.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,8 +965,20 @@ int btck_block_to_bytes(const btck_Block* block, btck_WriteBytes writer, void* u
965965
}
966966
}
967967

968+
int btck_block_get_serialize_size(const btck_Block* block)
969+
{
970+
try {
971+
return ::GetSerializeSize(TX_WITH_WITNESS(*block->m_block));
972+
} catch (...) {
973+
return -1;
974+
}
975+
}
976+
968977
int btck_block_pointer_to_bytes(const btck_BlockPointer* block_, btck_WriteBytes writer, void* user_data)
969978
{
979+
// auto*v=new std::vector<char>(500000);
980+
// ;delete v;
981+
970982
auto block{cast_const_cblock(block_)};
971983
try {
972984
WriterStream ws{writer, user_data};

depend/bitcoin/src/kernel/bitcoinkernel.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,16 @@ BITCOINKERNEL_API int btck_block_to_bytes(
11431143
void* user_data
11441144
) BITCOINKERNEL_ARG_NONNULL(1, 2);
11451145

1146+
/*
1147+
* @brief Returns the serialized size of the block.
1148+
*
1149+
* @param[in] block Non-null.
1150+
* @return The size in bytes of the serialized block, or -1 on error.
1151+
*/
1152+
BITCOINKERNEL_API int btck_block_get_serialize_size(
1153+
const btck_Block* block
1154+
) BITCOINKERNEL_ARG_NONNULL(1);
1155+
11461156
/*
11471157
* @brief Serializes the block pointer through the passed in callback to bytes.
11481158
* This is consensus serialization that is also used for the p2p network.

depend/bitcoin/src/kernel/bitcoinkernel_wrapper.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@ class RefWrapper
4949
};
5050

5151
template <typename T>
52-
std::vector<std::byte> write_bytes(const T* object, int (*to_bytes)(const T*, btck_WriteBytes, void*))
52+
std::vector<std::byte> write_bytes(const T* object, int (*to_bytes)(const T*, btck_WriteBytes, void*), size_t initial_size = 0)
5353
{
5454
std::vector<std::byte> bytes;
55+
if (initial_size > 0) {
56+
bytes.reserve(initial_size);
57+
}
5558
struct UserData {
5659
std::vector<std::byte>* bytes;
5760
std::exception_ptr exception;

kernel/block.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package kernel
55
*/
66
import "C"
77
import (
8+
"fmt"
89
"runtime"
910
"unsafe"
1011
)
@@ -57,6 +58,21 @@ func (b *Block) Bytes() ([]byte, error) {
5758
})
5859
}
5960

61+
func (b *Block) PreAllocBytes() ([]byte, error) {
62+
checkReady(b)
63+
64+
// Get the serialize size first
65+
size := int(C.btck_block_get_serialize_size(b.ptr))
66+
if size < 0 {
67+
return nil, fmt.Errorf("failed to get serialize size")
68+
}
69+
70+
// Use the callback helper with pre-allocated buffer to collect bytes from btck_block_to_bytes
71+
return writeToPreAllocBytes(func(writer C.btck_WriteBytes, userData unsafe.Pointer) C.int {
72+
return C.btck_block_to_bytes(b.ptr, writer, userData)
73+
}, size)
74+
}
75+
6076
// Copy creates a copy of the block
6177
func (b *Block) Copy() (*Block, error) {
6278
checkReady(b)

kernel/writer_helper.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package kernel
99
1010
// Bridge function: exported Go function that C library can call
1111
extern int go_writer_callback_bridge(void* bytes, size_t size, void* userdata);
12+
extern int go_pre_alloc_writer_callback_bridge(void* bytes, size_t size, void* userdata);
1213
*/
1314
import "C"
1415
import (
@@ -41,7 +42,14 @@ func go_writer_callback_bridge(bytes unsafe.Pointer, size C.size_t, userdata uns
4142
// writeToBytes is a helper function that uses a callback pattern to collect bytes
4243
// It takes a function that calls the C API with the writer callback
4344
func writeToBytes(writerFunc func(C.btck_WriteBytes, unsafe.Pointer) C.int) ([]byte, error) {
45+
return writeToPreAllocBytes(writerFunc, 0)
46+
}
47+
48+
func writeToPreAllocBytes(writerFunc func(C.btck_WriteBytes, unsafe.Pointer) C.int, preallocSize int) ([]byte, error) {
4449
callbackData := &writerCallbackData{}
50+
if preallocSize > 0 {
51+
callbackData.buffer = make([]byte, 0, preallocSize)
52+
}
4553

4654
// Create cgo handle for the callback data
4755
handle := cgo.NewHandle(callbackData)
@@ -57,6 +65,5 @@ func writeToBytes(writerFunc func(C.btck_WriteBytes, unsafe.Pointer) C.int) ([]b
5765
return nil, callbackData.err
5866
}
5967

60-
// Return exactly the bytes that were written (slice the buffer to actual size)
6168
return callbackData.buffer, nil
6269
}

0 commit comments

Comments
 (0)