optimization: bulk serialization writes in WriteBlockUndo and WriteBlock

Similarly to the serialization reads optimization, buffered writes will enable batched XOR calculations.
This is especially beneficial since the current implementation requires copying the write input's `std::span` to perform obfuscation.
Batching allows us to apply XOR operations on the internal buffer instead, reducing unnecessary data copying and improving performance.

------

> macOS Sequoia 15.3.1
> C++ compiler .......................... Clang 19.1.7
> cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ && cmake --build build -j$(nproc) && build/bin/bench_bitcoin -filter='WriteBlockBench' -min-time=10000

Before:

|               ns/op |                op/s |    err% |     total | benchmark
|--------------------:|--------------------:|--------:|----------:|:----------
|        5,149,564.31 |              194.19 |    0.8% |     10.95 | `WriteBlockBench`

After:

|               ns/op |                op/s |    err% |     total | benchmark
|--------------------:|--------------------:|--------:|----------:|:----------
|        2,990,564.63 |              334.39 |    1.5% |     11.27 | `WriteBlockBench`

------

> Ubuntu 24.04.2 LTS
> C++ compiler .......................... GNU 13.3.0
> cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ && cmake --build build -j$(nproc) && build/bin/bench_bitcoin -filter='WriteBlockBench' -min-time=20000

Before:

|               ns/op |                op/s |    err% |          ins/op |          cyc/op |    IPC |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|        5,152,973.58 |              194.06 |    2.2% |   19,350,886.41 |    8,784,539.75 |  2.203 |   3,079,335.21 |    0.4% |     23.18 | `WriteBlockBench`

After:

|               ns/op |                op/s |    err% |          ins/op |          cyc/op |    IPC |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|        4,145,681.13 |              241.21 |    4.0% |   15,337,596.85 |    5,732,186.47 |  2.676 |   2,239,662.64 |    0.1% |     23.94 | `WriteBlockBench`

Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
Co-authored-by: Cory Fields <cory-nospam-@coryfields.com>
This commit is contained in:
committed 2025-04-14 12:04:06 +02:00
1 parent 520965e293
commit 8d801e3efb
4 files changed
+155 -17

No files matched your search

+44
View File
@@ -445,6 +445,9 @@ public:
/** Wrapper around TruncateFile(). */
bool Truncate(unsigned size);
//! Write a mutable buffer more efficiently than write(), obfuscating the buffer in-place.
void write_buffer(std::span<std::byte> src);
//
// Stream subset
//
@@ -658,4 +661,45 @@ public:
}
};
/**
* Wrapper that buffers writes to an underlying stream.
* Requires underlying stream to support write_buffer() method
* for efficient buffer flushing and obfuscation.
*/
template <typename S>
class BufferedWriter
{
S& m_dst;
DataBuffer m_buf;
size_t m_buf_pos{0};
public:
explicit BufferedWriter(S& stream LIFETIMEBOUND, size_t size = 1 << 16) : m_dst{stream}, m_buf(size) {}
~BufferedWriter() { flush(); }
void flush()
{
if (m_buf_pos) m_dst.write_buffer(std::span{m_buf}.first(m_buf_pos));
m_buf_pos = 0;
}
void write(std::span<const std::byte> src)
{
while (const auto available{std::min(src.size(), m_buf.size() - m_buf_pos)}) {
std::copy_n(src.begin(), available, m_buf.begin() + m_buf_pos);
m_buf_pos += available;
if (m_buf_pos == m_buf.size()) flush();
src = src.subspan(available);
}
}
template <typename T>
BufferedWriter& operator<<(const T& obj)
{
Serialize(*this, obj);
return *this;
}
};
#endif // BITCOIN_STREAMS_H