optimization: Bulk serialization writes in WriteBlockUndo and WriteBlock

Added `AutoFile::write_large` 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.

`DataStream` constructor was also added to enable presized serialization and writing in a single command.

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

> cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release && cmake --build build -j$(nproc) && build/bin/bench_bitcoin -filter='WriteBlockBench' -min-time=10000

> C++ compiler .......................... AppleClang 16.0.0.16000026

Before:
|               ns/op |                op/s |    err% |     total | benchmark
|--------------------:|--------------------:|--------:|----------:|:----------
|        5,182,762.89 |              192.95 |    0.5% |     11.04 | `WriteBlockBench`

After:
|               ns/op |                op/s |    err% |     total | benchmark
|--------------------:|--------------------:|--------:|----------:|:----------
|        1,791,444.08 |              558.21 |    0.9% |     11.08 | `WriteBlockBench`

> C++ compiler .......................... GNU 13.3.0

Before:
|               ns/op |                op/s |    err% |          ins/op |          cyc/op |    IPC |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|        4,128,530.90 |              242.22 |    3.8% |   19,358,001.33 |    8,601,983.31 |  2.250 |   3,079,334.76 |    0.4% |     10.64 | `WriteBlockBench`

After:
|               ns/op |                op/s |    err% |          ins/op |          cyc/op |    IPC |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|        3,130,556.05 |              319.43 |    4.7% |   17,305,378.56 |    6,457,946.37 |  2.680 |   2,579,854.87 |    0.3% |     10.83 | `WriteBlockBench`

Co-authored-by: Cory Fields <cory-nospam-@coryfields.com>
Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
This commit is contained in:
Lőrinc 2025-01-26 19:44:03 +01:00
parent b3ab94b12c
commit 5a1c2bd341
4 changed files with 46 additions and 7 deletions

View File

@ -959,14 +959,14 @@ bool BlockManager::WriteBlockUndo(const CBlockUndo& blockundo, BlockValidationSt
}
// Write index header
fileout << GetParams().MessageStart() << blockundo_size;
BufferedFileW(fileout, HEADER_BYTE_SIZE) << GetParams().MessageStart() << blockundo_size;
pos.nPos += HEADER_BYTE_SIZE;
{
// Calculate checksum
HashWriter hasher{};
hasher << block.pprev->GetBlockHash() << blockundo;
// Write undo data & checksum
fileout << blockundo << hasher.GetHash();
BufferedFileW(fileout, blockundo_size + sizeof(uint256)) << blockundo << hasher.GetHash();
}
// rev files are written in block height order, whereas blk files are written as blocks come in (often out of order)
@ -1117,10 +1117,10 @@ FlatFilePos BlockManager::WriteBlock(const CBlock& block, int nHeight)
}
// Write index header
fileout << GetParams().MessageStart() << block_size;
BufferedFileW(fileout, HEADER_BYTE_SIZE) << GetParams().MessageStart() << block_size;
pos.nPos += HEADER_BYTE_SIZE;
// Write block
fileout << TX_WITH_WITNESS(block);
BufferedFileW(fileout, block_size) << TX_WITH_WITNESS(block);
return pos;
}

View File

@ -102,6 +102,16 @@ void AutoFile::write(Span<const std::byte> src)
}
}
void AutoFile::write_large(Span<std::byte> src)
{
if (!m_file) throw std::ios_base::failure("AutoFile::write_large: 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_large: write failed");
}
if (m_position) *m_position += src.size();
}
bool AutoFile::Commit()
{
return ::FileCommit(m_file);

View File

@ -163,6 +163,7 @@ public:
typedef vector_type::reverse_iterator reverse_iterator;
explicit DataStream() = default;
explicit DataStream(size_type n) { reserve(n); }
explicit DataStream(Span<const uint8_t> sp) : DataStream{AsBytes(sp)} {}
explicit DataStream(Span<const value_type> sp) : vch(sp.data(), sp.data() + sp.size()) {}
@ -452,6 +453,7 @@ public:
void read(Span<std::byte> dst);
void ignore(size_t nSize);
void write(Span<const std::byte> src);
void write_large(Span<std::byte> src); // Note that src will be mutated
template <typename T>
AutoFile& operator<<(const T& obj)
@ -468,6 +470,32 @@ public:
}
};
class BufferedFileW
{
AutoFile& m_file;
uint32_t m_buffer_size;
DataStream m_buf;
public:
explicit BufferedFileW(AutoFile& file, const uint32_t buffer_size)
: m_file(file), m_buffer_size{buffer_size}, m_buf{buffer_size} {}
~BufferedFileW()
{
Assert(m_buf.size() <= m_buffer_size);
m_file.write_large(m_buf);
}
void write(Span<const std::byte> src) { m_buf.write(src); }
template <typename T>
BufferedFileW& operator<<(const T& obj)
{
Serialize(m_buf, obj);
return *this;
}
};
class BufferedFileR
{
DataStream m_buf;

View File

@ -572,11 +572,12 @@ BOOST_AUTO_TEST_CASE(streams_datastream_write_large)
const uint32_t v1{m_rng.rand32()}, v2{m_rng.rand32()}, v3{m_rng.rand32()};
const fs::path tmp_path{m_args.GetDataDirBase() / "test_datastream_write_large.bin"};
// Write out the values to file
// Write out the values through a precisely sized BufferedFileW
{
AutoFile file{fsbridge::fopen(tmp_path, "w+b")};
file << v1 << v2;
file.write(AsBytes(Span{&v3, 1}));
BufferedFileW f(file, sizeof(v1) + sizeof(v2) + sizeof(v3));
f << v1 << v2;
f.write(AsBytes(Span{&v3, 1}));
}
// Read back and verify using BufferedFileR
{