From e6ca996255bb7edaebe18a104b113d69a07bc445 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 9 Jul 2026 12:46:30 +0000 Subject: [PATCH 1/3] clusterlin: avoid heap allocations in GetLinearization Replace GetLinearization's four per-call heap-allocated vectors (the ready chunk/transaction heaps and both dependency counters) with stack arrays and explicit size counters. This especially benefits linearization of small clusters, where the allocations make up a significant fraction of the total runtime. --- src/cluster_linearize.h | 46 +++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 23cb98f4563..eb9da60ae55 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1471,16 +1471,22 @@ public: /** A heap with all chunks (by set index) that can currently be included, sorted by * chunk feerate (high to low), chunk size (small to large), and by least maximum element * according to the fallback order (which is the second pair element). */ - std::vector> ready_chunks; + std::array, SetType::Size()> ready_chunks; + /** The number of entries of ready_chunks in use. */ + unsigned num_ready_chunks{0}; /** For every chunk, indexed by SetIdx, the number of unmet dependencies the chunk has on * other chunks (not including dependencies within the chunk itself). */ - std::vector chunk_deps(m_set_info.size(), 0); + std::array chunk_deps; + std::fill_n(chunk_deps.begin(), m_set_info.size(), TxIdx{0}); /** For every transaction, indexed by TxIdx, the number of unmet dependencies the * transaction has. */ - std::vector tx_deps(m_tx_data.size(), 0); + std::array tx_deps; + std::fill_n(tx_deps.begin(), m_tx_data.size(), TxIdx{0}); /** A heap with all transactions within the current chunk that can be included, sorted by * tx feerate (high to low), tx size (small to large), and fallback order. */ - std::vector ready_tx; + std::array ready_tx; + /** The number of entries of ready_tx in use. */ + unsigned num_ready_tx{0}; // Populate chunk_deps and tx_deps. unsigned num_deps{0}; for (TxIdx chl_idx : m_transaction_idxs) { @@ -1549,31 +1555,31 @@ public: // Construct a heap with all chunks that have no out-of-chunk dependencies. for (SetIdx chunk_idx : m_chunk_idxs) { if (chunk_deps[chunk_idx] == 0) { - ready_chunks.emplace_back(chunk_idx, max_fallback_fn(chunk_idx)); + ready_chunks[num_ready_chunks++] = {chunk_idx, max_fallback_fn(chunk_idx)}; } } - std::make_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); + std::make_heap(ready_chunks.begin(), ready_chunks.begin() + num_ready_chunks, chunk_cmp_fn); // Pop chunks off the heap. - while (!ready_chunks.empty()) { + while (num_ready_chunks > 0) { auto [chunk_idx, _rnd] = ready_chunks.front(); - std::pop_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); - ready_chunks.pop_back(); + std::pop_heap(ready_chunks.begin(), ready_chunks.begin() + num_ready_chunks, chunk_cmp_fn); + --num_ready_chunks; Assume(chunk_deps[chunk_idx] == 0); const auto& chunk_txn = m_set_info[chunk_idx].transactions; // Build heap of all includable transactions in chunk. - Assume(ready_tx.empty()); + Assume(num_ready_tx == 0); for (TxIdx tx_idx : chunk_txn) { - if (tx_deps[tx_idx] == 0) ready_tx.push_back(tx_idx); + if (tx_deps[tx_idx] == 0) ready_tx[num_ready_tx++] = tx_idx; } - Assume(!ready_tx.empty()); - std::make_heap(ready_tx.begin(), ready_tx.end(), tx_cmp_fn); + Assume(num_ready_tx > 0); + std::make_heap(ready_tx.begin(), ready_tx.begin() + num_ready_tx, tx_cmp_fn); // Pick transactions from the ready heap, append them to linearization, and decrement // dependency counts. - while (!ready_tx.empty()) { + while (num_ready_tx > 0) { // Pop an element from the tx_ready heap. auto tx_idx = ready_tx.front(); - std::pop_heap(ready_tx.begin(), ready_tx.end(), tx_cmp_fn); - ready_tx.pop_back(); + std::pop_heap(ready_tx.begin(), ready_tx.begin() + num_ready_tx, tx_cmp_fn); + --num_ready_tx; // Append to linearization. ret.push_back(tx_idx); // Decrement dependency counts. @@ -1584,16 +1590,16 @@ public: Assume(tx_deps[chl_idx] > 0); if (--tx_deps[chl_idx] == 0 && chunk_txn[chl_idx]) { // Child tx has no dependencies left, and is in this chunk. Add it to the tx heap. - ready_tx.push_back(chl_idx); - std::push_heap(ready_tx.begin(), ready_tx.end(), tx_cmp_fn); + ready_tx[num_ready_tx++] = chl_idx; + std::push_heap(ready_tx.begin(), ready_tx.begin() + num_ready_tx, tx_cmp_fn); } // Decrement chunk dependency count if this is out-of-chunk dependency. if (chl_data.chunk_idx != chunk_idx) { Assume(chunk_deps[chl_data.chunk_idx] > 0); if (--chunk_deps[chl_data.chunk_idx] == 0) { // Child chunk has no dependencies left. Add it to the chunk heap. - ready_chunks.emplace_back(chl_data.chunk_idx, max_fallback_fn(chl_data.chunk_idx)); - std::push_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); + ready_chunks[num_ready_chunks++] = {chl_data.chunk_idx, max_fallback_fn(chl_data.chunk_idx)}; + std::push_heap(ready_chunks.begin(), ready_chunks.begin() + num_ready_chunks, chunk_cmp_fn); } } } From 4b91ad149f0056c0a6dfdbc816b0c3b6f8d74827 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 9 Jul 2026 12:46:30 +0000 Subject: [PATCH 2/3] clusterlin: reserve the suboptimal-chunk queue up front Reserve capacity for one entry per transaction in m_suboptimal_chunks at construction time, avoiding repeated reallocations while MakeTopological and StartOptimizing fill the queue. --- src/cluster_linearize.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index eb9da60ae55..aa28d04634f 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1183,6 +1183,7 @@ public: m_tx_data.resize(depgraph.PositionRange()); m_set_info.resize(num_transactions); m_reachable.resize(num_transactions); + m_suboptimal_chunks.reserve(num_transactions); size_t num_chunks = 0; size_t num_deps = 0; for (auto tx_idx : m_transaction_idxs) { From efb4eae33861d3f316d77788f5c83b58dad94a0a Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 9 Jul 2026 12:46:30 +0000 Subject: [PATCH 3/3] clusterlin: avoid recomputing intersections in MergeChunks MergeChunks scans the top chunk's transactions twice: once to count the dependencies on the bottom chunk, and once to locate the randomly picked one. Remember the per-transaction dependency counts from the first pass, so the second pass only computes the intersection of the selected transaction, rather than one per transaction scanned. --- src/cluster_linearize.h | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index aa28d04634f..7b262edb46c 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -954,11 +954,16 @@ private: Assume(m_chunk_idxs[bottom_idx]); auto& top_chunk_info = m_set_info[top_idx]; auto& bottom_chunk_info = m_set_info[bottom_idx]; - // Count the number of dependencies between bottom_chunk and top_chunk. + // Count the number of dependencies between bottom_chunk and top_chunk, remembering the + // per-transaction counts so the picking loop below does not need to recompute the + // intersections. unsigned num_deps{0}; + std::array counts; for (auto tx_idx : top_chunk_info.transactions) { auto& tx_data = m_tx_data[tx_idx]; - num_deps += (tx_data.children & bottom_chunk_info.transactions).Count(); + auto count = (tx_data.children & bottom_chunk_info.transactions).Count(); + counts[tx_idx] = count; + num_deps += count; } m_cost.MergeChunksMid(/*num_txns=*/top_chunk_info.transactions.Count()); Assume(num_deps > 0); @@ -967,10 +972,10 @@ private: unsigned num_steps = 0; for (auto tx_idx : top_chunk_info.transactions) { ++num_steps; - auto& tx_data = m_tx_data[tx_idx]; - auto intersect = tx_data.children & bottom_chunk_info.transactions; - auto count = intersect.Count(); + auto count = counts[tx_idx]; if (pick < count) { + auto& tx_data = m_tx_data[tx_idx]; + auto intersect = tx_data.children & bottom_chunk_info.transactions; for (auto child_idx : intersect) { if (pick == 0) { m_cost.MergeChunksEnd(/*num_steps=*/num_steps);