From 520965e2939567e0e5b7bcf598f3891bf4a806c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 26 Mar 2025 12:52:29 +0100 Subject: [PATCH] optimization: bulk serialization reads in `UndoRead`, `ReadBlock` The obfuscation (XOR) operations are currently done byte-by-byte during serialization. Buffering the reads will enable batching the obfuscation operations later. Different operating systems handle file caching differently, so reading larger batches (and processing them from memory) is measurably faster, likely because of fewer native fread calls and reduced lock contention. Note that `ReadRawBlock` doesn't need buffering since it already reads the whole block directly. Unlike `ReadBlockUndo`, the new `ReadBlock` implementation delegates to `ReadRawBlock`, which uses more memory than a buffered alternative but results in slightly simpler code and a small performance increase (~0.4%). This approach also clearly documents that `ReadRawBlock` is a logical subset of `ReadBlock` functionality. The current implementation, which iterates over a fixed-size buffer, provides a more general alternative to Cory Fields' solution of reading the entire block size in advance. Buffer sizes were selected based on benchmarking to ensure the buffered reader produces performance similar to reading the whole block into memory. Smaller buffers were slower, while larger ones showed diminishing returns. ------ > 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='ReadBlockBench' -min-time=10000 Before: | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 2,271,441.67 | 440.25 | 0.1% | 11.00 | `ReadBlockBench` After: | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 1,738,971.29 | 575.05 | 0.2% | 10.97 | `ReadBlockBench` ------ > 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='ReadBlockBench' -min-time=20000 Before: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 6,895,987.11 | 145.01 | 0.0% | 71,055,269.86 | 23,977,374.37 | 2.963 | 5,074,828.78 | 0.4% | 22.00 | `ReadBlockBench` After: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 5,771,882.71 | 173.25 | 0.0% | 65,741,889.82 | 20,453,232.33 | 3.214 | 3,971,321.75 | 0.3% | 22.01 | `ReadBlockBench` Co-authored-by: maflcko <6399679+maflcko@users.noreply.github.com> Co-authored-by: Ryan Ofsky Co-authored-by: Martin Leitner-Ankerl Co-authored-by: Cory Fields --- src/node/blockstorage.cpp | 12 ++++----- src/streams.h | 46 +++++++++++++++++++++++++++++++- src/test/streams_tests.cpp | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index cf2e388c174..5e1de9f0af4 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -660,11 +660,12 @@ bool BlockManager::ReadBlockUndo(CBlockUndo& blockundo, const CBlockIndex& index const FlatFilePos pos{WITH_LOCK(::cs_main, return index.GetUndoPos())}; // Open history file to read - AutoFile filein{OpenUndoFile(pos, true)}; - if (filein.IsNull()) { + AutoFile file{OpenUndoFile(pos, true)}; + if (file.IsNull()) { LogError("OpenUndoFile failed for %s while reading block undo", pos.ToString()); return false; } + BufferedReader filein{std::move(file)}; try { // Read block @@ -996,15 +997,14 @@ bool BlockManager::ReadBlock(CBlock& block, const FlatFilePos& pos) const block.SetNull(); // Open history file to read - AutoFile filein{OpenBlockFile(pos, /*fReadOnly=*/true)}; - if (filein.IsNull()) { - LogError("OpenBlockFile failed for %s while reading block", pos.ToString()); + std::vector block_data; + if (!ReadRawBlock(block_data, pos)) { return false; } try { // Read block - filein >> TX_WITH_WITNESS(block); + SpanReader{block_data} >> TX_WITH_WITNESS(block); } catch (const std::exception& e) { LogError("Deserialize or I/O error - %s at %s while reading block", e.what(), pos.ToString()); return false; diff --git a/src/streams.h b/src/streams.h index 81d95feaa8e..f19bef5df22 100644 --- a/src/streams.h +++ b/src/streams.h @@ -467,6 +467,8 @@ public: } }; +using DataBuffer = std::vector; + /** Wrapper around an AutoFile& that implements a ring buffer to * deserialize from. It guarantees the ability to rewind a given number of bytes. * @@ -481,7 +483,7 @@ private: uint64_t m_read_pos{0}; //!< how many bytes have been read from this uint64_t nReadLimit; //!< up to which position we're allowed to read uint64_t nRewind; //!< how many bytes we guarantee to rewind - std::vector vchBuf; //!< the buffer + DataBuffer vchBuf; //! read data from the source to fill the buffer bool Fill() { @@ -614,4 +616,46 @@ public: } }; +/** + * Wrapper that buffers reads from an underlying stream. + * Requires underlying stream to support read() and detail_fread() calls + * to support fixed-size and variable-sized reads, respectively. + */ +template +class BufferedReader +{ + S& m_src; + DataBuffer m_buf; + size_t m_buf_pos; + +public: + //! Requires stream ownership to prevent leaving the stream at an unexpected position after buffered reads. + explicit BufferedReader(S&& stream LIFETIMEBOUND, size_t size = 1 << 16) + requires std::is_rvalue_reference_v + : m_src{stream}, m_buf(size), m_buf_pos{size} {} + + void read(std::span dst) + { + if (const auto available{std::min(dst.size(), m_buf.size() - m_buf_pos)}) { + std::copy_n(m_buf.begin() + m_buf_pos, available, dst.begin()); + m_buf_pos += available; + dst = dst.subspan(available); + } + if (dst.size()) { + assert(m_buf_pos == m_buf.size()); + m_src.read(dst); + + m_buf_pos = 0; + m_buf.resize(m_src.detail_fread(m_buf)); + } + } + + template + BufferedReader& operator>>(T&& obj) + { + Unserialize(*this, obj); + return *this; + } +}; + #endif // BITCOIN_STREAMS_H diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 1a44e66932c..6f47b98f297 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -2,6 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include +#include #include #include #include @@ -553,6 +555,58 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand) fs::remove(streams_test_filename); } +BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content) +{ + const size_t file_size{1 + m_rng.randrange(1 << 17)}; + const size_t buf_size{1 + m_rng.randrange(file_size)}; + const FlatFilePos pos{0, 0}; + + const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE}; + const std::vector obfuscation{m_rng.randbytes(8)}; + + // Write out the file with random content + { + AutoFile{test_file.Open(pos, /*read_only=*/false), obfuscation}.write(m_rng.randbytes(file_size)); + } + BOOST_CHECK_EQUAL(fs::file_size(test_file.FileName(pos)), file_size); + + { + AutoFile direct_file{test_file.Open(pos, /*read_only=*/true), obfuscation}; + + AutoFile buffered_file{test_file.Open(pos, /*read_only=*/true), obfuscation}; + BufferedReader buffered_reader{std::move(buffered_file), buf_size}; + + for (size_t total_read{0}; total_read < file_size;) { + const size_t read{Assert(std::min(1 + m_rng.randrange(m_rng.randbool() ? buf_size : 2 * buf_size), file_size - total_read))}; + + DataBuffer direct_file_buffer{read}; + direct_file.read(direct_file_buffer); + + DataBuffer buffered_buffer{read}; + buffered_reader.read(buffered_buffer); + + BOOST_CHECK_EQUAL_COLLECTIONS( + direct_file_buffer.begin(), direct_file_buffer.end(), + buffered_buffer.begin(), buffered_buffer.end() + ); + + total_read += read; + } + + { + DataBuffer excess_byte{1}; + BOOST_CHECK_EXCEPTION(direct_file.read(excess_byte), std::ios_base::failure, HasReason{"end of file"}); + } + + { + DataBuffer excess_byte{1}; + BOOST_CHECK_EXCEPTION(buffered_reader.read(excess_byte), std::ios_base::failure, HasReason{"end of file"}); + } + } + + fs::remove(test_file.FileName(pos)); +} + BOOST_AUTO_TEST_CASE(streams_hashed) { DataStream stream{};