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;
|
return false;
|
||||||
}
|
}
|
||||||
// Open history file to append
|
// Open history file to append
|
||||||
AutoFile fileout{OpenUndoFile(pos)};
|
AutoFile file{OpenUndoFile(pos)};
|
||||||
if (fileout.IsNull()) {
|
if (file.IsNull()) {
|
||||||
LogError("OpenUndoFile failed for %s while writing", pos.ToString());
|
LogError("OpenUndoFile failed for %s while writing", pos.ToString());
|
||||||
return FatalError(m_opts.notifications, state, _("Failed to write undo data."));
|
return FatalError(m_opts.notifications, state, _("Failed to write undo data."));
|
||||||
}
|
}
|
||||||
|
BufferedWriter fileout{file};
|
||||||
|
|
||||||
// Write index header
|
// Write index header
|
||||||
fileout << GetParams().MessageStart() << blockundo_size;
|
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());
|
LogError("FindNextBlockPos failed for %s while writing", pos.ToString());
|
||||||
return FlatFilePos();
|
return FlatFilePos();
|
||||||
}
|
}
|
||||||
AutoFile fileout{OpenBlockFile(pos)};
|
AutoFile file{OpenBlockFile(pos)};
|
||||||
if (fileout.IsNull()) {
|
if (file.IsNull()) {
|
||||||
LogError("OpenBlockFile failed for %s while writing", pos.ToString());
|
LogError("OpenBlockFile failed for %s while writing", pos.ToString());
|
||||||
m_opts.notifications.fatalError(_("Failed to write block."));
|
m_opts.notifications.fatalError(_("Failed to write block."));
|
||||||
return FlatFilePos();
|
return FlatFilePos();
|
||||||
}
|
}
|
||||||
|
BufferedWriter fileout{file};
|
||||||
|
|
||||||
// Write index header
|
// Write index header
|
||||||
fileout << GetParams().MessageStart() << block_size;
|
fileout << GetParams().MessageStart() << block_size;
|
||||||
|
|
|
@ -91,17 +91,23 @@ void AutoFile::write(std::span<const std::byte> src)
|
||||||
std::array<std::byte, 4096> buf;
|
std::array<std::byte, 4096> buf;
|
||||||
while (src.size() > 0) {
|
while (src.size() > 0) {
|
||||||
auto buf_now{std::span{buf}.first(std::min<size_t>(src.size(), buf.size()))};
|
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());
|
std::copy_n(src.begin(), buf_now.size(), buf_now.begin());
|
||||||
util::Xor(buf_now, m_xor, *m_position);
|
write_buffer(buf_now);
|
||||||
if (std::fwrite(buf_now.data(), 1, buf_now.size(), m_file) != buf_now.size()) {
|
|
||||||
throw std::ios_base::failure{"AutoFile::write: failed"};
|
|
||||||
}
|
|
||||||
src = src.subspan(buf_now.size());
|
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()
|
bool AutoFile::Commit()
|
||||||
{
|
{
|
||||||
return ::FileCommit(m_file);
|
return ::FileCommit(m_file);
|
||||||
|
|
|
@ -465,6 +465,9 @@ public:
|
||||||
::Unserialize(*this, obj);
|
::Unserialize(*this, obj);
|
||||||
return *this;
|
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>;
|
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
|
#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)
|
BOOST_AUTO_TEST_CASE(streams_hashed)
|
||||||
{
|
{
|
||||||
DataStream stream{};
|
DataStream stream{};
|
||||||
|
|
Loading…
Add table
Reference in a new issue