From 90bf35a530078c6450d1ed8c02b9740c1084673a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 10 Apr 2026 11:54:32 +0200 Subject: [PATCH 1/9] bench: drop duplicate balance benchmark `WalletBalanceMine` duplicated `WalletBalanceClean` exactly. Both registrations called `WalletBalance(bench, /*set_dirty=*/false, /*add_mine=*/true)`. No runtime reproducer is needed here because the duplicate is visible in the registration code itself. Remove the duplicate registration so the balance benchmark list stays distinct. --- src/bench/wallet_balance.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bench/wallet_balance.cpp b/src/bench/wallet_balance.cpp index 45f23494280a..d9cd4bbb9521 100644 --- a/src/bench/wallet_balance.cpp +++ b/src/bench/wallet_balance.cpp @@ -62,11 +62,9 @@ static void WalletBalance(benchmark::Bench& bench, const bool set_dirty, const b static void WalletBalanceDirty(benchmark::Bench& bench) { WalletBalance(bench, /*set_dirty=*/true, /*add_mine=*/true); } static void WalletBalanceClean(benchmark::Bench& bench) { WalletBalance(bench, /*set_dirty=*/false, /*add_mine=*/true); } -static void WalletBalanceMine(benchmark::Bench& bench) { WalletBalance(bench, /*set_dirty=*/false, /*add_mine=*/true); } static void WalletBalanceWatch(benchmark::Bench& bench) { WalletBalance(bench, /*set_dirty=*/false, /*add_mine=*/false); } BENCHMARK(WalletBalanceDirty); BENCHMARK(WalletBalanceClean); -BENCHMARK(WalletBalanceMine); BENCHMARK(WalletBalanceWatch); } // namespace wallet From 975bad7b10ca340aeee830e3de8bb6e47922187b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 15:57:49 +0200 Subject: [PATCH 2/9] bench: fix ephemeral spend inputs `MempoolCheckEphemeralSpends` only filled `tx2.vin[0]` in a loop. That left the rest of the inputs with default prevouts and built the wrong package shape. Write each prevout to `vin[i]` instead and assert that the last child input spends the last parent output. --- src/bench/mempool_ephemeral_spends.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bench/mempool_ephemeral_spends.cpp b/src/bench/mempool_ephemeral_spends.cpp index f0d8eb0bea7b..afaa5a1f23d1 100644 --- a/src/bench/mempool_ephemeral_spends.cpp +++ b/src/bench/mempool_ephemeral_spends.cpp @@ -59,8 +59,8 @@ static void MempoolCheckEphemeralSpends(benchmark::Bench& bench) CMutableTransaction tx2; tx2.vin.resize(tx1.vout.size()); for (size_t i = 0; i < tx2.vin.size(); i++) { - tx2.vin[0].prevout.hash = parent_txid; - tx2.vin[0].prevout.n = i; + tx2.vin[i].prevout.hash = parent_txid; + tx2.vin[i].prevout.n = i; } tx2.vout.resize(1); @@ -71,6 +71,8 @@ static void MempoolCheckEphemeralSpends(benchmark::Bench& bench) const CTransactionRef tx2_r{MakeTransactionRef(tx2)}; AddTx(tx1_r, pool); + assert(tx2_r->vin.back().prevout.hash == parent_txid); + assert(tx2_r->vin.back().prevout.n == tx2_r->vin.size() - 1); uint32_t iteration{0}; From 9177dbb44f82ace11cad005ebf12a0ea814f1b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 10 Apr 2026 12:37:05 +0200 Subject: [PATCH 3/9] bench: guard `setup()` against shared samples `setup()` in nanobench runs once per epoch, while an epoch can still execute the benchmark body multiple times. With the default multi-epoch benchmarking, that makes it easy to mistake per-sample setup for per-iteration setup and silently benchmark changing state inside one sample. Fail fast when `setup()` is combined with anything other than `epochIterations(1)`. --- src/bench/nanobench.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bench/nanobench.h b/src/bench/nanobench.h index 1f798b848b3d..12a03bc42d71 100644 --- a/src/bench/nanobench.h +++ b/src/bench/nanobench.h @@ -40,6 +40,7 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// #include // high_resolution_clock +#include // assert #include // memcpy #include // for std::ostream* custom output target in Config #include // all names @@ -1238,6 +1239,8 @@ class SetupRunner { template ANKERL_NANOBENCH_NO_SANITIZE("integer") Bench& run(Op&& op) { + assert(mBench.epochIterations() == 1 && + "setup() runs once per epoch, not once per iteration; use epochIterations(1) when setup() must reset state for each timed call"); return mBench.runImpl(mSetupOp, std::forward(op)); } From 6e3ee8c6bae0b94c1d04eae5d527064dc038082e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 16:31:13 +0200 Subject: [PATCH 4/9] bench: add reviewer checks for stateful runs Add commented-out assertions ahead of the fixes in this stack. Each assertion can be uncommented on the pre-fix code to confirm the benchmark does not start each timed call from the same state. The follow-up commits uncomment these checks and make them pass. --- src/bench/chacha20.cpp | 2 ++ src/bench/lockedpool.cpp | 4 +++- src/bench/pool.cpp | 2 ++ src/bench/readwriteblock.cpp | 3 +++ src/bench/rollingbloom.cpp | 3 +++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp index 371651771f79..77c3f2f7fb39 100644 --- a/src/bench/chacha20.cpp +++ b/src/bench/chacha20.cpp @@ -26,6 +26,7 @@ static void CHACHA20(benchmark::Bench& bench, size_t buffersize) std::vector out(buffersize, {}); bench.batch(in.size()).unit("byte").run([&] { ctx.Crypt(in, out); + // assert(out[0] == std::byte{0x76}); }); } @@ -38,6 +39,7 @@ static void FSCHACHA20POLY1305(benchmark::Bench& bench, size_t buffersize) std::vector out(buffersize + FSChaCha20Poly1305::EXPANSION); bench.batch(in.size()).unit("byte").run([&] { ctx.Encrypt(in, aad, out); + // assert(out[0] == std::byte{0x9f}); }); } diff --git a/src/bench/lockedpool.cpp b/src/bench/lockedpool.cpp index 27fd609a52dc..31f55ab779b2 100644 --- a/src/bench/lockedpool.cpp +++ b/src/bench/lockedpool.cpp @@ -11,6 +11,7 @@ #define ASIZE 2048 #define MSIZE 2048 +static constexpr uint32_t INITIAL_STATE{0x12345678}; static void BenchLockedPool(benchmark::Bench& bench) { @@ -19,8 +20,9 @@ static void BenchLockedPool(benchmark::Bench& bench) Arena b(synth_base, synth_size, 16); std::vector addr{ASIZE, nullptr}; - uint32_t s = 0x12345678; + uint32_t s = INITIAL_STATE; bench.run([&] { + // assert(s == INITIAL_STATE); int idx = s & (addr.size() - 1); if (s & 0x80000000) { b.free(addr[idx]); diff --git a/src/bench/pool.cpp b/src/bench/pool.cpp index cf4ba132bf43..aadc233a40eb 100644 --- a/src/bench/pool.cpp +++ b/src/bench/pool.cpp @@ -15,11 +15,13 @@ template void BenchFillClearMap(benchmark::Bench& bench, Map& map) { size_t batch_size = 5000; + const size_t empty_bucket_count = map.bucket_count(); // make sure each iteration of the benchmark contains exactly 5000 inserts and one clear. // do this at least 10 times so we get reasonable accurate results bench.batch(batch_size).minEpochIterations(10).run([&] { + // assert(map.bucket_count() == empty_bucket_count); auto rng = ankerl::nanobench::Rng(1234); for (size_t i = 0; i < batch_size; ++i) { map[rng()]; diff --git a/src/bench/readwriteblock.cpp b/src/bench/readwriteblock.cpp index b8e226c6eb96..2c7f1995d612 100644 --- a/src/bench/readwriteblock.cpp +++ b/src/bench/readwriteblock.cpp @@ -31,9 +31,12 @@ static void WriteBlockBench(benchmark::Bench& bench) const auto testing_setup{MakeNoLogFileContext(ChainType::MAIN)}; auto& blockman{testing_setup->m_node.chainman->m_blockman}; const CBlock block{CreateTestBlock()}; + // std::optional expected_pos; bench.run([&] { const auto pos{blockman.WriteBlock(block, 413'567)}; assert(!pos.IsNull()); + // assert(!expected_pos || pos == *expected_pos); + // expected_pos = pos; }); } diff --git a/src/bench/rollingbloom.cpp b/src/bench/rollingbloom.cpp index 8331eb6a7c90..1d474854d492 100644 --- a/src/bench/rollingbloom.cpp +++ b/src/bench/rollingbloom.cpp @@ -15,8 +15,11 @@ static void RollingBloom(benchmark::Bench& bench) { CRollingBloomFilter filter(120000, 0.000001); std::vector data(32); + // std::vector first_insert(32); + // WriteLE32(first_insert.data(), 1); uint32_t count = 0; bench.run([&] { + // assert(!filter.contains(first_insert)); count++; WriteLE32(data.data(), count); filter.insert(data); From 90bf0fdad82bb2dffa76b8d375891df5cb47dc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 15:59:12 +0200 Subject: [PATCH 5/9] bench: reset ChaCha cipher state `CHACHA20` and `FSCHACHA20POLY1305` kept one cipher object alive across timed calls. That advanced the stream position, and `FSChaCha20Poly1305` eventually crossed a rekey boundary. The benchmark now keeps the same reviewer check in place. A fresh zero-key `CHACHA20` stream starts with `0x76`, and a fresh zero-key `FSChaCha20Poly1305` encryption starts with `0x9f`. Those assertions fail on the old code on the second timed call. Rebuild the cipher in `setup()` for each timed call so the measured work starts from the same state. --- src/bench/chacha20.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp index 77c3f2f7fb39..e214d53235da 100644 --- a/src/bench/chacha20.cpp +++ b/src/bench/chacha20.cpp @@ -8,8 +8,10 @@ #include #include +#include #include #include +#include #include /* Number of bytes to process per iteration */ @@ -20,26 +22,32 @@ static const uint64_t BUFFER_SIZE_LARGE = 1024*1024; static void CHACHA20(benchmark::Bench& bench, size_t buffersize) { std::vector key(32, {}); - ChaCha20 ctx(key); - ctx.Seek({0, 0}, 0); + std::optional ctx; std::vector in(buffersize, {}); std::vector out(buffersize, {}); - bench.batch(in.size()).unit("byte").run([&] { - ctx.Crypt(in, out); - // assert(out[0] == std::byte{0x76}); + bench.batch(in.size()).unit("byte").epochIterations(1) + .setup([&] { + ctx.emplace(key); + ctx->Seek({0, 0}, 0); + }).run([&] { + ctx->Crypt(in, out); + assert(out[0] == std::byte{0x76}); }); } static void FSCHACHA20POLY1305(benchmark::Bench& bench, size_t buffersize) { std::vector key(32); - FSChaCha20Poly1305 ctx(key, 224); + std::optional ctx; std::vector in(buffersize); std::vector aad; std::vector out(buffersize + FSChaCha20Poly1305::EXPANSION); - bench.batch(in.size()).unit("byte").run([&] { - ctx.Encrypt(in, aad, out); - // assert(out[0] == std::byte{0x9f}); + bench.batch(in.size()).unit("byte").epochIterations(1) + .setup([&] { + ctx.emplace(key, 224); + }).run([&] { + ctx->Encrypt(in, aad, out); + assert(out[0] == std::byte{0x9f}); }); } From 74241459a8be11c86460b4b130e980eb8682deb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 15:59:51 +0200 Subject: [PATCH 6/9] bench: reset `BenchLockedPool` state `BenchLockedPool` carried allocator state across timed calls. That changed the work performed after the first call. The benchmark keeps the same reviewer check in place. The initial seed must still be `INITIAL_STATE`, and that assertion fails on the old code on the second timed call. Rebuild the arena in `setup()` so each timed call starts from the same allocator state. --- src/bench/lockedpool.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bench/lockedpool.cpp b/src/bench/lockedpool.cpp index 31f55ab779b2..67eebc9cc8af 100644 --- a/src/bench/lockedpool.cpp +++ b/src/bench/lockedpool.cpp @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include #define ASIZE 2048 @@ -17,27 +19,29 @@ static void BenchLockedPool(benchmark::Bench& bench) { void *synth_base = reinterpret_cast(0x08000000); const size_t synth_size = 1024*1024; - Arena b(synth_base, synth_size, 16); + std::optional b; std::vector addr{ASIZE, nullptr}; uint32_t s = INITIAL_STATE; - bench.run([&] { - // assert(s == INITIAL_STATE); + bench.epochIterations(1) + .setup([&] { + b.emplace(synth_base, synth_size, 16); + addr.assign(ASIZE, nullptr); + s = INITIAL_STATE; + }).run([&] { + assert(s == INITIAL_STATE); int idx = s & (addr.size() - 1); if (s & 0x80000000) { - b.free(addr[idx]); + b->free(addr[idx]); addr[idx] = nullptr; } else if (!addr[idx]) { - addr[idx] = b.alloc((s >> 16) & (MSIZE - 1)); + addr[idx] = b->alloc((s >> 16) & (MSIZE - 1)); } bool lsb = s & 1; s >>= 1; if (lsb) s ^= 0xf00f00f0; // LFSR period 0xf7ffffe0 }); - for (void *ptr: addr) - b.free(ptr); - addr.clear(); } BENCHMARK(BenchLockedPool); From e8da58e7eafae966485be7bd782dc906b739db0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 16:00:54 +0200 Subject: [PATCH 7/9] bench: reset pool allocator maps The pool map benchmarks cleared their maps inside the timed call but reused the grown bucket state. That changed the insertion preconditions for later iterations. The benchmark keeps the same reviewer check in place. It records the reset bucket count and asserts that each timed call starts from that same bucket layout. That fails on the old code on the second timed call. Reset the maps in `setup()` with `rehash(0)` so each timed call starts from the same empty bucket layout. --- src/bench/pool.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/bench/pool.cpp b/src/bench/pool.cpp index aadc233a40eb..370c35c5c507 100644 --- a/src/bench/pool.cpp +++ b/src/bench/pool.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -15,13 +16,15 @@ template void BenchFillClearMap(benchmark::Bench& bench, Map& map) { size_t batch_size = 5000; - const size_t empty_bucket_count = map.bucket_count(); + size_t empty_bucket_count = 0; - // make sure each iteration of the benchmark contains exactly 5000 inserts and one clear. - // do this at least 10 times so we get reasonable accurate results - - bench.batch(batch_size).minEpochIterations(10).run([&] { - // assert(map.bucket_count() == empty_bucket_count); + bench.batch(batch_size).epochIterations(1) + .setup([&] { + map.clear(); + map.rehash(0); + empty_bucket_count = map.bucket_count(); + }).run([&] { + assert(map.bucket_count() == empty_bucket_count); auto rng = ankerl::nanobench::Rng(1234); for (size_t i = 0; i < batch_size; ++i) { map[rng()]; From 6df0aedd139964ca6b65fa24fe7efe639c0ccbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 16:01:35 +0200 Subject: [PATCH 8/9] bench: reset `WriteBlockBench` state `WriteBlockBench` kept appending to one block manager across timed calls. That changed the write position for later iterations. The benchmark keeps the same reviewer check in place. A fresh block manager should write to `FlatFilePos{0, STORAGE_HEADER_BYTES}`, and that assertion fails on the old code on the second timed call. Rebuild the testing setup in `setup()` so each timed call writes into the same fresh block storage state. --- src/bench/readwriteblock.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/bench/readwriteblock.cpp b/src/bench/readwriteblock.cpp index 2c7f1995d612..0285559cb916 100644 --- a/src/bench/readwriteblock.cpp +++ b/src/bench/readwriteblock.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include static CBlock CreateTestBlock() @@ -28,15 +29,18 @@ static CBlock CreateTestBlock() static void WriteBlockBench(benchmark::Bench& bench) { - const auto testing_setup{MakeNoLogFileContext(ChainType::MAIN)}; - auto& blockman{testing_setup->m_node.chainman->m_blockman}; const CBlock block{CreateTestBlock()}; - // std::optional expected_pos; - bench.run([&] { + std::unique_ptr testing_setup; + std::optional expected_pos; + bench.epochIterations(1) + .setup([&] { + testing_setup = MakeNoLogFileContext(ChainType::MAIN); + }).run([&] { + auto& blockman{testing_setup->m_node.chainman->m_blockman}; const auto pos{blockman.WriteBlock(block, 413'567)}; assert(!pos.IsNull()); - // assert(!expected_pos || pos == *expected_pos); - // expected_pos = pos; + assert(!expected_pos || pos == *expected_pos); + expected_pos = pos; }); } From 0c2e2f496dffd31e73140e2bcdc425795e7a4e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 12 Apr 2026 16:01:54 +0200 Subject: [PATCH 9/9] bench: reset rolling bloom state `RollingBloom` kept one filter alive across timed calls. That changed the filter state and the later work it performed. The benchmark keeps the same reviewer check in place. A fresh timed call should not already contain the first inserted value, and that assertion fails on the old code on the second timed call. Reset the filter in `setup()` so each timed call starts from the same empty state. --- src/bench/rollingbloom.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/bench/rollingbloom.cpp b/src/bench/rollingbloom.cpp index 1d474854d492..913127f463b9 100644 --- a/src/bench/rollingbloom.cpp +++ b/src/bench/rollingbloom.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -15,11 +16,16 @@ static void RollingBloom(benchmark::Bench& bench) { CRollingBloomFilter filter(120000, 0.000001); std::vector data(32); - // std::vector first_insert(32); - // WriteLE32(first_insert.data(), 1); + std::vector first_insert(32); + WriteLE32(first_insert.data(), 1); uint32_t count = 0; - bench.run([&] { - // assert(!filter.contains(first_insert)); + bench.epochIterations(1) + .setup([&] { + filter.reset(); + data.assign(32, 0); + count = 0; + }).run([&] { + assert(!filter.contains(first_insert)); count++; WriteLE32(data.data(), count); filter.insert(data);