[IBD] coins: reduce lookups in dbcache layer propagation #33602

pull l0rinc wants to merge 2 commits into bitcoin:master from l0rinc:l0rinc/BatchWrite-lookup-optimization changing 5 files +20 −26
  1. l0rinc commented at 6:32 pm on October 12, 2025: contributor

    This change is part of [IBD] - Tracking PR for speeding up Initial Block Download

    Summary

    Previously, when the parent coins cache had no entry and the child did, BatchWrite performed a find followed by try_emplace, which resulted in multiple SipHash computations and bucket traversals on the common insert path. On a different path, these caches were recreated needlessly for every block connection.

    Fix for double fetch

    This change uses a single leading try_emplace and branches on the returned inserted flag. In the FRESH && SPENT case (not used in production, only exercised by tests), we erase the just-inserted placeholder (which is constant time with no rehash anyway). Semantics are unchanged for all valid parent/child state combinations.

    This change is a minimal version of bitcoin/bitcoin@723c49b (#32128) and draws simplification ideas bitcoin/bitcoin@ae76ec7 (#30673) and #30326.

    Fix for temporary cache recreation

    Related to parent cache propagation, the second commit makes it possible to avoid destructuring-recreating-destructuring of these short-live parent caches created for each new block. A few temporary CCoinsViewCache’s are destructed right after the Flush(), therefore it is not necessary to call ReallocateCache to recreate them right before they’re killed anyway.

    This change was based on a subset of #28945, the original authors and relevant commenters were added as coauthors to this version.


    Reindex-chainstate indicates ~1% speedup.

     0COMMITS="647cdb4f7e8041affed887e2325ee03a91078bb1 0b0c3293ffd75afb27dadc0b28426b40132a8c6b"; \                       
     1STOP=909090; DBCACHE=4500; \                                                                                                                                                     
     2CC=gcc; CXX=g++; \                                                                      
     3BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \                                                                                        
     4(echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done; echo "") && \
     5hyperfine \                                                                             
     6  --sort command \                                                                      
     7  --runs 2 \                                                                            
     8  --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
     9  --parameter-list COMMIT ${COMMITS// /,} \                                             
    10  --prepare "killall bitcoind 2>/dev/null; rm -f $DATA_DIR/debug.log; git checkout {COMMIT}; git clean -fxd; git reset --hard && \
    11    cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind && \
    12    ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20" \
    13  --cleanup "cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \                                                                                                   
    14  "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0"
    15
    16647cdb4f7e Merge bitcoin/bitcoin#33311: net: Quiet down logging when router doesn't support natpmp/pcp
    170b0c3293ff validation: don't reallocate cache for short-lived CCoinsViewCache                                                                                                    
    18
    19Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=909090 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 647cdb4f7e8041affed887e2325ee03a91078bb1)
    20  Time (mean ± σ):     16233.508 s ±  9.501 s    [User: 19064.578 s, System: 951.672 s]                                                                                          
    21  Range (min  max):   16226.790 s  16240.226 s    2 runs                                                                                                                       
    22                                                                                        
    23Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=909090 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 0b0c3293ffd75afb27dadc0b28426b40132a8c6b)
    24  Time (mean ± σ):     16039.626 s ± 17.284 s    [User: 18870.130 s, System: 950.722 s]
    25  Range (min  max):   16027.405 s  16051.848 s    2 runs
    26  
    27Relative speed comparison
    28        1.01 ±  0.00  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=909090 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 647cdb4f7e8041affed887e2325ee03a91078bb1)
    29        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=909090 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 0b0c3293ffd75afb27dadc0b28426b40132a8c6b)
    
  2. DrahtBot commented at 6:32 pm on October 12, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33602.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK andrewtoth, optout21

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33866 (refactor: Let CCoinsViewCache::BatchWrite return void by TheCharlatan)
    • #33512 (coins: use number of dirty cache entries in flush warnings/logs by l0rinc)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. in src/coins.cpp:252 in 195fc2fe10 outdated
    244@@ -245,12 +245,14 @@ bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &ha
    245     return true;
    246 }
    247 
    248-bool CCoinsViewCache::Flush() {
    249+bool CCoinsViewCache::Flush(bool reallocate_cache) {
    


    optout21 commented at 9:36 am on October 14, 2025:
    How about adding a default value true to the new reallocate_cache parameter, to suggest that that is the more common usage pattern? It also would reduce the diff a bit.

    l0rinc commented at 1:51 pm on October 14, 2025:
    I thought about that, it’s what the original PR did, but we want to see exactly which ones are reallocating and which aren’t - I don’t like implicit behavior, it’s not obvious for this to be true or false by default, both are valid and expected usages so I prefer making it explicit

    optout21 commented at 2:14 pm on October 14, 2025:
    Fair enough. It’s called in more places with ’true’ than with ‘false’, and the original version was corresponding to the ’true’ case, that’s the ’true’ alternative looks more a default. What is important is that comments are added to all places where it is called with ‘false’.

    l0rinc commented at 0:09 am on October 16, 2025:
    Rebased, added explanation to commit message

    andrewtoth commented at 5:33 pm on November 1, 2025:
    I also think it would make sense to have a default true. However, if we want to be more explicit about intent to reuse the cache rather than implementation details of what we do if we don’t, should we rename this parameter something more descriptive like will_reuse? That is the first thing that came to mind, maybe there are better names. reallocate_cache is an implementation detail and may not be obvious to callers. What we want to communicate to callers is whether they will use this cache again or if it will immediately fall out of scope.

    l0rinc commented at 9:59 pm on November 1, 2025:

    Changed it to will_reuse_cache - I didn’t think a lot about the name, I still hope we can completely get rid of the reallocation, I don’t think it makes a lot of sense this way. But at least let’s turn it off for the cases where it’s obviously wrong.

    I also think it would make sense to have a default true.

    We have multiple cases for both, it’s not obvious why any should be default. I’d rather prefer explicit in this case.


    andrewtoth commented at 10:07 pm on November 1, 2025:

    I still hope we can completely get rid of the reallocation, I don’t think it makes a lot of sense this way.

    Well, we would need to get rid of the custom allocator. I think that would be a big perf hit…

    We have multiple cases for both, it’s not obvious why any should be default.

    Default IMO should be we can reuse, because it doesn’t break if we accidentally pass the reuse option when we end up throwing it away anyways. The reverse is not true. The non-reuse case should be the only one where we have to be explicit.


    l0rinc commented at 10:20 pm on November 1, 2025:

    we would need to get rid of the custom allocator

    I’m not so sure, I’d have different dbcache sizes during IBD and only reallocate after it finishes, since we don’t actually need to narrow the cache size during IBD. After your input fetcher change we may never need to narrow it. We’ll see, let’s discuss that separately.

    The non-reuse case should be the only one where we have to be explicit.

    I explicitly wanted to avoid that, but let me know if you insist, I don’t mind that much.


    andrewtoth commented at 10:26 pm on November 1, 2025:

    I explicitly wanted to avoid that, but let me know if you insist, I don’t mind that much.

    Don’t you think it’s safer to have the case that won’t break either way be default, and then the performant case that can break if you pick the wrong option be explicit?


    l0rinc commented at 10:30 pm on November 1, 2025:
    Maybe, not sure, but I took your advice and pushed.
  4. in src/coins.cpp:188 in b23c6a1fc8 outdated
    181@@ -182,18 +182,16 @@ void CCoinsViewCache::SetBestBlock(const uint256 &hashBlockIn) {
    182 
    183 bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlockIn) {
    184     for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {
    185-        // Ignore non-dirty entries (optimization).
    186-        if (!it->second.IsDirty()) {
    187+        if (!it->second.IsDirty()) { // TODO a cursor can only contain dirty entries
    


    optout21 commented at 9:38 am on October 14, 2025:
    This TODO (and the one few lines below) seems to be independent of the change of this commit (it just exposes a pre-existing TODO condition), maybe isolate in a pre commit for clarity.

    l0rinc commented at 1:49 pm on October 14, 2025:
    Yes, it just documents that what we’re modifying here is actually incorrect and needs to be fixed in the future.

    l0rinc commented at 0:09 am on October 16, 2025:
    Added explanation to commit message
  5. optout21 commented at 9:38 am on October 14, 2025: none

    Concept ACK 195fc2fe102197eae27c33b5e3969a6b15d1253a

    Nice optimization

  6. sipa commented at 1:51 pm on October 14, 2025: member
    Why is this Draft?
  7. l0rinc marked this as ready for review on Oct 14, 2025
  8. DrahtBot added the label Needs rebase on Oct 15, 2025
  9. l0rinc force-pushed on Oct 16, 2025
  10. l0rinc commented at 0:10 am on October 16, 2025: contributor
    Rebased after #32313.
  11. DrahtBot removed the label Needs rebase on Oct 16, 2025
  12. in src/coins.cpp:193 in 6657e5127c outdated
    198-                // Create the coin in the parent cache, move the data up
    199-                // and mark it as dirty.
    200-                itUs = cacheCoins.try_emplace(it->first).first;
    201+        auto [itUs, inserted]{cacheCoins.try_emplace(it->first)};
    202+        if (inserted) {
    203+            if (it->second.IsFresh() && it->second.coin.IsSpent()) {
    


    andrewtoth commented at 5:27 pm on November 1, 2025:
    Can we hoist this check out before the try_emplace, and just continue if this is the case?

    l0rinc commented at 9:59 pm on November 1, 2025:

    You mean something like:

    0if (it->second.IsFresh() && it->second.coin.IsSpent() && !cacheCoins.contains(it->first)) {
    1    continue; // nothing to propagate
    2}
    

    Wouldn’t that defeat the purpose, doing the check twice? Or you mean that we don’t have to check cacheCoins for presence and should update coins_tests/ccoins_write to not have the false values? That’s why I’ve added a TODO instead, I want this PR to be simpler and do the cleanup separately. What do you think?


    andrewtoth commented at 10:03 pm on November 1, 2025:

    This check

    0if (it->second.IsFresh() && it->second.coin.IsSpent()) {
    

    Does not depend on itUs or cacheCoins, right?

    So we can move it up to

    0if (!it->second.IsDirty() || ((it->second.IsFresh() && it->second.coin.IsSpent())) { // TODO a cursor can only contain dirty entries
    

    Right?


    l0rinc commented at 10:16 pm on November 1, 2025:

    Does not depend on itUs or cacheCoins, right?

    Not exactly, we’re only executing that branch when cacheCoins was missing it->first. Doing your suggested:

    0if (!it->second.IsDirty() || (it->second.IsFresh() && it->second.coin.IsSpent())) { // TODO a cursor can only contain dirty entries
    1    continue;
    2}
    

    results in the following test failures

    0cmake -B build  -DCMAKE_BUILD_TYPE=Debug && cmake --build build && ./build/bin/test_bitcoin --run_test=coins_tests
    1
    2test/coins_tests.cpp:799: error: in "coins_tests/ccoins_write": check GetCoinsMapEntry(test.cache.map()) == *expected_coin has failed [-1, 0 != -1, 1]
    3test/coins_tests.cpp:799: error: in "coins_tests/ccoins_write": check GetCoinsMapEntry(test.cache.map()) == *expected_coin has failed [-1, 2 != std::nullopt]
    4test/coins_tests.cpp:799: error: in "coins_tests/ccoins_write": check GetCoinsMapEntry(test.cache.map()) == *expected_coin has failed [-1, 3 != std::nullopt]
    5test/coins_tests.cpp:801: error: in "coins_tests/ccoins_write": exception std::logic_error expected but not raised
    6test/coins_tests.cpp:801: error: in "coins_tests/ccoins_write": exception std::logic_error expected but not raised
    7test/coins_tests.cpp:801: error: in "coins_tests/ccoins_write": exception std::logic_error expected but not raised
    8test/coins_tests.cpp:801: error: in "coins_tests/ccoins_write": exception std::logic_error expected but not raised
    

    That’s why I asked:

    Or you mean that we don’t have to check cacheCoins for presence and should update coins_tests/ccoins_write to not have the false values?


    andrewtoth commented at 10:35 pm on November 1, 2025:
    Ahh right, we handle this case even though it can’t be hit in production. I suppose it’s valuable for regression testing, so we throw instead of silently allowing this.
  13. coins: reduce lookups in dbcache layer propagation
    Previously, when the parent coins cache had no entry and the child did, `BatchWrite` performed a find followed by `try_emplace`, which resulted in multiple `SipHash` computations and bucket traversals on the common insert path.
    
    This change uses a single leading `try_emplace` and branches on the returned `inserted` flag.
    In the `FRESH && SPENT` case (only exercised by tests), we erase the just-inserted placeholder (which is constant time with no rehash anyway).
    Semantics are unchanged for all valid parent/child state combinations.
    
    This change is a minimal version of https://github.com/bitcoin/bitcoin/pull/32128/commits/723c49b63bb10da843fbb6efc6928dca415cc47f and draws simplification ideas https://github.com/bitcoin/bitcoin/pull/30673/commits/ae76ec7bcff0a08a61f294882a71e46d177b009f.
    
    Added TODO versions for related pre-existing issues that should be fixed in follow-ups.
    
    Co-authored-by: Martin Ankerl <martin.ankerl@gmail.com>
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
    Co-authored-by: optout <13562139+optout21@users.noreply.github.com>
    c8f5e446dc
  14. l0rinc force-pushed on Nov 1, 2025
  15. l0rinc force-pushed on Nov 1, 2025
  16. in src/test/fuzz/coinscache_sim.cpp:410 in e4e4f05f18 outdated
    401@@ -402,14 +402,6 @@ FUZZ_TARGET(coinscache_sim)
    402                 caches.back()->Sync();
    403             },
    404 
    405-            [&]() { // Flush + ReallocateCache.
    406-                // Apply to simulation data.
    407-                flush();
    408-                // Apply to real caches.
    409-                caches.back()->Flush();
    410-                caches.back()->ReallocateCache();
    


    andrewtoth commented at 7:12 pm on November 2, 2025:
    I believe this case should have been removed in #25325, which was merged just after this was introduced in #27011. So it likely got missed that ReallocateCache is always called.
  17. in src/coins.h:442 in e4e4f05f18
    438@@ -439,9 +439,10 @@ class CCoinsViewCache : public CCoinsViewBacked
    439      * Push the modifications applied to this cache to its base and wipe local state.
    440      * Failure to call this method or Sync() before destruction will cause the changes
    441      * to be forgotten.
    442+     * If specified, the cache will be cleared and reallocated for memory efficiency after flushing.
    


    andrewtoth commented at 7:23 pm on November 2, 2025:
    0     * If will_reuse_cache is false, the cache will retain the same memory footprint after flushing and should be destroyed to deallocate.
    

    l0rinc commented at 8:41 pm on November 2, 2025:
  18. validation: don't reallocate cache for short-lived CCoinsViewCache
    A few temporary `CCoinsViewCache`'s are destructed right after the `Flush()`, therefore it is not necessary to call `ReallocateCache` to recreate them right before they're killed anyway.
    
    * `Flush()` - retains existing functionality;
    * `Flush(/*will_reuse_cache=*/false)` - skips destruction and reallocation of the parent cache since it will soon go out of scope anyway;
    
    For the `will_reuse_cache` parameter we want to see exactly which ones will reallocate memory and which won't - since both can be valid usages.
    
    This change was based on a subset of https://github.com/bitcoin/bitcoin/pull/28945.
    
    Co-authored-by: Martin Ankerl <martin.ankerl@gmail.com>
    0ac969cddf
  19. l0rinc force-pushed on Nov 2, 2025
  20. andrewtoth approved
  21. andrewtoth commented at 10:47 pm on November 2, 2025: contributor

    utACK 0ac969cddfdba52f7947e9b140ef36e2b19c2c41

    I did not verify the speedup, but it is a nice optimization that will benefit every Flush.

  22. DrahtBot requested review from optout21 on Nov 2, 2025
  23. optout21 commented at 12:13 pm on November 3, 2025: none

    utACK 0ac969cddfdba52f7947e9b140ef36e2b19c2c41

    A performance optimization, in a sensitive place but well localized.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-11-21 21:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me