coins: avoid redundant prevout preparation allocations #36496

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/coins-reuse-prevout-preparation changing 3 files +32 −27
  1. l0rinc commented at 3:14 PM on October 10, 2026: contributor

    Problem: Block preparation inserts an unused final transaction ID, repeatedly constructs salted transaction-ID sets, and allocates a worker-task vector. Small input queues also submit more tasks than can do useful work.

    Fix: Insert each transaction ID before scanning the following transaction's inputs. Reuse the set's buckets and hash salt and the worker-task vector across blocks. Limit submitted tasks to the smaller of the worker and queued-input counts.

  2. DrahtBot added the label UTXO Db and Indexes on Oct 10, 2026
  3. DrahtBot commented at 3:14 PM on October 10, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK hodlinator
    Stale ACK andrewtoth

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. l0rinc force-pushed on Oct 10, 2026
  5. DrahtBot added the label CI failed on Oct 10, 2026
  6. DrahtBot removed the label CI failed on Oct 10, 2026
  7. l0rinc marked this as ready for review on Oct 11, 2026
  8. in src/coins.cpp:400 in 696ddc5f28
     393 | @@ -394,10 +394,10 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block
     394 |          m_earlier_txids.clear();
     395 |          // Only submit tasks if we have something to fetch.
     396 |          if (m_inputs.size()) {
     397 | -            std::vector<std::function<void()>> tasks(workers_count, [this] {
     398 | +            m_tasks.assign(workers_count, [this] {
     399 |                  while (ProcessInput()) {}
     400 |              });
     401 | -            if (auto futures{m_thread_pool->Submit(std::move(tasks))}) {
     402 | +            if (auto futures{m_thread_pool->Submit(std::move(m_tasks))}) {
    


    hodlinator commented at 9:44 AM on October 13, 2026:

    696ddc5 coins: reuse prevout task vector storage:

    The vector is cast to an r-value reference. (The template type becoming an l-value is banned through !std::is_lvalue_reference_v<R>). The argument to Submit() is never used to construct a new vector inside the method, which means the storage of the underlying vector is never currently "reaped". My inference is that the ThreadPool was written that way since we move from the individual elements of the range.

    It would probably be more honest/correct to allow non-const l-values to be passed into Submit() and remove the move() here. !std::is_const_v<std::remove_reference_t<R>> could be sufficient, and the l-value/r-value nature of R would be up to the caller as was intended when r-value references were introduced.

    <details><summary>Compiling diff</summary>

    diff --git a/src/coins.cpp b/src/coins.cpp
    index e32e7f21d8..bf8c2bd766 100644
    --- a/src/coins.cpp
    +++ b/src/coins.cpp
    @@ -397,7 +397,7 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block
                 m_tasks.assign(std::min(workers_count, m_inputs.size()), [this] {
                     while (ProcessInput()) {}
                 });
    -            if (auto futures{m_thread_pool->Submit(std::move(m_tasks))}) {
    +            if (auto futures{m_thread_pool->Submit(m_tasks)}) {
                     m_futures = std::move(*futures);
                 } else {
                     // Submit can fail if a shared owner of the thread pool outside of this class calls Stop() or
    diff --git a/src/util/threadpool.h b/src/util/threadpool.h
    index 6c16851525..421f956367 100644
    --- a/src/util/threadpool.h
    +++ b/src/util/threadpool.h
    @@ -216,7 +216,7 @@ public:
          *          uncaught exceptions, as they would otherwise be silently discarded.
          */
         template <std::ranges::sized_range R>
    -        requires(!std::is_lvalue_reference_v<R>)
    +        requires(!std::is_const_v<std::remove_reference_t<R>>)
         [[nodiscard]] util::Expected<std::vector<RangeFuture<R>>, SubmitError> Submit(R&& fns) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
         {
             std::vector<RangeFuture<R>> futures;
    

    </details>


    l0rinc commented at 7:05 AM on October 15, 2026:

    Thanks, added you as coauthor and also handled the empty tasks internally


    hodlinator commented at 7:48 AM on October 22, 2026:

    Upon further consideration, influenced by on Ralph's feedback in https://git.fish.foo/bitcoin/bitcoin/pulls/36496#issuecomment-610179; it may be safer to revert my suggestion and instead opt for adding Assume(m_tasks.capacity() >= std::min(workers_count, m_inputs.size())) with an explanatory comment like so:

    diff --git a/src/coins.cpp b/src/coins.cpp
    index cdc5a8858d..88bea6c391 100644
    --- a/src/coins.cpp
    +++ b/src/coins.cpp
    @@ -395,7 +395,10 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block
             m_tasks.assign(std::min(workers_count, m_inputs.size()), [this] {
                 while (ProcessInput()) {}
             });
    -        if (auto futures{m_thread_pool->Submit(m_tasks)}) {
    +        if (auto futures{m_thread_pool->Submit(std::move(m_tasks))}) {
    +            // We expect to be able to reuse the backing array.
    +            Assume(m_tasks.capacity() >= std::min(workers_count, m_inputs.size()));
    +
                 m_futures = std::move(*futures);
             } else {
                 // Submit can fail if a shared owner of the thread pool outside of this class calls Stop() or
    diff --git a/src/test/threadpool_tests.cpp b/src/test/threadpool_tests.cpp
    index 3f802b754f..4811d73215 100644
    --- a/src/test/threadpool_tests.cpp
    +++ b/src/test/threadpool_tests.cpp
    @@ -469,7 +469,6 @@ BOOST_AUTO_TEST_CASE(submit_range_of_tasks_complete_successfully)
         std::array<std::function<int32_t()>, static_cast<size_t>(num_tasks)> array_tasks;
         std::vector<std::function<int32_t()>> vector_tasks;
         vector_tasks.reserve(static_cast<size_t>(num_tasks));
    -    BOOST_CHECK(Assert(threadPool.Submit(vector_tasks))->empty());
         for (const auto i : std::views::iota(int32_t{1}, num_tasks + 1)) {
             array_tasks.at(static_cast<size_t>(i - 1)) = [i, square] { return square(i); };
             vector_tasks.emplace_back([i, square] { return square(i); });
    @@ -477,7 +476,7 @@ BOOST_AUTO_TEST_CASE(submit_range_of_tasks_complete_successfully)
     
         auto futures{std::move(*Assert(threadPool.Submit(std::move(array_tasks))))};
         BOOST_CHECK_EQUAL(futures.size(), static_cast<size_t>(num_tasks));
    -    std::ranges::move(*Assert(threadPool.Submit(vector_tasks)), std::back_inserter(futures));
    +    std::ranges::move(*Assert(threadPool.Submit(std::move(vector_tasks))), std::back_inserter(futures));
         BOOST_CHECK_EQUAL(futures.size(), static_cast<size_t>(num_tasks * 2));
     
         auto squares_sum{0};
    diff --git a/src/util/threadpool.h b/src/util/threadpool.h
    index 80971009ee..c3c11c53ac 100644
    --- a/src/util/threadpool.h
    +++ b/src/util/threadpool.h
    @@ -216,7 +216,7 @@ public:
          *          uncaught exceptions, as they would otherwise be silently discarded.
          */
         template <std::ranges::sized_range R>
    -        requires(!std::is_const_v<std::remove_reference_t<R>>)
    +        requires(!std::is_lvalue_reference_v<R>)
         [[nodiscard]] util::Expected<std::vector<RangeFuture<R>>, SubmitError> Submit(R&& fns) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
         {
             std::vector<RangeFuture<R>> futures;
    

    Ran functional tests to affirm that the Assume() holds.


    l0rinc commented at 10:28 AM on October 22, 2026:

    Changed back to keep task consumption explicit without broadening the API. Skipped the capacity check since Submit preserves the storage and assign() handles any allocation needed.


    hodlinator commented at 11:35 AM on October 22, 2026:

    Still think it's worth adding the Assume() to catch potential changes in behavior of how Submit() uses the argument. Agree the comment was overkill as the reason for m_tasks being a member is already documented in the header. Not going to insist again though.

  9. hodlinator commented at 10:01 AM on October 13, 2026: contributor

    Concept ACK b0debcf4daad3c227be25f64ad192ce9663dedc6

  10. andrewtoth commented at 4:23 AM on October 15, 2026: contributor

    ACK b0debcf4daad3c227be25f64ad192ce9663dedc6

  11. DrahtBot requested review from hodlinator on Oct 15, 2026
  12. coins: omit final txid from prevout classification
    Insert each transaction ID before scanning the following transaction's inputs, omitting the unused final ID.
    This reduces txid insertions from N to N-1 and replaces the separate coinbase seed and trailing insertion with one insertion at the start of the loop.
    9c2bcaf104
  13. coins: reuse prevout buckets and hash salt
    Retain the classification set across blocks to reuse its buckets and avoid reseeding its hasher.
    The set still allocates and frees its nodes for each block.
    9057724461
  14. l0rinc force-pushed on Oct 15, 2026
  15. andrewtoth approved
  16. andrewtoth commented at 11:23 PM on October 19, 2026: contributor

    reACK 483c9d0f448a5bcd6d24e24b00d45a8f22975fb7

  17. hodlinator commented at 9:34 AM on October 22, 2026: contributor

    Performed some measurements which ended up with an increased average run time but a decreased minimum run time. They leave me not worrying too much about unexpected pessimization.

    Before/649bc0a645eb127a9b1e6d078f8d129be6380c87 (PR base)

    ₿ hyperfine --runs=3 "./build/bin/bitcoind -stopatheight=700000 -reindex-chainstate"
    Benchmark 1: ./build/bin/bitcoind -stopatheight=700000 -reindex-chainstate
      Time (mean ± σ):     5747.993 s ±  8.252 s    [User: 12214.062 s, System: 866.596 s]
      Range (min … max):   5738.535 s … 5753.728 s    3 runs
    

    After/483c9d0f448a5bcd6d24e24b00d45a8f22975fb7 (PR tip)

    ₿ hyperfine --runs=3 "./build/bin/bitcoind -stopatheight=700000 -reindex-chainstate"
    Benchmark 1: ./build/bin/bitcoind -stopatheight=700000 -reindex-chainstate
      Time (mean ± σ):     5781.568 s ± 117.178 s    [User: 12371.366 s, System: 875.421 s]
      Range (min … max):   5705.545 s … 5916.514 s    3 runs
    
  18. l0rinc force-pushed on Oct 22, 2026
  19. coins: reuse prevout task vector storage
    `ThreadPool::Submit` binds its range by reference and moves the individual callables without taking the vector's storage, so keep the vector on the overlay for reuse across blocks.
    0fdf36a365
  20. coins: cap prevout tasks at the input count
    Each task drains the shared input queue, so tasks beyond the number of inputs cannot do useful work.
    For an empty queue, submit an empty range and skip the worker notification inside `ThreadPool::Submit` instead of guarding submission at the caller.
    0977948b35
  21. l0rinc force-pushed on Oct 22, 2026
  22. DrahtBot added the label CI failed on Oct 22, 2026
  23. DrahtBot removed the label CI failed on Oct 22, 2026
  24. in src/coins.cpp:407 in 0977948b35
     424 | +            // Interrupt() on a different thread after we call WorkersCount() above. In that case parallel
     425 | +            // fetching will not make progress, so we clear the inputs to fall back to single threaded fetching.
     426 | +            LogWarning("Failed to submit prevout fetch tasks (%s); falling back to single-threaded fetching for this block.", SubmitErrorString(futures.error()));
     427 | +            m_inputs.clear();
     428 | +            StopFetching(); // Assert nothing changed if we failed to start tasks.
     429 |          }
    


    hodlinator commented at 11:46 AM on October 22, 2026:

    nit: We will now perform more work when we have blocks with only have a coinbase tx (previously we would not call ThreadPool::Submit(). In the case that bitcoin as a whole succeeds those blocks are outliers, so I agree with that part of the change.

    However, the warning in the else-block becomes somewhat weird when m_tasks is empty (failing to submit 0 tasks). How about bringing the m_inputs.size() check back for that?

            } else if (m_inputs.size()) {
                // Submit can fail if a shared owner of the thread pool outside of this class calls Stop() or
                // Interrupt() on a different thread after we call WorkersCount() above. In that case parallel
                // fetching will not make progress, so we clear the inputs to fall back to single threaded fetching.
                LogWarning("Failed to submit prevout fetch tasks (%s); falling back to single-threaded fetching for this block.", SubmitErrorString(futures.error()));
                m_inputs.clear();
                StopFetching(); // Assert nothing changed if we failed to start tasks.
            }
    

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: 2026-10-31 09:51 UTC

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