mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04:00
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:
parent
c0e8ef1b7a
commit
652b4e3de5
4 changed files with 151 additions and 10 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{};
|
||||
|
|
Loading…
Add table
Reference in a new issue