From 3dd815f048c80c9a35f01972e0537eb42531aec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 22 Nov 2025 13:33:09 +0100 Subject: [PATCH] validation: pre-reserve leaves to prevent reallocs with odd vtx count `ComputeMerkleRoot` duplicates the last hash when the input size is odd. If the caller provides a `std::vector` whose capacity equals its size, that extra `push_back` forces a reallocation, doubling its capacity (allocating 3x the necessary memory). This affects roughly half of the created blocks (those with odd transaction counts), causing unnecessary memory fragmentation during every block validation. Fix this by pre-reserving the vector capacity to account for the odd-count duplication. The expression `(size + 1) & ~1ULL` adds 1 to the size and clears the last bit, effectively rounding up to the next even number. This syntax produces optimal assembly across x86/ARM and 32/64-bit platforms for gcc/clang, see https://godbolt.org/z/xzscoq7nv. Also switch from `resize` to `reserve` + `push_back` to eliminate the default construction of `uint256` objects that are immediately overwritten. > ./build/bin/bench_bitcoin -filter='MerkleRoot.*' -min-time=1000 | ns/leaf | leaf/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 43.73 | 22,867,350.51 | 0.0% | 1.10 | `MerkleRoot` | 44.17 | 22,640,349.14 | 0.0% | 1.10 | `MerkleRootWithMutation` Massif memory measurements after show 0.8 MB peak memory usage KB 801.4^ # | # | # | # | # | # | # | # :::::@:::::@: | #:::@@@::@:::::::::::::::@::@:@:::@@:::::::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: | #:::@ @: @:::::::::::::::@::@:@:::@ :::: ::::@::::::@:::::@::::@:::::@: 0 +----------------------------------------------------------------------->s 0 227.5 and the stacks don't show reallocs anymore: 96.37% (790,809B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc. ->35.10% (288,064B) 0x2234AF: allocate (new_allocator.h:151) | ->35.10% (288,064B) 0x2234AF: allocate (allocator.h:203) | ->35.10% (288,064B) 0x2234AF: allocate (alloc_traits.h:614) | ->35.10% (288,064B) 0x2234AF: _M_allocate (stl_vector.h:387) | ->35.10% (288,064B) 0x2234AF: reserve (vector.tcc:79) | ->35.10% (288,064B) 0x2234AF: ToMerkleLeaves, MerkleRoot(ankerl::nanobench::Bench&):::: > (merkle.h:19) | ->35.10% (288,064B) 0x2234AF: operator() (merkle_root.cpp:25) | ->35.10% (288,064B) 0x2234AF: ankerl::nanobench::Bench& ankerl::nanobench::Bench::run Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- src/bench/merkle_root.cpp | 4 ++-- src/consensus/merkle.cpp | 10 +++++----- src/signet.cpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/bench/merkle_root.cpp b/src/bench/merkle_root.cpp index 5be592707ff..0e4c779f142 100644 --- a/src/bench/merkle_root.cpp +++ b/src/bench/merkle_root.cpp @@ -24,9 +24,9 @@ static void MerkleRoot(benchmark::Bench& bench) for (bool mutate : {false, true}) { bench.name(mutate ? "MerkleRootWithMutation" : "MerkleRoot").batch(hashes.size()).unit("leaf").run([&] { std::vector leaves; - leaves.resize(hashes.size()); + leaves.reserve((hashes.size() + 1) & ~1ULL); // capacity rounded up to even for (size_t s = 0; s < hashes.size(); s++) { - leaves[s] = hashes[s]; + leaves.push_back(hashes[s]); } bool mutated{false}; diff --git a/src/consensus/merkle.cpp b/src/consensus/merkle.cpp index 703a824e16f..b0819a1763e 100644 --- a/src/consensus/merkle.cpp +++ b/src/consensus/merkle.cpp @@ -66,9 +66,9 @@ uint256 ComputeMerkleRoot(std::vector hashes, bool* mutated) { uint256 BlockMerkleRoot(const CBlock& block, bool* mutated) { std::vector leaves; - leaves.resize(block.vtx.size()); + leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even for (size_t s = 0; s < block.vtx.size(); s++) { - leaves[s] = block.vtx[s]->GetHash().ToUint256(); + leaves.push_back(block.vtx[s]->GetHash().ToUint256()); } return ComputeMerkleRoot(std::move(leaves), mutated); } @@ -76,10 +76,10 @@ uint256 BlockMerkleRoot(const CBlock& block, bool* mutated) uint256 BlockWitnessMerkleRoot(const CBlock& block) { std::vector leaves; - leaves.resize(block.vtx.size()); - leaves[0].SetNull(); // The witness hash of the coinbase is 0. + leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even + leaves.emplace_back(); // The witness hash of the coinbase is 0. for (size_t s = 1; s < block.vtx.size(); s++) { - leaves[s] = block.vtx[s]->GetWitnessHash().ToUint256(); + leaves.push_back(block.vtx[s]->GetWitnessHash().ToUint256()); } return ComputeMerkleRoot(std::move(leaves)); } diff --git a/src/signet.cpp b/src/signet.cpp index 6524ebff45e..6c1df371aa9 100644 --- a/src/signet.cpp +++ b/src/signet.cpp @@ -58,10 +58,10 @@ static bool FetchAndClearCommitmentSection(const std::span header static uint256 ComputeModifiedMerkleRoot(const CMutableTransaction& cb, const CBlock& block) { std::vector leaves; - leaves.resize(block.vtx.size()); - leaves[0] = cb.GetHash().ToUint256(); + leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even + leaves.push_back(cb.GetHash().ToUint256()); for (size_t s = 1; s < block.vtx.size(); ++s) { - leaves[s] = block.vtx[s]->GetHash().ToUint256(); + leaves.push_back(block.vtx[s]->GetHash().ToUint256()); } return ComputeMerkleRoot(std::move(leaves)); }