optimization: Bulk serialization writes in WriteBlockUndo and WriteBlock

Added `AutoFile::write_buffer` for batching obfuscation operations, so instead of copying the data and doing the xor in a 4096 byte array, we're doing it directly on the input.

Similarly to the serialization reads, buffered writes will enable batched xor calculations - especially since currently we need to copy the write input's std::span to do the obfuscation on it, batching enables doing the xor on the internal buffer instead.

------

> 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:
Lőrinc 2025-03-26 12:54:07 +01:00
parent c0e8ef1b7a
commit 652b4e3de5
4 changed files with 151 additions and 10 deletions

View file

@ -940,11 +940,12 @@ bool BlockManager::WriteBlockUndo(const CBlockUndo& blockundo, BlockValidationSt
return false;
}
// Open history file to append
AutoFile fileout{OpenUndoFile(pos)};
if (fileout.IsNull()) {
AutoFile file{OpenUndoFile(pos)};
if (file.IsNull()) {
LogError("OpenUndoFile failed for %s while writing", pos.ToString());
return FatalError(m_opts.notifications, state, _("Failed to write undo data."));
}
BufferedWriter fileout{file};
// Write index header
fileout << GetParams().MessageStart() << blockundo_size;
@ -1082,12 +1083,13 @@ FlatFilePos BlockManager::WriteBlock(const CBlock& block, int nHeight)
LogError("FindNextBlockPos failed for %s while writing", pos.ToString());
return FlatFilePos();
}
AutoFile fileout{OpenBlockFile(pos)};
if (fileout.IsNull()) {
AutoFile file{OpenBlockFile(pos)};
if (file.IsNull()) {
LogError("OpenBlockFile failed for %s while writing", pos.ToString());
m_opts.notifications.fatalError(_("Failed to write block."));
return FlatFilePos();
}
BufferedWriter fileout{file};
// Write index header
fileout << GetParams().MessageStart() << block_size;

View file

@ -91,17 +91,23 @@ void AutoFile::write(std::span<const std::byte> src)
std::array<std::byte, 4096> buf;
while (src.size() > 0) {
auto buf_now{std::span{buf}.first(std::min<size_t>(src.size(), buf.size()))};
std::copy(src.begin(), src.begin() + buf_now.size(), buf_now.begin());
util::Xor(buf_now, m_xor, *m_position);
if (std::fwrite(buf_now.data(), 1, buf_now.size(), m_file) != buf_now.size()) {
throw std::ios_base::failure{"AutoFile::write: failed"};
}
std::copy_n(src.begin(), buf_now.size(), buf_now.begin());
write_buffer(buf_now);
src = src.subspan(buf_now.size());
*m_position += buf_now.size();
}
}
}
void AutoFile::write_buffer(std::span<std::byte> src)
{
if (!m_file) throw std::ios_base::failure("AutoFile::write_buffer: file handle is nullptr");
util::Xor(src, m_xor, *m_position); // obfuscate in-place
if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) {
throw std::ios_base::failure("AutoFile::write_buffer: write failed");
}
if (m_position) *m_position += src.size();
}
bool AutoFile::Commit()
{
return ::FileCommit(m_file);

View file

@ -465,6 +465,9 @@ public:
::Unserialize(*this, obj);
return *this;
}
//! Write a mutable buffer more efficiently than write(), obfuscating the buffer in-place.
void write_buffer(std::span<std::byte> src);
};
using BufferData = std::vector<std::byte>;
@ -657,4 +660,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;
BufferData m_buf;
size_t m_buf_pos{0};
public:
explicit BufferedWriter(S& stream, size_t size = 1 << 20) : 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

View file

@ -595,6 +595,95 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content)
}
}
BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content)
{
for (int rep{0}; rep < 10; ++rep) {
const size_t file_size{1 + m_rng.randrange<size_t>(1 << 17)};
const size_t buf_size{1 + m_rng.randrange(file_size)};
const FlatFilePos pos{0, 0};
const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE};
const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE};
const std::vector obfuscation{m_rng.randbytes<std::byte>(8)};
{
BufferData test_data{m_rng.randbytes<std::byte>(file_size)};
AutoFile direct_file{test_direct.Open(pos, false), obfuscation};
AutoFile buffered_file{test_buffered.Open(pos, false), obfuscation};
BufferedWriter buffered{buffered_file, buf_size};
for (size_t total_written{0}; total_written < file_size;) {
const size_t write_size{Assert(std::min(1 + m_rng.randrange(m_rng.randbool() ? buf_size : 2 * buf_size), file_size - total_written))};
auto current_span = std::span{test_data}.subspan(total_written, write_size);
direct_file.write(current_span);
buffered.write(current_span);
total_written += write_size;
}
// Destructors of AutoFile and BufferedWriter will flush/close here
}
// Compare the resulting files
BufferData direct_result{file_size};
{
AutoFile verify_direct{test_direct.Open(pos, true), obfuscation};
verify_direct.read(direct_result);
BufferData excess_byte{1};
BOOST_CHECK_EXCEPTION(verify_direct.read(excess_byte), std::ios_base::failure, HasReason{"end of file"});
}
BufferData buffered_result{file_size};
{
AutoFile verify_buffered{test_buffered.Open(pos, true), obfuscation};
verify_buffered.read(buffered_result);
BufferData excess_byte{1};
BOOST_CHECK_EXCEPTION(verify_buffered.read(excess_byte), std::ios_base::failure, HasReason{"end of file"});
}
BOOST_CHECK_EQUAL_COLLECTIONS(
direct_result.begin(), direct_result.end(),
buffered_result.begin(), buffered_result.end()
);
try {
fs::remove(test_direct.FileName(pos));
fs::remove(test_buffered.FileName(pos));
} catch (...) {}
}
}
BOOST_AUTO_TEST_CASE(buffered_writer_reader)
{
const uint32_t v1{m_rng.rand32()}, v2{m_rng.rand32()}, v3{m_rng.rand32()};
const fs::path test_file{m_args.GetDataDirBase() / "test_buffered_write_read.bin"};
// Write out the values through a precisely sized BufferedWriter
{
AutoFile file{fsbridge::fopen(test_file, "w+b")};
BufferedWriter f(file, sizeof(v1) + sizeof(v2) + sizeof(v3));
f << v1 << v2;
f.write(std::as_bytes(std::span{&v3, 1}));
}
// Read back and verify using BufferedReader
{
AutoFile file{fsbridge::fopen(test_file, "rb")};
uint32_t _v1{0}, _v2{0}, _v3{0};
BufferedReader f(std::move(file), sizeof(v1) + sizeof(v2) + sizeof(v3));
f >> _v1 >> _v2;
f.read(std::as_writable_bytes(std::span{&_v3, 1}));
BOOST_CHECK_EQUAL(_v1, v1);
BOOST_CHECK_EQUAL(_v2, v2);
BOOST_CHECK_EQUAL(_v3, v3);
}
try { fs::remove(test_file); } catch (...) {}
}
BOOST_AUTO_TEST_CASE(streams_hashed)
{
DataStream stream{};