From 666b37970f1566b7b369c7c5f840099fa6eda800 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 17 Feb 2026 09:00:01 -0500 Subject: [PATCH 01/20] clusterlin: fix type to count dependencies --- src/cluster_linearize.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 00627d6f167..9a2166bc77a 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -845,14 +845,14 @@ private: auto& bottom_chunk = m_tx_data[bottom_rep]; Assume(bottom_chunk.chunk_rep == bottom_rep); // Count the number of dependencies between bottom_chunk and top_chunk. - TxIdx num_deps{0}; + unsigned num_deps{0}; for (auto tx : top_chunk.chunk_setinfo.transactions) { auto& tx_data = m_tx_data[tx]; num_deps += (tx_data.children & bottom_chunk.chunk_setinfo.transactions).Count(); } if (num_deps == 0) return TxIdx(-1); // Uniformly randomly pick one of them and activate it. - TxIdx pick = m_rng.randrange(num_deps); + unsigned pick = m_rng.randrange(num_deps); for (auto tx : top_chunk.chunk_setinfo.transactions) { auto& tx_data = m_tx_data[tx]; auto intersect = tx_data.children & bottom_chunk.chunk_setinfo.transactions; From 900e45977889f9a189036f7b0e562f8e404c7190 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 16 Feb 2026 12:33:04 -0500 Subject: [PATCH 02/20] clusterlin: avoid depgraph argument in SanityCheck (cleanup) Since the deterministic ordering change, SpanningForestState holds a reference to the DepGraph it is linearizing. So this means we do not need to pass it to SanityCheck() as an argument anymore. --- src/cluster_linearize.h | 18 +++++++++--------- src/test/fuzz/cluster_linearize.cpp | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 9a2166bc77a..d2041b9b929 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1440,7 +1440,7 @@ public: uint64_t GetCost() const noexcept { return m_cost; } /** Verify internal consistency of the data structure. */ - void SanityCheck(const DepGraph& depgraph) const + void SanityCheck() const { // // Verify dependency parent/child information, and build list of (active) dependencies. @@ -1448,8 +1448,8 @@ public: std::vector> expected_dependencies; std::vector> all_dependencies; std::vector> active_dependencies; - for (auto parent_idx : depgraph.Positions()) { - for (auto child_idx : depgraph.GetReducedChildren(parent_idx)) { + for (auto parent_idx : m_depgraph.Positions()) { + for (auto child_idx : m_depgraph.GetReducedChildren(parent_idx)) { expected_dependencies.emplace_back(parent_idx, child_idx); } } @@ -1473,7 +1473,7 @@ public: // // Verify the chunks against the list of active dependencies // - for (auto tx_idx: depgraph.Positions()) { + for (auto tx_idx: m_depgraph.Positions()) { // Only process chunks for now. if (m_tx_data[tx_idx].chunk_rep == tx_idx) { const auto& chunk_data = m_tx_data[tx_idx]; @@ -1505,14 +1505,14 @@ public: assert(chunk_data.chunk_setinfo.transactions == expected_chunk); // Verify the chunk's feerate. assert(chunk_data.chunk_setinfo.feerate == - depgraph.FeeRate(chunk_data.chunk_setinfo.transactions)); + m_depgraph.FeeRate(chunk_data.chunk_setinfo.transactions)); } } // // Verify other transaction data. // - assert(m_transaction_idxs == depgraph.Positions()); + assert(m_transaction_idxs == m_depgraph.Positions()); for (auto tx_idx : m_transaction_idxs) { const auto& tx_data = m_tx_data[tx_idx]; // Verify it has a valid chunk representative, and that chunk includes this @@ -1520,8 +1520,8 @@ public: assert(m_tx_data[tx_data.chunk_rep].chunk_rep == tx_data.chunk_rep); assert(m_tx_data[tx_data.chunk_rep].chunk_setinfo.transactions[tx_idx]); // Verify parents/children. - assert(tx_data.parents == depgraph.GetReducedParents(tx_idx)); - assert(tx_data.children == depgraph.GetReducedChildren(tx_idx)); + assert(tx_data.parents == m_depgraph.GetReducedParents(tx_idx)); + assert(tx_data.children == m_depgraph.GetReducedChildren(tx_idx)); // Verify list of child dependencies. std::vector expected_child_deps; for (const auto& [par_idx, chl_idx, dep_idx] : all_dependencies) { @@ -1559,7 +1559,7 @@ public: assert(dep_data.top_setinfo.transactions == expected_top); // Verify the top_info's feerate. assert(dep_data.top_setinfo.feerate == - depgraph.FeeRate(dep_data.top_setinfo.transactions)); + m_depgraph.FeeRate(dep_data.top_setinfo.transactions)); } // diff --git a/src/test/fuzz/cluster_linearize.cpp b/src/test/fuzz/cluster_linearize.cpp index b4df22a5372..5dc7eb212bf 100644 --- a/src/test/fuzz/cluster_linearize.cpp +++ b/src/test/fuzz/cluster_linearize.cpp @@ -919,7 +919,7 @@ FUZZ_TARGET(clusterlin_sfl) if (rng.randbits(4) == 0) { // Perform sanity checks from time to time (too computationally expensive to do after // every step). - sfl.SanityCheck(depgraph); + sfl.SanityCheck(); } auto diagram = sfl.GetDiagram(); if (rng.randbits(4) == 0) { From f66fa69ce008e512af8d7cc6b22f0b3a0a080b2c Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 22 Dec 2025 14:47:16 -0500 Subject: [PATCH 03/20] clusterlin: split tx/chunk dep counting (preparation) This splits the chunk_deps variable in LoadLinearization in two, one for tracking tx dependencies and one for chunk dependencies. This is a preparation for a later commit, where chunks won't be identified anymore by a representative transaction in them, but by a separate index. With that, it seems weird to keep them both in the same structure if they will be indexed in an unrelated way. Note that the changes in src/test/util/cluster_linearize.h to the table of worst observed iteration counts are due to switching to a different data set, and are unrelated to the changes in this commit. --- src/cluster_linearize.h | 34 +++++++++++++++---------------- src/test/util/cluster_linearize.h | 16 +++++++-------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index d2041b9b929..57d6a7a6c4b 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1274,28 +1274,26 @@ public: * 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; - /** Information about chunks: - * - The first value is only used for chunk representatives, and counts the number of - * unmet dependencies this chunk has on other chunks (not including dependencies within - * the chunk itself). - * - The second value is the number of unmet dependencies overall. - */ - std::vector> chunk_deps(m_tx_data.size(), {0, 0}); + /** For every chunk, indexed by representative, the number of unmet dependencies the chunk has on + * other chunks (not including dependencies within the chunk itself). */ + std::vector chunk_deps(m_tx_data.size(), 0); + /** For every transaction, indexed by TxIdx, the number of unmet dependencies the + * transaction has. */ + std::vector tx_deps(m_tx_data.size(), 0); /** The set of all chunk representatives. */ SetType chunk_reps; /** 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; - // Populate chunk_deps[c] with the number of {out-of-chunk dependencies, dependencies} the - // child has. + // Populate chunk_deps and tx_deps. for (TxIdx chl_idx : m_transaction_idxs) { const auto& chl_data = m_tx_data[chl_idx]; - chunk_deps[chl_idx].second = chl_data.parents.Count(); + tx_deps[chl_idx] = chl_data.parents.Count(); auto chl_chunk_rep = chl_data.chunk_rep; chunk_reps.Set(chl_chunk_rep); for (auto par_idx : chl_data.parents) { auto par_chunk_rep = m_tx_data[par_idx].chunk_rep; - chunk_deps[chl_chunk_rep].first += (par_chunk_rep != chl_chunk_rep); + chunk_deps[chl_chunk_rep] += (par_chunk_rep != chl_chunk_rep); } } /** Function to compute the highest element of a chunk, by fallback_order. */ @@ -1355,7 +1353,7 @@ public: }; // Construct a heap with all chunks that have no out-of-chunk dependencies. for (TxIdx chunk_rep : chunk_reps) { - if (chunk_deps[chunk_rep].first == 0) { + if (chunk_deps[chunk_rep] == 0) { ready_chunks.emplace_back(chunk_rep, max_fallback_fn(chunk_rep)); } } @@ -1366,12 +1364,12 @@ public: std::pop_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); ready_chunks.pop_back(); Assume(m_tx_data[chunk_rep].chunk_rep == chunk_rep); - Assume(chunk_deps[chunk_rep].first == 0); + Assume(chunk_deps[chunk_rep] == 0); const auto& chunk_txn = m_tx_data[chunk_rep].chunk_setinfo.transactions; // Build heap of all includable transactions in chunk. Assume(ready_tx.empty()); for (TxIdx tx_idx : chunk_txn) { - if (chunk_deps[tx_idx].second == 0) ready_tx.push_back(tx_idx); + if (tx_deps[tx_idx] == 0) ready_tx.push_back(tx_idx); } Assume(!ready_tx.empty()); std::make_heap(ready_tx.begin(), ready_tx.end(), tx_cmp_fn); @@ -1389,16 +1387,16 @@ public: for (TxIdx chl_idx : tx_data.children) { auto& chl_data = m_tx_data[chl_idx]; // Decrement tx dependency count. - Assume(chunk_deps[chl_idx].second > 0); - if (--chunk_deps[chl_idx].second == 0 && chunk_txn[chl_idx]) { + 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); } // Decrement chunk dependency count if this is out-of-chunk dependency. if (chl_data.chunk_rep != chunk_rep) { - Assume(chunk_deps[chl_data.chunk_rep].first > 0); - if (--chunk_deps[chl_data.chunk_rep].first == 0) { + Assume(chunk_deps[chl_data.chunk_rep] > 0); + if (--chunk_deps[chl_data.chunk_rep] == 0) { // Child chunk has no dependencies left. Add it to the chunk heap. ready_chunks.emplace_back(chl_data.chunk_rep, max_fallback_fn(chl_data.chunk_rep)); std::push_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); diff --git a/src/test/util/cluster_linearize.h b/src/test/util/cluster_linearize.h index 40ce7f62723..b6b07d1bf57 100644 --- a/src/test/util/cluster_linearize.h +++ b/src/test/util/cluster_linearize.h @@ -402,14 +402,14 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count) // *some* reasonable cost bound, optimal linearizations are always found. static constexpr uint64_t ITERS[65] = { 0, - 0, 4, 10, 34, 76, 118, 184, 225, - 320, 376, 464, 573, 830, 868, 1019, 1468, - 1375, 1785, 1880, 1854, 2551, 2559, 4336, 4784, - 5547, 5807, 6157, 6075, 6961, 7403, 7756, 8001, - 8041, 7579, 8483, 10077, 9015, 9388, 9626, 12371, - 12847, 12102, 15173, 15800, 20319, 22190, 23183, 24361, - 24909, 19225, 27419, 23789, 25909, 21993, 25596, 24130, - 26349, 31823, 31855, 31250, 32688, 34825, 41710, 45478 + 0, 4, 10, 34, 76, 156, 229, 380, + 432, 607, 738, 896, 1037, 1366, 1464, 1711, + 2060, 2542, 3068, 3116, 4029, 3949, 5324, 5402, + 6481, 7161, 7441, 8329, 9307, 9353, 11104, 11269, + 11791, 11981, 12413, 14513, 15331, 12397, 13581, 19665, + 18737, 16581, 23217, 25542, 27123, 28913, 32969, 33951, + 34414, 26227, 38792, 38045, 40814, 29622, 38732, 32122, + 35915, 49823, 39722, 43765, 44002, 49716, 59417, 67035 }; assert(cluster_count < std::size(ITERS)); // Multiply the table number by two, to account for the fact that they are not absolutes. From d69c9f56ea96e333dbf2f5b4b4e63685147fbc72 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 22 Dec 2025 15:11:56 -0500 Subject: [PATCH 04/20] clusterlin: count chunk deps without loop (optimization) This small optimization avoids the need to loop over the parents of each transaction when initializing the dependency-counting structures inside GetLinearization(). --- src/cluster_linearize.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 57d6a7a6c4b..882b335fdb6 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1291,10 +1291,8 @@ public: tx_deps[chl_idx] = chl_data.parents.Count(); auto chl_chunk_rep = chl_data.chunk_rep; chunk_reps.Set(chl_chunk_rep); - for (auto par_idx : chl_data.parents) { - auto par_chunk_rep = m_tx_data[par_idx].chunk_rep; - chunk_deps[chl_chunk_rep] += (par_chunk_rep != chl_chunk_rep); - } + const auto& chl_chunk_txn = m_tx_data[chl_chunk_rep].chunk_setinfo.transactions; + chunk_deps[chl_chunk_rep] += (chl_data.parents - chl_chunk_txn).Count(); } /** Function to compute the highest element of a chunk, by fallback_order. */ auto max_fallback_fn = [&](TxIdx chunk_rep) noexcept { From 268fcb6a53ec034ec12a9c2cbaceca677664962b Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sat, 14 Feb 2026 17:07:01 -0500 Subject: [PATCH 05/20] clusterlin: add more Assumes and sanity checks (tests) --- src/cluster_linearize.h | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 882b335fdb6..a4af1641a8b 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -865,6 +865,7 @@ private: --pick; } } + Assume(false); break; } pick -= count; @@ -1031,6 +1032,7 @@ public: /** Make state topological. Can be called after constructing, or after LoadLinearization. */ void MakeTopological() noexcept { + Assume(m_suboptimal_chunks.empty()); for (auto tx : m_transaction_idxs) { auto& tx_data = m_tx_data[tx]; if (tx_data.chunk_rep == tx) { @@ -1074,6 +1076,7 @@ public: /** Initialize the data structure for optimization. It must be topological already. */ void StartOptimizing() noexcept { + Assume(m_suboptimal_chunks.empty()); // Mark chunks suboptimal. for (auto tx : m_transaction_idxs) { auto& tx_data = m_tx_data[tx]; @@ -1469,6 +1472,7 @@ public: // // Verify the chunks against the list of active dependencies // + SetType chunk_cover; for (auto tx_idx: m_depgraph.Positions()) { // Only process chunks for now. if (m_tx_data[tx_idx].chunk_rep == tx_idx) { @@ -1478,6 +1482,8 @@ public: for (auto chunk_tx : chunk_data.chunk_setinfo.transactions) { assert(m_tx_data[chunk_tx].chunk_rep == tx_idx); } + assert(!chunk_cover.Overlaps(chunk_data.chunk_setinfo.transactions)); + chunk_cover |= chunk_data.chunk_setinfo.transactions; // Verify the chunk's transaction set: it must contain the representative, and for // every active dependency, if it contains the parent or child, it must contain // both. It must have exactly N-1 active dependencies in it, guaranteeing it is @@ -1504,6 +1510,8 @@ public: m_depgraph.FeeRate(chunk_data.chunk_setinfo.transactions)); } } + // Verify that together, the chunks cover all transactions. + assert(chunk_cover == m_depgraph.Positions()); // // Verify other transaction data. @@ -1539,17 +1547,23 @@ public: const auto& dep_data = m_dep_data[dep_idx]; // Verify the top_info's transactions: it must contain the parent, and for every // active dependency, except dep_idx itself, if it contains the parent or child, it - // must contain both. + // must contain both. It must have exactly N-1 active dependencies in it, guaranteeing + // it is acyclic. SetType expected_top = SetType::Singleton(par_idx); while (true) { auto old = expected_top; + size_t active_dep_count{0}; for (const auto& [par2_idx, chl2_idx, dep2_idx] : active_dependencies) { if (dep2_idx != dep_idx && (expected_top[par2_idx] || expected_top[chl2_idx])) { expected_top.Set(par2_idx); expected_top.Set(chl2_idx); + ++active_dep_count; } } - if (old == expected_top) break; + if (old == expected_top) { + assert(expected_top.Count() == active_dep_count + 1); + break; + } } assert(!expected_top[chl_idx]); assert(dep_data.top_setinfo.transactions == expected_top); From 20e2f3e96df31ffd32f0752e28d90de30942f5ed Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 22 Dec 2025 15:37:15 -0500 Subject: [PATCH 06/20] scripted-diff: rename _rep -> _idx in SFL This is a preparation for the next commit, where chunks will no longer be identified using a representative transaction, but using a set index. Reduce the load of line changes by doing this rename ahead of time. -BEGIN VERIFY SCRIPT- sed --in-place 's/_rep/_idx/g' src/cluster_linearize.h -END VERIFY SCRIPT- --- src/cluster_linearize.h | 196 ++++++++++++++++++++-------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index a4af1641a8b..f842f97cf7c 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -661,7 +661,7 @@ private: SetType children; /** Which transaction holds the chunk_setinfo for the chunk this transaction is in * (the representative for the chunk). */ - TxIdx chunk_rep; + TxIdx chunk_idx; /** (Only if this transaction is the representative for the chunk it is in) the total * chunk set and feerate. */ SetInfo chunk_setinfo; @@ -715,18 +715,18 @@ private: } /** Update a chunk: - * - All transactions have their chunk representative set to `chunk_rep`. + * - All transactions have their chunk representative set to `chunk_idx`. * - All dependencies which have `query` in their top_setinfo get `dep_change` added to it * (if `!Subtract`) or removed from it (if `Subtract`). */ template - void UpdateChunk(const SetType& chunk, TxIdx query, TxIdx chunk_rep, const SetInfo& dep_change) noexcept + void UpdateChunk(const SetType& chunk, TxIdx query, TxIdx chunk_idx, const SetInfo& dep_change) noexcept { // Iterate over all the chunk's transactions. for (auto tx_idx : chunk) { auto& tx_data = m_tx_data[tx_idx]; // Update the chunk representative. - tx_data.chunk_rep = chunk_rep; + tx_data.chunk_idx = chunk_idx; // Iterate over all active dependencies with tx_idx as parent. Combined with the outer // loop this iterates over all internal active dependencies of the chunk. auto child_deps = std::span{tx_data.child_deps}; @@ -757,10 +757,10 @@ private: auto& parent_tx_data = m_tx_data[dep_data.parent]; // Gather information about the parent and child chunks. - Assume(parent_tx_data.chunk_rep != child_tx_data.chunk_rep); - auto& par_chunk_data = m_tx_data[parent_tx_data.chunk_rep]; - auto& chl_chunk_data = m_tx_data[child_tx_data.chunk_rep]; - TxIdx top_rep = parent_tx_data.chunk_rep; + Assume(parent_tx_data.chunk_idx != child_tx_data.chunk_idx); + auto& par_chunk_data = m_tx_data[parent_tx_data.chunk_idx]; + auto& chl_chunk_data = m_tx_data[child_tx_data.chunk_idx]; + TxIdx top_idx = parent_tx_data.chunk_idx; auto top_part = par_chunk_data.chunk_setinfo; auto bottom_part = chl_chunk_data.chunk_setinfo; // Update the parent chunk to also contain the child. @@ -784,20 +784,20 @@ private: // // Let UpdateChunk traverse the old parent chunk top_part (ABC in example), and add // bottom_part (DEF) to every dependency's top_set which has the parent (C) in it. The - // representative of each of these transactions was already top_rep, so that is not being + // representative of each of these transactions was already top_idx, so that is not being // changed here. UpdateChunk(/*chunk=*/top_part.transactions, /*query=*/dep_data.parent, - /*chunk_rep=*/top_rep, /*dep_change=*/bottom_part); + /*chunk_idx=*/top_idx, /*dep_change=*/bottom_part); // Let UpdateChunk traverse the old child chunk bottom_part (DEF in example), and add // top_part (ABC) to every dependency's top_set which has the child (E) in it. At the same - // time, change the representative of each of these transactions to be top_rep, which + // time, change the representative of each of these transactions to be top_idx, which // becomes the representative for the merged chunk. UpdateChunk(/*chunk=*/bottom_part.transactions, /*query=*/dep_data.child, - /*chunk_rep=*/top_rep, /*dep_change=*/top_part); + /*chunk_idx=*/top_idx, /*dep_change=*/top_part); // Make active. dep_data.active = true; dep_data.top_setinfo = top_part; - return top_rep; + return top_idx; } /** Make a specified active dependency inactive. */ @@ -809,15 +809,15 @@ private: // Make inactive. dep_data.active = false; // Update representatives. - auto& chunk_data = m_tx_data[parent_tx_data.chunk_rep]; + auto& chunk_data = m_tx_data[parent_tx_data.chunk_idx]; m_cost += chunk_data.chunk_setinfo.transactions.Count(); auto top_part = dep_data.top_setinfo; auto bottom_part = chunk_data.chunk_setinfo - top_part; - TxIdx bottom_rep = dep_data.child; - auto& bottom_chunk_data = m_tx_data[bottom_rep]; + TxIdx bottom_idx = dep_data.child; + auto& bottom_chunk_data = m_tx_data[bottom_idx]; bottom_chunk_data.chunk_setinfo = bottom_part; - TxIdx top_rep = dep_data.parent; - auto& top_chunk_data = m_tx_data[top_rep]; + TxIdx top_idx = dep_data.parent; + auto& top_chunk_data = m_tx_data[top_idx]; top_chunk_data.chunk_setinfo = top_part; // See the comment above in Activate(). We perform the opposite operations here, @@ -825,25 +825,25 @@ private: // // Let UpdateChunk traverse the old parent chunk top_part, and remove bottom_part from // every dependency's top_set which has the parent in it. At the same time, change the - // representative of each of these transactions to be top_rep. + // representative of each of these transactions to be top_idx. UpdateChunk(/*chunk=*/top_part.transactions, /*query=*/dep_data.parent, - /*chunk_rep=*/top_rep, /*dep_change=*/bottom_part); + /*chunk_idx=*/top_idx, /*dep_change=*/bottom_part); // Let UpdateChunk traverse the old child chunk bottom_part, and remove top_part from every // dependency's top_set which has the child in it. At the same time, change the - // representative of each of these transactions to be bottom_rep. + // representative of each of these transactions to be bottom_idx. UpdateChunk(/*chunk=*/bottom_part.transactions, /*query=*/dep_data.child, - /*chunk_rep=*/bottom_rep, /*dep_change=*/top_part); + /*chunk_idx=*/bottom_idx, /*dep_change=*/top_part); } /** Activate a dependency from the chunk represented by bottom_idx to the chunk represented by * top_idx. Return the representative of the merged chunk, or TxIdx(-1) if no merge is * possible. */ - TxIdx MergeChunks(TxIdx top_rep, TxIdx bottom_rep) noexcept + TxIdx MergeChunks(TxIdx top_idx, TxIdx bottom_idx) noexcept { - auto& top_chunk = m_tx_data[top_rep]; - Assume(top_chunk.chunk_rep == top_rep); - auto& bottom_chunk = m_tx_data[bottom_rep]; - Assume(bottom_chunk.chunk_rep == bottom_rep); + auto& top_chunk = m_tx_data[top_idx]; + Assume(top_chunk.chunk_idx == top_idx); + auto& bottom_chunk = m_tx_data[bottom_idx]; + Assume(bottom_chunk.chunk_idx == bottom_idx); // Count the number of dependencies between bottom_chunk and top_chunk. unsigned num_deps{0}; for (auto tx : top_chunk.chunk_setinfo.transactions) { @@ -877,10 +877,10 @@ private: /** Perform an upward or downward merge step, on the specified chunk representative. Returns * the representative of the merged chunk, or TxIdx(-1) if no merge took place. */ template - TxIdx MergeStep(TxIdx chunk_rep) noexcept + TxIdx MergeStep(TxIdx chunk_idx) noexcept { /** Information about the chunk that tx_idx is currently in. */ - auto& chunk_data = m_tx_data[chunk_rep]; + auto& chunk_data = m_tx_data[chunk_idx]; SetType chunk_txn = chunk_data.chunk_setinfo.transactions; // Iterate over all transactions in the chunk, figuring out which other chunk each // depends on, but only testing each other chunk once. For those depended-on chunks, @@ -896,7 +896,7 @@ private: * feerate, but is updated to be the current best candidate whenever one is found. */ FeeFrac best_other_chunk_feerate = chunk_data.chunk_setinfo.feerate; /** The representative for the best candidate chunk to merge with. -1 if none. */ - TxIdx best_other_chunk_rep = TxIdx(-1); + TxIdx best_other_chunk_idx = TxIdx(-1); /** We generate random tiebreak values to pick between equal-feerate candidate chunks. * This variable stores the tiebreak of the current best candidate. */ uint64_t best_other_chunk_tiebreak{0}; @@ -908,8 +908,8 @@ private: explored |= newly_reached; while (newly_reached.Any()) { // Find a chunk inside newly_reached, and remove it from newly_reached. - auto reached_chunk_rep = m_tx_data[newly_reached.First()].chunk_rep; - auto& reached_chunk = m_tx_data[reached_chunk_rep].chunk_setinfo; + auto reached_chunk_idx = m_tx_data[newly_reached.First()].chunk_idx; + auto& reached_chunk = m_tx_data[reached_chunk_idx].chunk_setinfo; newly_reached -= reached_chunk.transactions; // See if it has an acceptable feerate. auto cmp = DownWard ? FeeRateCompare(best_other_chunk_feerate, reached_chunk.feerate) @@ -918,20 +918,20 @@ private: uint64_t tiebreak = m_rng.rand64(); if (cmp < 0 || tiebreak >= best_other_chunk_tiebreak) { best_other_chunk_feerate = reached_chunk.feerate; - best_other_chunk_rep = reached_chunk_rep; + best_other_chunk_idx = reached_chunk_idx; best_other_chunk_tiebreak = tiebreak; } } } // Stop if there are no candidate chunks to merge with. - if (best_other_chunk_rep == TxIdx(-1)) return TxIdx(-1); + if (best_other_chunk_idx == TxIdx(-1)) return TxIdx(-1); if constexpr (DownWard) { - chunk_rep = MergeChunks(chunk_rep, best_other_chunk_rep); + chunk_idx = MergeChunks(chunk_idx, best_other_chunk_idx); } else { - chunk_rep = MergeChunks(best_other_chunk_rep, chunk_rep); + chunk_idx = MergeChunks(best_other_chunk_idx, chunk_idx); } - Assume(chunk_rep != TxIdx(-1)); - return chunk_rep; + Assume(chunk_idx != TxIdx(-1)); + return chunk_idx; } @@ -939,14 +939,14 @@ private: template void MergeSequence(TxIdx tx_idx) noexcept { - auto chunk_rep = m_tx_data[tx_idx].chunk_rep; + auto chunk_idx = m_tx_data[tx_idx].chunk_idx; while (true) { - auto merged_rep = MergeStep(chunk_rep); - if (merged_rep == TxIdx(-1)) break; - chunk_rep = merged_rep; + auto merged_idx = MergeStep(chunk_idx); + if (merged_idx == TxIdx(-1)) break; + chunk_idx = merged_idx; } // Add the chunk to the queue of improvable chunks. - m_suboptimal_chunks.push_back(chunk_rep); + m_suboptimal_chunks.push_back(chunk_idx); } /** Split a chunk, and then merge the resulting two chunks to make the graph topological @@ -991,7 +991,7 @@ public: for (auto tx : m_transaction_idxs) { // Fill in transaction data. auto& tx_data = m_tx_data[tx]; - tx_data.chunk_rep = tx; + tx_data.chunk_idx = tx; tx_data.chunk_setinfo.transactions = SetType::Singleton(tx); tx_data.chunk_setinfo.feerate = depgraph.FeeRate(tx); // Add its dependencies. @@ -1020,11 +1020,11 @@ public: { // Add transactions one by one, in order of existing linearization. for (DepGraphIndex tx : old_linearization) { - auto chunk_rep = m_tx_data[tx].chunk_rep; + auto chunk_idx = m_tx_data[tx].chunk_idx; // Merge the chunk upwards, as long as merging succeeds. while (true) { - chunk_rep = MergeStep(chunk_rep); - if (chunk_rep == TxIdx(-1)) break; + chunk_idx = MergeStep(chunk_idx); + if (chunk_idx == TxIdx(-1)) break; } } } @@ -1035,7 +1035,7 @@ public: Assume(m_suboptimal_chunks.empty()); for (auto tx : m_transaction_idxs) { auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_rep == tx) { + if (tx_data.chunk_idx == tx) { m_suboptimal_chunks.emplace_back(tx); // Randomize the initial order of suboptimal chunks in the queue. TxIdx j = m_rng.randrange(m_suboptimal_chunks.size()); @@ -1051,7 +1051,7 @@ public: auto& chunk_data = m_tx_data[chunk]; // If what was popped is not currently a chunk representative, continue. This may // happen when it was merged with something else since being added. - if (chunk_data.chunk_rep != chunk) continue; + if (chunk_data.chunk_idx != chunk) continue; int flip = m_rng.randbool(); for (int i = 0; i < 2; ++i) { if (i ^ flip) { @@ -1080,7 +1080,7 @@ public: // Mark chunks suboptimal. for (auto tx : m_transaction_idxs) { auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_rep == tx) { + if (tx_data.chunk_idx == tx) { m_suboptimal_chunks.push_back(tx); // Randomize the initial order of suboptimal chunks in the queue. TxIdx j = m_rng.randrange(m_suboptimal_chunks.size()); @@ -1102,7 +1102,7 @@ public: // If what was popped is not currently a chunk representative, continue. This may // happen when a split chunk merges in Improve() with one or more existing chunks that // are themselves on the suboptimal queue already. - if (chunk_data.chunk_rep != chunk) continue; + if (chunk_data.chunk_idx != chunk) continue; // Remember the best dependency seen so far. DepIdx candidate_dep = DepIdx(-1); uint64_t candidate_tiebreak = 0; @@ -1149,7 +1149,7 @@ public: // direction, to m_nonminimal_chunks. for (auto tx : m_transaction_idxs) { auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_rep == tx) { + if (tx_data.chunk_idx == tx) { TxIdx pivot_idx = PickRandomTx(tx_data.chunk_setinfo.transactions); m_nonminimal_chunks.emplace_back(tx, pivot_idx, m_rng.randbits<1>()); // Randomize the initial order of nonminimal chunks in the queue. @@ -1167,10 +1167,10 @@ public: // If the queue of potentially-non-minimal chunks is empty, we are done. if (m_nonminimal_chunks.empty()) return false; // Pop an entry from the potentially-non-minimal chunk queue. - auto [chunk_rep, pivot_idx, flags] = m_nonminimal_chunks.front(); + auto [chunk_idx, pivot_idx, flags] = m_nonminimal_chunks.front(); m_nonminimal_chunks.pop_front(); - auto& chunk_data = m_tx_data[chunk_rep]; - Assume(chunk_data.chunk_rep == chunk_rep); + auto& chunk_data = m_tx_data[chunk_idx]; + Assume(chunk_data.chunk_idx == chunk_idx); /** Whether to move the pivot down rather than up. */ bool move_pivot_down = flags & 1; /** Whether this is already the second stage. */ @@ -1211,23 +1211,23 @@ public: if (candidate_tiebreak == 0) { // Switch to other direction, and to second phase. flags ^= 3; - if (!second_stage) m_nonminimal_chunks.emplace_back(chunk_rep, pivot_idx, flags); + if (!second_stage) m_nonminimal_chunks.emplace_back(chunk_idx, pivot_idx, flags); return true; } // Otherwise, deactivate the dependency that was found. Deactivate(candidate_dep); auto& dep_data = m_dep_data[candidate_dep]; - auto parent_chunk_rep = m_tx_data[dep_data.parent].chunk_rep; - auto child_chunk_rep = m_tx_data[dep_data.child].chunk_rep; + auto parent_chunk_idx = m_tx_data[dep_data.parent].chunk_idx; + auto child_chunk_idx = m_tx_data[dep_data.child].chunk_idx; // Try to activate a dependency between the new bottom and the new top (opposite from the // dependency that was just deactivated). - auto merged_chunk_rep = MergeChunks(child_chunk_rep, parent_chunk_rep); - if (merged_chunk_rep != TxIdx(-1)) { + auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); + if (merged_chunk_idx != TxIdx(-1)) { // A self-merge happened. - // Re-insert the chunk into the queue, in the same direction. Note that the chunk_rep + // Re-insert the chunk into the queue, in the same direction. Note that the chunk_idx // will have changed. - m_nonminimal_chunks.emplace_back(merged_chunk_rep, pivot_idx, flags); + m_nonminimal_chunks.emplace_back(merged_chunk_idx, pivot_idx, flags); } else { // No self-merge happens, and thus we have found a way to split the chunk. Create two // smaller chunks, and add them to the queue. The one that contains the current pivot @@ -1237,13 +1237,13 @@ public: // possible already. The new chunk without the current pivot gets a new randomly-chosen // one. if (move_pivot_down) { - auto parent_pivot_idx = PickRandomTx(m_tx_data[parent_chunk_rep].chunk_setinfo.transactions); - m_nonminimal_chunks.emplace_back(parent_chunk_rep, parent_pivot_idx, m_rng.randbits<1>()); - m_nonminimal_chunks.emplace_back(child_chunk_rep, pivot_idx, flags); + auto parent_pivot_idx = PickRandomTx(m_tx_data[parent_chunk_idx].chunk_setinfo.transactions); + m_nonminimal_chunks.emplace_back(parent_chunk_idx, parent_pivot_idx, m_rng.randbits<1>()); + m_nonminimal_chunks.emplace_back(child_chunk_idx, pivot_idx, flags); } else { - auto child_pivot_idx = PickRandomTx(m_tx_data[child_chunk_rep].chunk_setinfo.transactions); - m_nonminimal_chunks.emplace_back(parent_chunk_rep, pivot_idx, flags); - m_nonminimal_chunks.emplace_back(child_chunk_rep, child_pivot_idx, m_rng.randbits<1>()); + auto child_pivot_idx = PickRandomTx(m_tx_data[child_chunk_idx].chunk_setinfo.transactions); + m_nonminimal_chunks.emplace_back(parent_chunk_idx, pivot_idx, flags); + m_nonminimal_chunks.emplace_back(child_chunk_idx, child_pivot_idx, m_rng.randbits<1>()); } if (m_rng.randbool()) { std::swap(m_nonminimal_chunks.back(), m_nonminimal_chunks[m_nonminimal_chunks.size() - 2]); @@ -1284,7 +1284,7 @@ public: * transaction has. */ std::vector tx_deps(m_tx_data.size(), 0); /** The set of all chunk representatives. */ - SetType chunk_reps; + SetType chunk_idxs; /** 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; @@ -1292,14 +1292,14 @@ public: for (TxIdx chl_idx : m_transaction_idxs) { const auto& chl_data = m_tx_data[chl_idx]; tx_deps[chl_idx] = chl_data.parents.Count(); - auto chl_chunk_rep = chl_data.chunk_rep; - chunk_reps.Set(chl_chunk_rep); - const auto& chl_chunk_txn = m_tx_data[chl_chunk_rep].chunk_setinfo.transactions; - chunk_deps[chl_chunk_rep] += (chl_data.parents - chl_chunk_txn).Count(); + auto chl_chunk_idx = chl_data.chunk_idx; + chunk_idxs.Set(chl_chunk_idx); + const auto& chl_chunk_txn = m_tx_data[chl_chunk_idx].chunk_setinfo.transactions; + chunk_deps[chl_chunk_idx] += (chl_data.parents - chl_chunk_txn).Count(); } /** Function to compute the highest element of a chunk, by fallback_order. */ - auto max_fallback_fn = [&](TxIdx chunk_rep) noexcept { - auto& chunk = m_tx_data[chunk_rep].chunk_setinfo.transactions; + auto max_fallback_fn = [&](TxIdx chunk_idx) noexcept { + auto& chunk = m_tx_data[chunk_idx].chunk_setinfo.transactions; auto it = chunk.begin(); DepGraphIndex ret = *it; ++it; @@ -1353,20 +1353,20 @@ public: return a.second < b.second; }; // Construct a heap with all chunks that have no out-of-chunk dependencies. - for (TxIdx chunk_rep : chunk_reps) { - if (chunk_deps[chunk_rep] == 0) { - ready_chunks.emplace_back(chunk_rep, max_fallback_fn(chunk_rep)); + for (TxIdx chunk_idx : chunk_idxs) { + if (chunk_deps[chunk_idx] == 0) { + ready_chunks.emplace_back(chunk_idx, max_fallback_fn(chunk_idx)); } } std::make_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); // Pop chunks off the heap. while (!ready_chunks.empty()) { - auto [chunk_rep, _rnd] = ready_chunks.front(); + auto [chunk_idx, _rnd] = ready_chunks.front(); std::pop_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); ready_chunks.pop_back(); - Assume(m_tx_data[chunk_rep].chunk_rep == chunk_rep); - Assume(chunk_deps[chunk_rep] == 0); - const auto& chunk_txn = m_tx_data[chunk_rep].chunk_setinfo.transactions; + Assume(m_tx_data[chunk_idx].chunk_idx == chunk_idx); + Assume(chunk_deps[chunk_idx] == 0); + const auto& chunk_txn = m_tx_data[chunk_idx].chunk_setinfo.transactions; // Build heap of all includable transactions in chunk. Assume(ready_tx.empty()); for (TxIdx tx_idx : chunk_txn) { @@ -1395,11 +1395,11 @@ public: std::push_heap(ready_tx.begin(), ready_tx.end(), tx_cmp_fn); } // Decrement chunk dependency count if this is out-of-chunk dependency. - if (chl_data.chunk_rep != chunk_rep) { - Assume(chunk_deps[chl_data.chunk_rep] > 0); - if (--chunk_deps[chl_data.chunk_rep] == 0) { + 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_rep, max_fallback_fn(chl_data.chunk_rep)); + 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); } } @@ -1427,7 +1427,7 @@ public: { std::vector ret; for (auto tx : m_transaction_idxs) { - if (m_tx_data[tx].chunk_rep == tx) { + if (m_tx_data[tx].chunk_idx == tx) { ret.push_back(m_tx_data[tx].chunk_setinfo.feerate); } } @@ -1475,12 +1475,12 @@ public: SetType chunk_cover; for (auto tx_idx: m_depgraph.Positions()) { // Only process chunks for now. - if (m_tx_data[tx_idx].chunk_rep == tx_idx) { + if (m_tx_data[tx_idx].chunk_idx == tx_idx) { const auto& chunk_data = m_tx_data[tx_idx]; // Verify that transactions in the chunk point back to it. This guarantees // that chunks are non-overlapping. for (auto chunk_tx : chunk_data.chunk_setinfo.transactions) { - assert(m_tx_data[chunk_tx].chunk_rep == tx_idx); + assert(m_tx_data[chunk_tx].chunk_idx == tx_idx); } assert(!chunk_cover.Overlaps(chunk_data.chunk_setinfo.transactions)); chunk_cover |= chunk_data.chunk_setinfo.transactions; @@ -1521,8 +1521,8 @@ public: const auto& tx_data = m_tx_data[tx_idx]; // Verify it has a valid chunk representative, and that chunk includes this // transaction. - assert(m_tx_data[tx_data.chunk_rep].chunk_rep == tx_data.chunk_rep); - assert(m_tx_data[tx_data.chunk_rep].chunk_setinfo.transactions[tx_idx]); + assert(m_tx_data[tx_data.chunk_idx].chunk_idx == tx_data.chunk_idx); + assert(m_tx_data[tx_data.chunk_idx].chunk_setinfo.transactions[tx_idx]); // Verify parents/children. assert(tx_data.parents == m_depgraph.GetReducedParents(tx_idx)); assert(tx_data.children == m_depgraph.GetReducedChildren(tx_idx)); @@ -1583,15 +1583,15 @@ public: // // Verify m_nonminimal_chunks. // - SetType nonminimal_reps; + SetType nonminimal_idxs; for (size_t i = 0; i < m_nonminimal_chunks.size(); ++i) { - auto [chunk_rep, pivot, flags] = m_nonminimal_chunks[i]; - assert(m_tx_data[chunk_rep].chunk_rep == chunk_rep); - assert(m_tx_data[pivot].chunk_rep == chunk_rep); - assert(!nonminimal_reps[chunk_rep]); - nonminimal_reps.Set(chunk_rep); + auto [chunk_idx, pivot, flags] = m_nonminimal_chunks[i]; + assert(m_tx_data[chunk_idx].chunk_idx == chunk_idx); + assert(m_tx_data[pivot].chunk_idx == chunk_idx); + assert(!nonminimal_idxs[chunk_idx]); + nonminimal_idxs.Set(chunk_idx); } - assert(nonminimal_reps.IsSubsetOf(m_transaction_idxs)); + assert(nonminimal_idxs.IsSubsetOf(m_transaction_idxs)); } }; From 7c6f63a8a9dc7e3d168dcb023f55936cd47ccf0b Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sat, 14 Feb 2026 16:42:30 -0500 Subject: [PATCH 07/20] clusterlin: pool SetInfos (preparation) This significantly changes the data structures used in SFL, based on the observation that the DepData::top_setinfo fields are quite wasteful: there is one per dependency (up to n^2/4), but we only ever need one per active dependency (of which there at most n-1). In total, the number of chunks plus the number of active dependencies is always exactly equal to the number of transactions, so it makes sense to have a shared pool of SetInfos, which are used for both chunks and top sets. To that effect, introduce a separate m_set_info variable, which stores a SetInfo per transaction. Some of these are used for chunk sets, and some for active dependencies' top sets. Every activation transforms the parent's chunk into the top set for the new dependency. Every deactivation transforms the top set into the new parent chunk. With indexes into m_set_data (SetIdx) becoming bounded by the number of transactions, we can use a SetType to represent sets of SetIdxs. Specifically, an m_chunk_idxs is added which contains all SetIdx referring to chunks. This leads to a much more natural way of iterating over chunks. Also use this opportunity to normalize many variable names. --- src/cluster_linearize.h | 499 +++++++++++++++--------------- src/test/util/cluster_linearize.h | 12 +- 2 files changed, 253 insertions(+), 258 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index f842f97cf7c..857276437d2 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -649,9 +649,13 @@ private: using TxIdx = DepGraphIndex; /** Data type to represent indexing into m_dep_data. */ using DepIdx = uint32_t; + /** Data type to represent indexing into m_set_info. */ + using SetIdx = uint32_t; - /** Structure with information about a single transaction. For transactions that are the - * representative for the chunk they are in, this also stores chunk information. */ + /** An invalid SetIdx. */ + static constexpr SetIdx INVALID_SET_IDX = SetIdx(-1); + + /** Structure with information about a single transaction. */ struct TxData { /** The dependencies to children of this transaction. Immutable after construction. */ std::vector child_deps; @@ -659,12 +663,8 @@ private: SetType parents; /** The set of child transactions of this transaction. Immutable after construction. */ SetType children; - /** Which transaction holds the chunk_setinfo for the chunk this transaction is in - * (the representative for the chunk). */ - TxIdx chunk_idx; - /** (Only if this transaction is the representative for the chunk it is in) the total - * chunk set and feerate. */ - SetInfo chunk_setinfo; + /** Which chunk this transaction belongs to. */ + SetIdx chunk_idx; }; /** Structure with information about a single dependency. */ @@ -675,25 +675,30 @@ private: TxIdx parent, child; /** (Only if this dependency is active) the would-be top chunk and its feerate that would * be formed if this dependency were to be deactivated. */ - SetInfo top_setinfo; + SetIdx dep_top_idx; }; /** The set of all TxIdx's of transactions in the cluster indexing into m_tx_data. */ SetType m_transaction_idxs; + /** The set of all chunk SetIdx's. This excludes the SetIdxs that refer to active + * dependencies' tops. */ + SetType m_chunk_idxs; /** Information about each transaction (and chunks). Keeps the "holes" from DepGraph during * construction. Indexed by TxIdx. */ std::vector m_tx_data; + /** Information about each set (chunk, or active dependency top set). Indexed by SetIdx. */ + std::vector> m_set_info; /** Information about each dependency. Indexed by DepIdx. */ std::vector m_dep_data; - /** A FIFO of chunk representatives of chunks that may be improved still. */ - VecDeque m_suboptimal_chunks; - /** A FIFO of chunk representatives with a pivot transaction in them, and a flag to indicate - * their status: + /** A FIFO of chunk SetIdxs for chunks that may be improved still. */ + VecDeque m_suboptimal_chunks; + /** A FIFO of chunk indexes with a pivot transaction in them, and a flag to indicate their + * status: * - bit 1: currently attempting to move the pivot down, rather than up. * - bit 2: this is the second stage, so we have already tried moving the pivot in the other * direction. */ - VecDeque> m_nonminimal_chunks; + VecDeque> m_nonminimal_chunks; /** The number of updated transactions in activations/deactivations. */ uint64_t m_cost{0}; @@ -715,17 +720,17 @@ private: } /** Update a chunk: - * - All transactions have their chunk representative set to `chunk_idx`. - * - All dependencies which have `query` in their top_setinfo get `dep_change` added to it + * - All transactions have their chunk index set to `chunk_idx`. + * - All dependencies which have `query` in their top set get `dep_change` added to it * (if `!Subtract`) or removed from it (if `Subtract`). */ template - void UpdateChunk(const SetType& chunk, TxIdx query, TxIdx chunk_idx, const SetInfo& dep_change) noexcept + void UpdateChunk(const SetType& tx_idxs, TxIdx query, SetIdx chunk_idx, const SetInfo& dep_change) noexcept { // Iterate over all the chunk's transactions. - for (auto tx_idx : chunk) { + for (auto tx_idx : tx_idxs) { auto& tx_data = m_tx_data[tx_idx]; - // Update the chunk representative. + // Update the chunk index for this transaction. tx_data.chunk_idx = chunk_idx; // Iterate over all active dependencies with tx_idx as parent. Combined with the outer // loop this iterates over all internal active dependencies of the chunk. @@ -735,37 +740,40 @@ private: Assume(dep_entry.parent == tx_idx); // Skip inactive dependencies. if (!dep_entry.active) continue; - // If this dependency's top_setinfo contains query, update it to add/remove + auto& top_set_info = m_set_info[dep_entry.dep_top_idx]; + // If this dependency's top set contains query, update it to add/remove // dep_change. - if (dep_entry.top_setinfo.transactions[query]) { + if (top_set_info.transactions[query]) { if constexpr (Subtract) { - dep_entry.top_setinfo -= dep_change; + top_set_info -= dep_change; } else { - dep_entry.top_setinfo |= dep_change; + top_set_info |= dep_change; } } } } } - /** Make a specified inactive dependency active. Returns the merged chunk representative. */ + /** Make a specified inactive dependency active. Returns the merged chunk index. */ TxIdx Activate(DepIdx dep_idx) noexcept { auto& dep_data = m_dep_data[dep_idx]; Assume(!dep_data.active); - auto& child_tx_data = m_tx_data[dep_data.child]; - auto& parent_tx_data = m_tx_data[dep_data.parent]; - // Gather information about the parent and child chunks. - Assume(parent_tx_data.chunk_idx != child_tx_data.chunk_idx); - auto& par_chunk_data = m_tx_data[parent_tx_data.chunk_idx]; - auto& chl_chunk_data = m_tx_data[child_tx_data.chunk_idx]; - TxIdx top_idx = parent_tx_data.chunk_idx; - auto top_part = par_chunk_data.chunk_setinfo; - auto bottom_part = chl_chunk_data.chunk_setinfo; - // Update the parent chunk to also contain the child. - par_chunk_data.chunk_setinfo |= bottom_part; - m_cost += par_chunk_data.chunk_setinfo.transactions.Count(); + // Gather and check information about the parent and child transactions. + auto& parent_data = m_tx_data[dep_data.parent]; + auto& child_data = m_tx_data[dep_data.child]; + Assume(parent_data.children[dep_data.child]); + // Get the set index of the chunks the parent and child are currently in. The parent chunk + // will become the top set of the newly activated dependency, while the child chunk will be + // grown to become the merged chunk. + auto parent_chunk_idx = parent_data.chunk_idx; + auto child_chunk_idx = child_data.chunk_idx; + Assume(parent_chunk_idx != child_chunk_idx); + Assume(m_chunk_idxs[parent_chunk_idx]); + Assume(m_chunk_idxs[child_chunk_idx]); + auto& top_info = m_set_info[parent_chunk_idx]; + auto& bottom_info = m_set_info[child_chunk_idx]; // Consider the following example: // @@ -782,22 +790,27 @@ private: // dependency being activated (E->C here) in its top set, will have the opposite part added // to it. This is true for B->A and F->E, but not for C->A and F->D. // - // Let UpdateChunk traverse the old parent chunk top_part (ABC in example), and add - // bottom_part (DEF) to every dependency's top_set which has the parent (C) in it. The - // representative of each of these transactions was already top_idx, so that is not being - // changed here. - UpdateChunk(/*chunk=*/top_part.transactions, /*query=*/dep_data.parent, - /*chunk_idx=*/top_idx, /*dep_change=*/bottom_part); - // Let UpdateChunk traverse the old child chunk bottom_part (DEF in example), and add - // top_part (ABC) to every dependency's top_set which has the child (E) in it. At the same - // time, change the representative of each of these transactions to be top_idx, which - // becomes the representative for the merged chunk. - UpdateChunk(/*chunk=*/bottom_part.transactions, /*query=*/dep_data.child, - /*chunk_idx=*/top_idx, /*dep_change=*/top_part); + // Let UpdateChunk traverse the old parent chunk top_info (ABC in example), and add + // bottom_info (DEF) to every dependency's top set which has the parent (C) in it. At the + // same time, change the chunk_idx for each to be child_chunk_idx, which becomes the set for + // the merged chunk. + UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/dep_data.parent, + /*chunk_idx=*/child_chunk_idx, /*dep_change=*/bottom_info); + // Let UpdateChunk traverse the old child chunk bottom_info (DEF in example), and add + // top_info (ABC) to every dependency's top set which has the child (E) in it. The chunk + // these are part of isn't being changed here (already child_chunk_idx for each). + UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/dep_data.child, + /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); + // Merge top_info into bottom_info, which becomes the merged chunk. + bottom_info |= top_info; + m_cost += bottom_info.transactions.Count(); + // Make parent chunk the set for the new active dependency. + dep_data.dep_top_idx = parent_chunk_idx; + m_chunk_idxs.Reset(parent_chunk_idx); // Make active. dep_data.active = true; - dep_data.top_setinfo = top_part; - return top_idx; + // Return the newly merged chunk. + return child_chunk_idx; } /** Make a specified active dependency inactive. */ @@ -805,62 +818,60 @@ private: { auto& dep_data = m_dep_data[dep_idx]; Assume(dep_data.active); - auto& parent_tx_data = m_tx_data[dep_data.parent]; // Make inactive. dep_data.active = false; - // Update representatives. - auto& chunk_data = m_tx_data[parent_tx_data.chunk_idx]; - m_cost += chunk_data.chunk_setinfo.transactions.Count(); - auto top_part = dep_data.top_setinfo; - auto bottom_part = chunk_data.chunk_setinfo - top_part; - TxIdx bottom_idx = dep_data.child; - auto& bottom_chunk_data = m_tx_data[bottom_idx]; - bottom_chunk_data.chunk_setinfo = bottom_part; - TxIdx top_idx = dep_data.parent; - auto& top_chunk_data = m_tx_data[top_idx]; - top_chunk_data.chunk_setinfo = top_part; - // See the comment above in Activate(). We perform the opposite operations here, - // removing instead of adding. - // - // Let UpdateChunk traverse the old parent chunk top_part, and remove bottom_part from - // every dependency's top_set which has the parent in it. At the same time, change the - // representative of each of these transactions to be top_idx. - UpdateChunk(/*chunk=*/top_part.transactions, /*query=*/dep_data.parent, - /*chunk_idx=*/top_idx, /*dep_change=*/bottom_part); - // Let UpdateChunk traverse the old child chunk bottom_part, and remove top_part from every - // dependency's top_set which has the child in it. At the same time, change the - // representative of each of these transactions to be bottom_idx. - UpdateChunk(/*chunk=*/bottom_part.transactions, /*query=*/dep_data.child, - /*chunk_idx=*/bottom_idx, /*dep_change=*/top_part); + // Gather and check information about the parent transactions. + auto& parent_data = m_tx_data[dep_data.parent]; + Assume(parent_data.children[dep_data.child]); + // Get the top set of the active dependency (which will become the parent chunk) and the + // chunk set the transactions are currently in (which will become the bottom chunk). + auto parent_chunk_idx = dep_data.dep_top_idx; + auto child_chunk_idx = parent_data.chunk_idx; + Assume(parent_chunk_idx != child_chunk_idx); + Assume(m_chunk_idxs[child_chunk_idx]); + Assume(!m_chunk_idxs[parent_chunk_idx]); // top set, not a chunk + auto& top_info = m_set_info[parent_chunk_idx]; + auto& bottom_info = m_set_info[child_chunk_idx]; + // Remove the active dependency. + dep_data.dep_top_idx = INVALID_SET_IDX; + m_chunk_idxs.Set(parent_chunk_idx); + m_cost += bottom_info.transactions.Count(); + // Subtract the top_info from the bottom_info, as it will become the child chunk. + bottom_info -= top_info; + // See the comment above in Activate(). We perform the opposite operations here, removing + // instead of adding. + UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/dep_data.parent, + /*chunk_idx=*/parent_chunk_idx, /*dep_change=*/bottom_info); + UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/dep_data.child, + /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); } - /** Activate a dependency from the chunk represented by bottom_idx to the chunk represented by - * top_idx. Return the representative of the merged chunk, or TxIdx(-1) if no merge is - * possible. */ - TxIdx MergeChunks(TxIdx top_idx, TxIdx bottom_idx) noexcept + /** Activate a dependency from the bottom set to the top set. Return the index of the merged + * chunk, or INVALID_SET_IDX if no merge is possible. */ + SetIdx MergeChunks(SetIdx top_idx, SetIdx bottom_idx) noexcept { - auto& top_chunk = m_tx_data[top_idx]; - Assume(top_chunk.chunk_idx == top_idx); - auto& bottom_chunk = m_tx_data[bottom_idx]; - Assume(bottom_chunk.chunk_idx == bottom_idx); + Assume(m_chunk_idxs[top_idx]); + 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. unsigned num_deps{0}; - for (auto tx : top_chunk.chunk_setinfo.transactions) { - auto& tx_data = m_tx_data[tx]; - num_deps += (tx_data.children & bottom_chunk.chunk_setinfo.transactions).Count(); + 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(); } - if (num_deps == 0) return TxIdx(-1); + if (num_deps == 0) return INVALID_SET_IDX; // Uniformly randomly pick one of them and activate it. unsigned pick = m_rng.randrange(num_deps); - for (auto tx : top_chunk.chunk_setinfo.transactions) { - auto& tx_data = m_tx_data[tx]; - auto intersect = tx_data.children & bottom_chunk.chunk_setinfo.transactions; + for (auto tx_idx : top_chunk_info.transactions) { + auto& tx_data = m_tx_data[tx_idx]; + auto intersect = tx_data.children & bottom_chunk_info.transactions; auto count = intersect.Count(); if (pick < count) { for (auto dep : tx_data.child_deps) { auto& dep_data = m_dep_data[dep]; - if (bottom_chunk.chunk_setinfo.transactions[dep_data.child]) { + if (bottom_chunk_info.transactions[dep_data.child]) { if (pick == 0) return Activate(dep); --pick; } @@ -871,17 +882,17 @@ private: pick -= count; } Assume(false); - return TxIdx(-1); + return INVALID_SET_IDX; } - /** Perform an upward or downward merge step, on the specified chunk representative. Returns - * the representative of the merged chunk, or TxIdx(-1) if no merge took place. */ + /** Perform an upward or downward merge step, on the specified chunk. Returns the merged chunk, + * or INVALID_SET_IDX if no merge took place. */ template - TxIdx MergeStep(TxIdx chunk_idx) noexcept + SetIdx MergeStep(SetIdx chunk_idx) noexcept { - /** Information about the chunk that tx_idx is currently in. */ - auto& chunk_data = m_tx_data[chunk_idx]; - SetType chunk_txn = chunk_data.chunk_setinfo.transactions; + /** Information about the chunk. */ + auto& chunk_info = m_set_info[chunk_idx]; + SetType chunk_txn = chunk_info.transactions; // Iterate over all transactions in the chunk, figuring out which other chunk each // depends on, but only testing each other chunk once. For those depended-on chunks, // remember the highest-feerate (if DownWard) or lowest-feerate (if !DownWard) one. @@ -894,14 +905,14 @@ private: /** The minimum feerate (if downward) or maximum feerate (if upward) to consider when * looking for candidate chunks to merge with. Initially, this is the original chunk's * feerate, but is updated to be the current best candidate whenever one is found. */ - FeeFrac best_other_chunk_feerate = chunk_data.chunk_setinfo.feerate; - /** The representative for the best candidate chunk to merge with. -1 if none. */ - TxIdx best_other_chunk_idx = TxIdx(-1); + FeeFrac best_other_chunk_feerate = chunk_info.feerate; + /** The chunk index for the best candidate chunk to merge with. INVALID_SET_IDX if none. */ + SetIdx best_other_chunk_idx = INVALID_SET_IDX; /** We generate random tiebreak values to pick between equal-feerate candidate chunks. * This variable stores the tiebreak of the current best candidate. */ uint64_t best_other_chunk_tiebreak{0}; - for (auto tx : chunk_txn) { - auto& tx_data = m_tx_data[tx]; + for (auto tx_idx : chunk_txn) { + auto& tx_data = m_tx_data[tx_idx]; /** The transactions reached by following dependencies from tx that have not been * explored before. */ auto newly_reached = (DownWard ? tx_data.children : tx_data.parents) - explored; @@ -909,28 +920,28 @@ private: while (newly_reached.Any()) { // Find a chunk inside newly_reached, and remove it from newly_reached. auto reached_chunk_idx = m_tx_data[newly_reached.First()].chunk_idx; - auto& reached_chunk = m_tx_data[reached_chunk_idx].chunk_setinfo; - newly_reached -= reached_chunk.transactions; + auto& reached_chunk_info = m_set_info[reached_chunk_idx]; + newly_reached -= reached_chunk_info.transactions; // See if it has an acceptable feerate. - auto cmp = DownWard ? FeeRateCompare(best_other_chunk_feerate, reached_chunk.feerate) - : FeeRateCompare(reached_chunk.feerate, best_other_chunk_feerate); + auto cmp = DownWard ? FeeRateCompare(best_other_chunk_feerate, reached_chunk_info.feerate) + : FeeRateCompare(reached_chunk_info.feerate, best_other_chunk_feerate); if (cmp > 0) continue; uint64_t tiebreak = m_rng.rand64(); if (cmp < 0 || tiebreak >= best_other_chunk_tiebreak) { - best_other_chunk_feerate = reached_chunk.feerate; + best_other_chunk_feerate = reached_chunk_info.feerate; best_other_chunk_idx = reached_chunk_idx; best_other_chunk_tiebreak = tiebreak; } } } // Stop if there are no candidate chunks to merge with. - if (best_other_chunk_idx == TxIdx(-1)) return TxIdx(-1); + if (best_other_chunk_idx == INVALID_SET_IDX) return INVALID_SET_IDX; if constexpr (DownWard) { chunk_idx = MergeChunks(chunk_idx, best_other_chunk_idx); } else { chunk_idx = MergeChunks(best_other_chunk_idx, chunk_idx); } - Assume(chunk_idx != TxIdx(-1)); + Assume(chunk_idx != INVALID_SET_IDX); return chunk_idx; } @@ -941,9 +952,9 @@ private: { auto chunk_idx = m_tx_data[tx_idx].chunk_idx; while (true) { - auto merged_idx = MergeStep(chunk_idx); - if (merged_idx == TxIdx(-1)) break; - chunk_idx = merged_idx; + auto merged_chunk_idx = MergeStep(chunk_idx); + if (merged_chunk_idx == INVALID_SET_IDX) break; + chunk_idx = merged_chunk_idx; } // Add the chunk to the queue of improvable chunks. m_suboptimal_chunks.push_back(chunk_idx); @@ -980,6 +991,9 @@ public: m_transaction_idxs = depgraph.Positions(); auto num_transactions = m_transaction_idxs.Count(); m_tx_data.resize(depgraph.PositionRange()); + m_set_info.resize(num_transactions); + size_t num_chunks = 0; + // Reserve the maximum number of (reserved) dependencies the cluster can have, so // m_dep_data won't need any reallocations during construction. For a cluster with N // transactions, the worst case consists of two sets of transactions, the parents and the @@ -988,29 +1002,30 @@ public: // and the other can be (N - 1)/2, meaning (N^2 - 1)/4 dependencies. Because N^2 is odd in // this case, N^2/4 (with rounding-down division) is the correct value in both cases. m_dep_data.reserve((num_transactions * num_transactions) / 4); - for (auto tx : m_transaction_idxs) { + for (auto tx_idx : m_transaction_idxs) { // Fill in transaction data. - auto& tx_data = m_tx_data[tx]; - tx_data.chunk_idx = tx; - tx_data.chunk_setinfo.transactions = SetType::Singleton(tx); - tx_data.chunk_setinfo.feerate = depgraph.FeeRate(tx); + auto& tx_data = m_tx_data[tx_idx]; + tx_data.parents = depgraph.GetReducedParents(tx_idx); + // Create a singleton chunk for it. + tx_data.chunk_idx = num_chunks; + m_set_info[num_chunks++] = SetInfo(depgraph, tx_idx); // Add its dependencies. - SetType parents = depgraph.GetReducedParents(tx); - for (auto par : parents) { - auto& par_tx_data = m_tx_data[par]; + for (auto parent_idx : tx_data.parents) { + auto& par_tx_data = m_tx_data[parent_idx]; auto dep_idx = m_dep_data.size(); // Construct new dependency. auto& dep = m_dep_data.emplace_back(); dep.active = false; - dep.parent = par; - dep.child = tx; - // Add it as parent of the child. - tx_data.parents.Set(par); + dep.parent = parent_idx; + dep.child = tx_idx; // Add it as child of the parent. par_tx_data.child_deps.push_back(dep_idx); - par_tx_data.children.Set(tx); + par_tx_data.children.Set(tx_idx); } } + Assume(num_chunks == num_transactions); + // Mark all chunk sets as chunks. + m_chunk_idxs = SetType::Fill(num_chunks); } /** Load an existing linearization. Must be called immediately after constructor. The result is @@ -1019,12 +1034,12 @@ public: void LoadLinearization(std::span old_linearization) noexcept { // Add transactions one by one, in order of existing linearization. - for (DepGraphIndex tx : old_linearization) { - auto chunk_idx = m_tx_data[tx].chunk_idx; + for (DepGraphIndex tx_idx : old_linearization) { + auto chunk_idx = m_tx_data[tx_idx].chunk_idx; // Merge the chunk upwards, as long as merging succeeds. while (true) { chunk_idx = MergeStep(chunk_idx); - if (chunk_idx == TxIdx(-1)) break; + if (chunk_idx == INVALID_SET_IDX) break; } } } @@ -1033,38 +1048,34 @@ public: void MakeTopological() noexcept { Assume(m_suboptimal_chunks.empty()); - for (auto tx : m_transaction_idxs) { - auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_idx == tx) { - m_suboptimal_chunks.emplace_back(tx); - // Randomize the initial order of suboptimal chunks in the queue. - TxIdx j = m_rng.randrange(m_suboptimal_chunks.size()); - if (j != m_suboptimal_chunks.size() - 1) { - std::swap(m_suboptimal_chunks.back(), m_suboptimal_chunks[j]); - } + for (auto chunk_idx : m_chunk_idxs) { + m_suboptimal_chunks.emplace_back(chunk_idx); + // Randomize the initial order of suboptimal chunks in the queue. + SetIdx j = m_rng.randrange(m_suboptimal_chunks.size()); + if (j != m_suboptimal_chunks.size() - 1) { + std::swap(m_suboptimal_chunks.back(), m_suboptimal_chunks[j]); } } while (!m_suboptimal_chunks.empty()) { // Pop an entry from the potentially-suboptimal chunk queue. - TxIdx chunk = m_suboptimal_chunks.front(); + SetIdx chunk_idx = m_suboptimal_chunks.front(); m_suboptimal_chunks.pop_front(); - auto& chunk_data = m_tx_data[chunk]; - // If what was popped is not currently a chunk representative, continue. This may + // If what was popped is not currently a chunk, continue. This may // happen when it was merged with something else since being added. - if (chunk_data.chunk_idx != chunk) continue; + if (!m_chunk_idxs[chunk_idx]) continue; int flip = m_rng.randbool(); for (int i = 0; i < 2; ++i) { if (i ^ flip) { // Attempt to merge the chunk upwards. - auto result_up = MergeStep(chunk); - if (result_up != TxIdx(-1)) { + auto result_up = MergeStep(chunk_idx); + if (result_up != INVALID_SET_IDX) { m_suboptimal_chunks.push_back(result_up); break; } } else { // Attempt to merge the chunk downwards. - auto result_down = MergeStep(chunk); - if (result_down != TxIdx(-1)) { + auto result_down = MergeStep(chunk_idx); + if (result_down != INVALID_SET_IDX) { m_suboptimal_chunks.push_back(result_down); break; } @@ -1078,15 +1089,12 @@ public: { Assume(m_suboptimal_chunks.empty()); // Mark chunks suboptimal. - for (auto tx : m_transaction_idxs) { - auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_idx == tx) { - m_suboptimal_chunks.push_back(tx); - // Randomize the initial order of suboptimal chunks in the queue. - TxIdx j = m_rng.randrange(m_suboptimal_chunks.size()); - if (j != m_suboptimal_chunks.size() - 1) { - std::swap(m_suboptimal_chunks.back(), m_suboptimal_chunks[j]); - } + for (auto chunk_idx : m_chunk_idxs) { + m_suboptimal_chunks.push_back(chunk_idx); + // Randomize the initial order of suboptimal chunks in the queue. + SetIdx j = m_rng.randrange(m_suboptimal_chunks.size()); + if (j != m_suboptimal_chunks.size() - 1) { + std::swap(m_suboptimal_chunks.back(), m_suboptimal_chunks[j]); } } } @@ -1096,27 +1104,28 @@ public: { while (!m_suboptimal_chunks.empty()) { // Pop an entry from the potentially-suboptimal chunk queue. - TxIdx chunk = m_suboptimal_chunks.front(); + SetIdx chunk_idx = m_suboptimal_chunks.front(); m_suboptimal_chunks.pop_front(); - auto& chunk_data = m_tx_data[chunk]; - // If what was popped is not currently a chunk representative, continue. This may + auto& chunk_info = m_set_info[chunk_idx]; + // If what was popped is not currently a chunk, continue. This may // happen when a split chunk merges in Improve() with one or more existing chunks that // are themselves on the suboptimal queue already. - if (chunk_data.chunk_idx != chunk) continue; + if (!m_chunk_idxs[chunk_idx]) continue; // Remember the best dependency seen so far. DepIdx candidate_dep = DepIdx(-1); uint64_t candidate_tiebreak = 0; // Iterate over all transactions. - for (auto tx : chunk_data.chunk_setinfo.transactions) { - const auto& tx_data = m_tx_data[tx]; + for (auto tx_idx : chunk_info.transactions) { + const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. const auto children = std::span{tx_data.child_deps}; for (DepIdx dep_idx : children) { const auto& dep_data = m_dep_data[dep_idx]; if (!dep_data.active) continue; + auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; // Skip if this dependency is ineligible (the top chunk that would be created // does not have higher feerate than the chunk it is currently part of). - auto cmp = FeeRateCompare(dep_data.top_setinfo.feerate, chunk_data.chunk_setinfo.feerate); + auto cmp = FeeRateCompare(dep_top_info.feerate, chunk_info.feerate); if (cmp <= 0) continue; // Generate a random tiebreak for this dependency, and reject it if its tiebreak // is worse than the best so far. This means that among all eligible @@ -1147,16 +1156,13 @@ public: m_nonminimal_chunks.reserve(m_transaction_idxs.Count()); // Gather all chunks, and for each, add it with a random pivot in it, and a random initial // direction, to m_nonminimal_chunks. - for (auto tx : m_transaction_idxs) { - auto& tx_data = m_tx_data[tx]; - if (tx_data.chunk_idx == tx) { - TxIdx pivot_idx = PickRandomTx(tx_data.chunk_setinfo.transactions); - m_nonminimal_chunks.emplace_back(tx, pivot_idx, m_rng.randbits<1>()); - // Randomize the initial order of nonminimal chunks in the queue. - TxIdx j = m_rng.randrange(m_nonminimal_chunks.size()); - if (j != m_nonminimal_chunks.size() - 1) { - std::swap(m_nonminimal_chunks.back(), m_nonminimal_chunks[j]); - } + for (auto chunk_idx : m_chunk_idxs) { + TxIdx pivot_idx = PickRandomTx(m_set_info[chunk_idx].transactions); + m_nonminimal_chunks.emplace_back(chunk_idx, pivot_idx, m_rng.randbits<1>()); + // Randomize the initial order of nonminimal chunks in the queue. + SetIdx j = m_rng.randrange(m_nonminimal_chunks.size()); + if (j != m_nonminimal_chunks.size() - 1) { + std::swap(m_nonminimal_chunks.back(), m_nonminimal_chunks[j]); } } } @@ -1169,8 +1175,7 @@ public: // Pop an entry from the potentially-non-minimal chunk queue. auto [chunk_idx, pivot_idx, flags] = m_nonminimal_chunks.front(); m_nonminimal_chunks.pop_front(); - auto& chunk_data = m_tx_data[chunk_idx]; - Assume(chunk_data.chunk_idx == chunk_idx); + auto& chunk_info = m_set_info[chunk_idx]; /** Whether to move the pivot down rather than up. */ bool move_pivot_down = flags & 1; /** Whether this is already the second stage. */ @@ -1182,20 +1187,21 @@ public: uint64_t candidate_tiebreak{0}; bool have_any = false; // Iterate over all transactions. - for (auto tx_idx : chunk_data.chunk_setinfo.transactions) { + for (auto tx_idx : chunk_info.transactions) { const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. for (auto dep_idx : tx_data.child_deps) { auto& dep_data = m_dep_data[dep_idx]; // Skip inactive child dependencies. if (!dep_data.active) continue; + const auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; // Skip if this dependency does not have equal top and bottom set feerates. Note // that the top cannot have higher feerate than the bottom, or OptimizeSteps would // have dealt with it. - if (dep_data.top_setinfo.feerate << chunk_data.chunk_setinfo.feerate) continue; + if (dep_top_info.feerate << chunk_info.feerate) continue; have_any = true; // Skip if this dependency does not have pivot in the right place. - if (move_pivot_down == dep_data.top_setinfo.transactions[pivot_idx]) continue; + if (move_pivot_down == dep_top_info.transactions[pivot_idx]) continue; // Remember this as our chosen dependency if it has a better tiebreak. uint64_t tiebreak = m_rng.rand64() | 1; if (tiebreak > candidate_tiebreak) { @@ -1223,7 +1229,7 @@ public: // Try to activate a dependency between the new bottom and the new top (opposite from the // dependency that was just deactivated). auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); - if (merged_chunk_idx != TxIdx(-1)) { + if (merged_chunk_idx != INVALID_SET_IDX) { // A self-merge happened. // Re-insert the chunk into the queue, in the same direction. Note that the chunk_idx // will have changed. @@ -1237,11 +1243,11 @@ public: // possible already. The new chunk without the current pivot gets a new randomly-chosen // one. if (move_pivot_down) { - auto parent_pivot_idx = PickRandomTx(m_tx_data[parent_chunk_idx].chunk_setinfo.transactions); + auto parent_pivot_idx = PickRandomTx(m_set_info[parent_chunk_idx].transactions); m_nonminimal_chunks.emplace_back(parent_chunk_idx, parent_pivot_idx, m_rng.randbits<1>()); m_nonminimal_chunks.emplace_back(child_chunk_idx, pivot_idx, flags); } else { - auto child_pivot_idx = PickRandomTx(m_tx_data[child_chunk_idx].chunk_setinfo.transactions); + auto child_pivot_idx = PickRandomTx(m_set_info[child_chunk_idx].transactions); m_nonminimal_chunks.emplace_back(parent_chunk_idx, pivot_idx, flags); m_nonminimal_chunks.emplace_back(child_chunk_idx, child_pivot_idx, m_rng.randbits<1>()); } @@ -1272,19 +1278,17 @@ public: { /** The output linearization. */ std::vector ret; - ret.reserve(m_transaction_idxs.Count()); - /** A heap with all chunks (by representative) that can currently be included, sorted by + ret.reserve(m_set_info.size()); + /** 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; - /** For every chunk, indexed by representative, the number of unmet dependencies the chunk has on + std::vector> ready_chunks; + /** 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_tx_data.size(), 0); + std::vector chunk_deps(m_set_info.size(), 0); /** For every transaction, indexed by TxIdx, the number of unmet dependencies the * transaction has. */ std::vector tx_deps(m_tx_data.size(), 0); - /** The set of all chunk representatives. */ - SetType chunk_idxs; /** 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; @@ -1293,13 +1297,12 @@ public: const auto& chl_data = m_tx_data[chl_idx]; tx_deps[chl_idx] = chl_data.parents.Count(); auto chl_chunk_idx = chl_data.chunk_idx; - chunk_idxs.Set(chl_chunk_idx); - const auto& chl_chunk_txn = m_tx_data[chl_chunk_idx].chunk_setinfo.transactions; - chunk_deps[chl_chunk_idx] += (chl_data.parents - chl_chunk_txn).Count(); + auto& chl_chunk_info = m_set_info[chl_chunk_idx]; + chunk_deps[chl_chunk_idx] += (chl_data.parents - chl_chunk_info.transactions).Count(); } /** Function to compute the highest element of a chunk, by fallback_order. */ - auto max_fallback_fn = [&](TxIdx chunk_idx) noexcept { - auto& chunk = m_tx_data[chunk_idx].chunk_setinfo.transactions; + auto max_fallback_fn = [&](SetIdx chunk_idx) noexcept { + auto& chunk = m_set_info[chunk_idx].transactions; auto it = chunk.begin(); DepGraphIndex ret = *it; ++it; @@ -1337,8 +1340,8 @@ public: // Bail out for identical chunks. if (a.first == b.first) return false; // First sort by increasing chunk feerate. - auto& chunk_feerate_a = m_tx_data[a.first].chunk_setinfo.feerate; - auto& chunk_feerate_b = m_tx_data[b.first].chunk_setinfo.feerate; + auto& chunk_feerate_a = m_set_info[a.first].feerate; + auto& chunk_feerate_b = m_set_info[b.first].feerate; auto feerate_cmp = FeeRateCompare(chunk_feerate_a, chunk_feerate_b); if (feerate_cmp != 0) return feerate_cmp < 0; // Then by decreasing chunk size. @@ -1353,7 +1356,7 @@ public: return a.second < b.second; }; // Construct a heap with all chunks that have no out-of-chunk dependencies. - for (TxIdx chunk_idx : chunk_idxs) { + for (SetIdx chunk_idx : m_chunk_idxs) { if (chunk_deps[chunk_idx] == 0) { ready_chunks.emplace_back(chunk_idx, max_fallback_fn(chunk_idx)); } @@ -1364,9 +1367,8 @@ public: auto [chunk_idx, _rnd] = ready_chunks.front(); std::pop_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); ready_chunks.pop_back(); - Assume(m_tx_data[chunk_idx].chunk_idx == chunk_idx); Assume(chunk_deps[chunk_idx] == 0); - const auto& chunk_txn = m_tx_data[chunk_idx].chunk_setinfo.transactions; + const auto& chunk_txn = m_set_info[chunk_idx].transactions; // Build heap of all includable transactions in chunk. Assume(ready_tx.empty()); for (TxIdx tx_idx : chunk_txn) { @@ -1406,7 +1408,7 @@ public: } } } - Assume(ret.size() == m_transaction_idxs.Count()); + Assume(ret.size() == m_set_info.size()); return ret; } @@ -1426,10 +1428,8 @@ public: std::vector GetDiagram() const noexcept { std::vector ret; - for (auto tx : m_transaction_idxs) { - if (m_tx_data[tx].chunk_idx == tx) { - ret.push_back(m_tx_data[tx].chunk_setinfo.feerate); - } + for (auto chunk_idx : m_chunk_idxs) { + ret.push_back(m_set_info[chunk_idx].feerate); } std::sort(ret.begin(), ret.end(), std::greater{}); return ret; @@ -1473,56 +1473,52 @@ public: // Verify the chunks against the list of active dependencies // SetType chunk_cover; - for (auto tx_idx: m_depgraph.Positions()) { - // Only process chunks for now. - if (m_tx_data[tx_idx].chunk_idx == tx_idx) { - const auto& chunk_data = m_tx_data[tx_idx]; - // Verify that transactions in the chunk point back to it. This guarantees - // that chunks are non-overlapping. - for (auto chunk_tx : chunk_data.chunk_setinfo.transactions) { - assert(m_tx_data[chunk_tx].chunk_idx == tx_idx); - } - assert(!chunk_cover.Overlaps(chunk_data.chunk_setinfo.transactions)); - chunk_cover |= chunk_data.chunk_setinfo.transactions; - // Verify the chunk's transaction set: it must contain the representative, and for - // every active dependency, if it contains the parent or child, it must contain - // both. It must have exactly N-1 active dependencies in it, guaranteeing it is - // acyclic. - SetType expected_chunk = SetType::Singleton(tx_idx); - while (true) { - auto old = expected_chunk; - size_t active_dep_count{0}; - for (const auto& [par, chl, _dep] : active_dependencies) { - if (expected_chunk[par] || expected_chunk[chl]) { - expected_chunk.Set(par); - expected_chunk.Set(chl); - ++active_dep_count; - } - } - if (old == expected_chunk) { - assert(expected_chunk.Count() == active_dep_count + 1); - break; - } - } - assert(chunk_data.chunk_setinfo.transactions == expected_chunk); - // Verify the chunk's feerate. - assert(chunk_data.chunk_setinfo.feerate == - m_depgraph.FeeRate(chunk_data.chunk_setinfo.transactions)); + for (auto chunk_idx : m_chunk_idxs) { + const auto& chunk_info = m_set_info[chunk_idx]; + // Verify that transactions in the chunk point back to it. This guarantees + // that chunks are non-overlapping. + for (auto chunk_tx : chunk_info.transactions) { + assert(m_tx_data[chunk_tx].chunk_idx == chunk_idx); } + assert(!chunk_cover.Overlaps(chunk_info.transactions)); + chunk_cover |= chunk_info.transactions; + // Verify the chunk's transaction set: start from an arbitrary chunk transaction, + // and for every active dependency, if it contains the parent or child, add the + // other. It must have exactly N-1 active dependencies in it, guaranteeing it is + // acyclic. + assert(chunk_info.transactions.Any()); + SetType expected_chunk = SetType::Singleton(chunk_info.transactions.First()); + while (true) { + auto old = expected_chunk; + size_t active_dep_count{0}; + for (const auto& [par, chl, _dep] : active_dependencies) { + if (expected_chunk[par] || expected_chunk[chl]) { + expected_chunk.Set(par); + expected_chunk.Set(chl); + ++active_dep_count; + } + } + if (old == expected_chunk) { + assert(expected_chunk.Count() == active_dep_count + 1); + break; + } + } + assert(chunk_info.transactions == expected_chunk); + // Verify the chunk's feerate. + assert(chunk_info.feerate == m_depgraph.FeeRate(chunk_info.transactions)); } // Verify that together, the chunks cover all transactions. assert(chunk_cover == m_depgraph.Positions()); // - // Verify other transaction data. + // Verify transaction data. // assert(m_transaction_idxs == m_depgraph.Positions()); for (auto tx_idx : m_transaction_idxs) { const auto& tx_data = m_tx_data[tx_idx]; - // Verify it has a valid chunk representative, and that chunk includes this - // transaction. - assert(m_tx_data[tx_data.chunk_idx].chunk_idx == tx_data.chunk_idx); - assert(m_tx_data[tx_data.chunk_idx].chunk_setinfo.transactions[tx_idx]); + // Verify it has a valid chunk index, and that chunk includes this transaction. + assert(m_chunk_idxs[tx_data.chunk_idx]); + assert(m_set_info[tx_data.chunk_idx].transactions[tx_idx]); // Verify parents/children. assert(tx_data.parents == m_depgraph.GetReducedParents(tx_idx)); assert(tx_data.children == m_depgraph.GetReducedChildren(tx_idx)); @@ -1541,11 +1537,11 @@ public: } // - // Verify active dependencies' top_setinfo. + // Verify active dependencies' top sets. // for (const auto& [par_idx, chl_idx, dep_idx] : active_dependencies) { const auto& dep_data = m_dep_data[dep_idx]; - // Verify the top_info's transactions: it must contain the parent, and for every + // Verify the top set's transactions: it must contain the parent, and for every // active dependency, except dep_idx itself, if it contains the parent or child, it // must contain both. It must have exactly N-1 active dependencies in it, guaranteeing // it is acyclic. @@ -1566,18 +1562,18 @@ public: } } assert(!expected_top[chl_idx]); - assert(dep_data.top_setinfo.transactions == expected_top); - // Verify the top_info's feerate. - assert(dep_data.top_setinfo.feerate == - m_depgraph.FeeRate(dep_data.top_setinfo.transactions)); + auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; + assert(dep_top_info.transactions == expected_top); + // Verify the top set's feerate. + assert(dep_top_info.feerate == m_depgraph.FeeRate(dep_top_info.transactions)); } // // Verify m_suboptimal_chunks. // for (size_t i = 0; i < m_suboptimal_chunks.size(); ++i) { - auto tx_idx = m_suboptimal_chunks[i]; - assert(m_transaction_idxs[tx_idx]); + auto chunk_idx = m_suboptimal_chunks[i]; + assert(chunk_idx < m_set_info.size()); } // @@ -1586,12 +1582,11 @@ public: SetType nonminimal_idxs; for (size_t i = 0; i < m_nonminimal_chunks.size(); ++i) { auto [chunk_idx, pivot, flags] = m_nonminimal_chunks[i]; - assert(m_tx_data[chunk_idx].chunk_idx == chunk_idx); assert(m_tx_data[pivot].chunk_idx == chunk_idx); assert(!nonminimal_idxs[chunk_idx]); nonminimal_idxs.Set(chunk_idx); } - assert(nonminimal_idxs.IsSubsetOf(m_transaction_idxs)); + assert(nonminimal_idxs.IsSubsetOf(m_chunk_idxs)); } }; diff --git a/src/test/util/cluster_linearize.h b/src/test/util/cluster_linearize.h index b6b07d1bf57..7e98312cbbb 100644 --- a/src/test/util/cluster_linearize.h +++ b/src/test/util/cluster_linearize.h @@ -404,12 +404,12 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count) 0, 0, 4, 10, 34, 76, 156, 229, 380, 432, 607, 738, 896, 1037, 1366, 1464, 1711, - 2060, 2542, 3068, 3116, 4029, 3949, 5324, 5402, - 6481, 7161, 7441, 8329, 9307, 9353, 11104, 11269, - 11791, 11981, 12413, 14513, 15331, 12397, 13581, 19665, - 18737, 16581, 23217, 25542, 27123, 28913, 32969, 33951, - 34414, 26227, 38792, 38045, 40814, 29622, 38732, 32122, - 35915, 49823, 39722, 43765, 44002, 49716, 59417, 67035 + 2060, 2542, 3068, 3116, 4029, 3467, 5324, 5402, + 6481, 7161, 7441, 8329, 8843, 9353, 11104, 11269, + 11791, 11981, 12413, 14259, 15331, 12397, 13581, 18569, + 18737, 16581, 23217, 23271, 27350, 28591, 33636, 34486, + 34414, 26227, 35570, 38045, 40814, 29622, 37793, 32122, + 35915, 49823, 39722, 43765, 42365, 53620, 59417, 67035 }; assert(cluster_count < std::size(ITERS)); // Multiply the table number by two, to account for the fact that they are not absolutes. From 73cbd15d457221e2e896549a32df7f5a59a9df5c Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sat, 14 Feb 2026 16:42:32 -0500 Subject: [PATCH 08/20] clusterlin: get rid of DepData (optimization) With the earlier change to pool SetInfo objects, there is little need for DepData anymore. Use parent/child TxIdxs to refer to dependencies, and find their top set by having a child TxIdx-indexed vector in each TxData, rather than a list of dependencies. This makes code for iterating over dependencies more natural and simpler. --- src/cluster_linearize.h | 215 ++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 139 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 857276437d2..50606942e9f 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -647,8 +647,6 @@ private: /** Data type to represent indexing into m_tx_data. */ using TxIdx = DepGraphIndex; - /** Data type to represent indexing into m_dep_data. */ - using DepIdx = uint32_t; /** Data type to represent indexing into m_set_info. */ using SetIdx = uint32_t; @@ -657,8 +655,10 @@ private: /** Structure with information about a single transaction. */ struct TxData { - /** The dependencies to children of this transaction. Immutable after construction. */ - std::vector child_deps; + /** The top set for every active child dependency this transaction has, indexed by child + * TxIdx. INVALID_SET_IDX if there is no active dependency with the corresponding child. + */ + std::vector dep_top_idx; /** The set of parent transactions of this transaction. Immutable after construction. */ SetType parents; /** The set of child transactions of this transaction. Immutable after construction. */ @@ -667,17 +667,6 @@ private: SetIdx chunk_idx; }; - /** Structure with information about a single dependency. */ - struct DepData { - /** Whether this dependency is active. */ - bool active; - /** What the parent and child transactions are. Immutable after construction. */ - TxIdx parent, child; - /** (Only if this dependency is active) the would-be top chunk and its feerate that would - * be formed if this dependency were to be deactivated. */ - SetIdx dep_top_idx; - }; - /** The set of all TxIdx's of transactions in the cluster indexing into m_tx_data. */ SetType m_transaction_idxs; /** The set of all chunk SetIdx's. This excludes the SetIdxs that refer to active @@ -688,8 +677,6 @@ private: std::vector m_tx_data; /** Information about each set (chunk, or active dependency top set). Indexed by SetIdx. */ std::vector> m_set_info; - /** Information about each dependency. Indexed by DepIdx. */ - std::vector m_dep_data; /** A FIFO of chunk SetIdxs for chunks that may be improved still. */ VecDeque m_suboptimal_chunks; /** A FIFO of chunk indexes with a pivot transaction in them, and a flag to indicate their @@ -734,13 +721,10 @@ private: tx_data.chunk_idx = chunk_idx; // Iterate over all active dependencies with tx_idx as parent. Combined with the outer // loop this iterates over all internal active dependencies of the chunk. - auto child_deps = std::span{tx_data.child_deps}; - for (auto dep_idx : child_deps) { - auto& dep_entry = m_dep_data[dep_idx]; - Assume(dep_entry.parent == tx_idx); + for (auto child_idx : tx_data.children) { // Skip inactive dependencies. - if (!dep_entry.active) continue; - auto& top_set_info = m_set_info[dep_entry.dep_top_idx]; + if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + auto& top_set_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // If this dependency's top set contains query, update it to add/remove // dep_change. if (top_set_info.transactions[query]) { @@ -754,16 +738,15 @@ private: } } - /** Make a specified inactive dependency active. Returns the merged chunk index. */ - TxIdx Activate(DepIdx dep_idx) noexcept + /** Make the inactive dependency from child to parent, which must not be in the same chunk + * already, active. Returns the merged chunk idx. */ + SetIdx Activate(TxIdx parent_idx, TxIdx child_idx) noexcept { - auto& dep_data = m_dep_data[dep_idx]; - Assume(!dep_data.active); - // Gather and check information about the parent and child transactions. - auto& parent_data = m_tx_data[dep_data.parent]; - auto& child_data = m_tx_data[dep_data.child]; - Assume(parent_data.children[dep_data.child]); + auto& parent_data = m_tx_data[parent_idx]; + auto& child_data = m_tx_data[child_idx]; + Assume(parent_data.children[child_idx]); + Assume(parent_data.dep_top_idx[child_idx] == INVALID_SET_IDX); // Get the set index of the chunks the parent and child are currently in. The parent chunk // will become the top set of the newly activated dependency, while the child chunk will be // grown to become the merged chunk. @@ -794,56 +777,51 @@ private: // bottom_info (DEF) to every dependency's top set which has the parent (C) in it. At the // same time, change the chunk_idx for each to be child_chunk_idx, which becomes the set for // the merged chunk. - UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/dep_data.parent, + UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/parent_idx, /*chunk_idx=*/child_chunk_idx, /*dep_change=*/bottom_info); // Let UpdateChunk traverse the old child chunk bottom_info (DEF in example), and add // top_info (ABC) to every dependency's top set which has the child (E) in it. The chunk // these are part of isn't being changed here (already child_chunk_idx for each). - UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/dep_data.child, + UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/child_idx, /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); // Merge top_info into bottom_info, which becomes the merged chunk. bottom_info |= top_info; m_cost += bottom_info.transactions.Count(); // Make parent chunk the set for the new active dependency. - dep_data.dep_top_idx = parent_chunk_idx; + parent_data.dep_top_idx[child_idx] = parent_chunk_idx; m_chunk_idxs.Reset(parent_chunk_idx); - // Make active. - dep_data.active = true; // Return the newly merged chunk. return child_chunk_idx; } /** Make a specified active dependency inactive. */ - void Deactivate(DepIdx dep_idx) noexcept + void Deactivate(TxIdx parent_idx, TxIdx child_idx) noexcept { - auto& dep_data = m_dep_data[dep_idx]; - Assume(dep_data.active); - // Make inactive. - dep_data.active = false; - // Gather and check information about the parent transactions. - auto& parent_data = m_tx_data[dep_data.parent]; - Assume(parent_data.children[dep_data.child]); + auto& parent_data = m_tx_data[parent_idx]; + Assume(parent_data.children[child_idx]); + Assume(parent_data.dep_top_idx[child_idx] != INVALID_SET_IDX); // Get the top set of the active dependency (which will become the parent chunk) and the // chunk set the transactions are currently in (which will become the bottom chunk). - auto parent_chunk_idx = dep_data.dep_top_idx; + auto parent_chunk_idx = parent_data.dep_top_idx[child_idx]; auto child_chunk_idx = parent_data.chunk_idx; Assume(parent_chunk_idx != child_chunk_idx); Assume(m_chunk_idxs[child_chunk_idx]); Assume(!m_chunk_idxs[parent_chunk_idx]); // top set, not a chunk auto& top_info = m_set_info[parent_chunk_idx]; auto& bottom_info = m_set_info[child_chunk_idx]; + // Remove the active dependency. - dep_data.dep_top_idx = INVALID_SET_IDX; + parent_data.dep_top_idx[child_idx] = INVALID_SET_IDX; m_chunk_idxs.Set(parent_chunk_idx); m_cost += bottom_info.transactions.Count(); // Subtract the top_info from the bottom_info, as it will become the child chunk. bottom_info -= top_info; // See the comment above in Activate(). We perform the opposite operations here, removing // instead of adding. - UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/dep_data.parent, + UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/parent_idx, /*chunk_idx=*/parent_chunk_idx, /*dep_change=*/bottom_info); - UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/dep_data.child, + UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/child_idx, /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); } @@ -869,12 +847,9 @@ private: auto intersect = tx_data.children & bottom_chunk_info.transactions; auto count = intersect.Count(); if (pick < count) { - for (auto dep : tx_data.child_deps) { - auto& dep_data = m_dep_data[dep]; - if (bottom_chunk_info.transactions[dep_data.child]) { - if (pick == 0) return Activate(dep); - --pick; - } + for (auto child_idx : intersect) { + if (pick == 0) return Activate(tx_idx, child_idx); + --pick; } Assume(false); break; @@ -962,24 +937,22 @@ private: /** Split a chunk, and then merge the resulting two chunks to make the graph topological * again. */ - void Improve(DepIdx dep_idx) noexcept + void Improve(TxIdx parent_idx, TxIdx child_idx) noexcept { - auto& dep_data = m_dep_data[dep_idx]; - Assume(dep_data.active); // Deactivate the specified dependency, splitting it into two new chunks: a top containing // the parent, and a bottom containing the child. The top should have a higher feerate. - Deactivate(dep_idx); + Deactivate(parent_idx, child_idx); // At this point we have exactly two chunks which may violate topology constraints (the - // parent chunk and child chunk that were produced by deactivating dep_idx). We can fix + // parent chunk and child chunk that were produced by deactivation). We can fix // these using just merge sequences, one upwards and one downwards, avoiding the need for a // full MakeTopological. // Merge the top chunk with lower-feerate chunks it depends on (which may be the bottom it // was just split from, or other pre-existing chunks). - MergeSequence(dep_data.parent); + MergeSequence(parent_idx); // Merge the bottom chunk with higher-feerate chunks that depend on it. - MergeSequence(dep_data.child); + MergeSequence(child_idx); } public: @@ -993,35 +966,18 @@ public: m_tx_data.resize(depgraph.PositionRange()); m_set_info.resize(num_transactions); size_t num_chunks = 0; - - // Reserve the maximum number of (reserved) dependencies the cluster can have, so - // m_dep_data won't need any reallocations during construction. For a cluster with N - // transactions, the worst case consists of two sets of transactions, the parents and the - // children, where each child depends on each parent and nothing else. For even N, both - // sets can be sized N/2, which means N^2/4 dependencies. For odd N, one can be (N + 1)/2 - // and the other can be (N - 1)/2, meaning (N^2 - 1)/4 dependencies. Because N^2 is odd in - // this case, N^2/4 (with rounding-down division) is the correct value in both cases. - m_dep_data.reserve((num_transactions * num_transactions) / 4); for (auto tx_idx : m_transaction_idxs) { // Fill in transaction data. auto& tx_data = m_tx_data[tx_idx]; tx_data.parents = depgraph.GetReducedParents(tx_idx); + for (auto parent_idx : tx_data.parents) { + m_tx_data[parent_idx].children.Set(tx_idx); + } // Create a singleton chunk for it. tx_data.chunk_idx = num_chunks; m_set_info[num_chunks++] = SetInfo(depgraph, tx_idx); - // Add its dependencies. - for (auto parent_idx : tx_data.parents) { - auto& par_tx_data = m_tx_data[parent_idx]; - auto dep_idx = m_dep_data.size(); - // Construct new dependency. - auto& dep = m_dep_data.emplace_back(); - dep.active = false; - dep.parent = parent_idx; - dep.child = tx_idx; - // Add it as child of the parent. - par_tx_data.child_deps.push_back(dep_idx); - par_tx_data.children.Set(tx_idx); - } + // Mark all its dependencies inactive. + tx_data.dep_top_idx.assign(m_tx_data.size(), INVALID_SET_IDX); } Assume(num_chunks == num_transactions); // Mark all chunk sets as chunks. @@ -1111,18 +1067,16 @@ public: // happen when a split chunk merges in Improve() with one or more existing chunks that // are themselves on the suboptimal queue already. if (!m_chunk_idxs[chunk_idx]) continue; - // Remember the best dependency seen so far. - DepIdx candidate_dep = DepIdx(-1); + // Remember the best dependency {par, chl} seen so far. + std::pair candidate_dep = {TxIdx(-1), TxIdx(-1)}; uint64_t candidate_tiebreak = 0; // Iterate over all transactions. for (auto tx_idx : chunk_info.transactions) { const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. - const auto children = std::span{tx_data.child_deps}; - for (DepIdx dep_idx : children) { - const auto& dep_data = m_dep_data[dep_idx]; - if (!dep_data.active) continue; - auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; + for (auto child_idx : tx_data.children) { + if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // Skip if this dependency is ineligible (the top chunk that would be created // does not have higher feerate than the chunk it is currently part of). auto cmp = FeeRateCompare(dep_top_info.feerate, chunk_info.feerate); @@ -1133,13 +1087,15 @@ public: uint64_t tiebreak = m_rng.rand64(); if (tiebreak < candidate_tiebreak) continue; // Remember this as our (new) candidate dependency. - candidate_dep = dep_idx; + candidate_dep = {tx_idx, child_idx}; candidate_tiebreak = tiebreak; } } // If a candidate with positive gain was found, deactivate it and then make the state // topological again with a sequence of merges. - if (candidate_dep != DepIdx(-1)) Improve(candidate_dep); + if (candidate_dep.first != TxIdx(-1)) { + Improve(candidate_dep.first, candidate_dep.second); + } // Stop processing for now, even if nothing was activated, as the loop above may have // had a nontrivial cost. return !m_suboptimal_chunks.empty(); @@ -1183,18 +1139,17 @@ public: // Find a random dependency whose top and bottom set feerates are equal, and which has // pivot in bottom set (if move_pivot_down) or in top set (if !move_pivot_down). - DepIdx candidate_dep = DepIdx(-1); + std::pair candidate_dep; uint64_t candidate_tiebreak{0}; bool have_any = false; // Iterate over all transactions. for (auto tx_idx : chunk_info.transactions) { const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. - for (auto dep_idx : tx_data.child_deps) { - auto& dep_data = m_dep_data[dep_idx]; + for (auto child_idx : tx_data.children) { // Skip inactive child dependencies. - if (!dep_data.active) continue; - const auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; + if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + const auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // Skip if this dependency does not have equal top and bottom set feerates. Note // that the top cannot have higher feerate than the bottom, or OptimizeSteps would // have dealt with it. @@ -1206,7 +1161,7 @@ public: uint64_t tiebreak = m_rng.rand64() | 1; if (tiebreak > candidate_tiebreak) { candidate_tiebreak = tiebreak; - candidate_dep = dep_idx; + candidate_dep = {tx_idx, child_idx}; } } } @@ -1222,10 +1177,9 @@ public: } // Otherwise, deactivate the dependency that was found. - Deactivate(candidate_dep); - auto& dep_data = m_dep_data[candidate_dep]; - auto parent_chunk_idx = m_tx_data[dep_data.parent].chunk_idx; - auto child_chunk_idx = m_tx_data[dep_data.child].chunk_idx; + Deactivate(candidate_dep.first, candidate_dep.second); + auto parent_chunk_idx = m_tx_data[candidate_dep.first].chunk_idx; + auto child_chunk_idx = m_tx_data[candidate_dep.second].chunk_idx; // Try to activate a dependency between the new bottom and the new top (opposite from the // dependency that was just deactivated). auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); @@ -1445,29 +1399,24 @@ public: // Verify dependency parent/child information, and build list of (active) dependencies. // std::vector> expected_dependencies; - std::vector> all_dependencies; - std::vector> active_dependencies; + std::vector> all_dependencies; + std::vector> active_dependencies; for (auto parent_idx : m_depgraph.Positions()) { for (auto child_idx : m_depgraph.GetReducedChildren(parent_idx)) { expected_dependencies.emplace_back(parent_idx, child_idx); } } - for (DepIdx dep_idx = 0; dep_idx < m_dep_data.size(); ++dep_idx) { - const auto& dep_data = m_dep_data[dep_idx]; - all_dependencies.emplace_back(dep_data.parent, dep_data.child, dep_idx); - // Also add to active_dependencies if it is active. - if (m_dep_data[dep_idx].active) { - active_dependencies.emplace_back(dep_data.parent, dep_data.child, dep_idx); + for (auto tx_idx : m_transaction_idxs) { + for (auto child_idx : m_tx_data[tx_idx].children) { + all_dependencies.emplace_back(tx_idx, child_idx); + if (m_tx_data[tx_idx].dep_top_idx[child_idx] != INVALID_SET_IDX) { + active_dependencies.emplace_back(tx_idx, child_idx); + } } } std::sort(expected_dependencies.begin(), expected_dependencies.end()); std::sort(all_dependencies.begin(), all_dependencies.end()); - assert(expected_dependencies.size() == all_dependencies.size()); - for (size_t i = 0; i < expected_dependencies.size(); ++i) { - assert(expected_dependencies[i] == - std::make_pair(std::get<0>(all_dependencies[i]), - std::get<1>(all_dependencies[i]))); - } + assert(expected_dependencies == all_dependencies); // // Verify the chunks against the list of active dependencies @@ -1477,8 +1426,8 @@ public: const auto& chunk_info = m_set_info[chunk_idx]; // Verify that transactions in the chunk point back to it. This guarantees // that chunks are non-overlapping. - for (auto chunk_tx : chunk_info.transactions) { - assert(m_tx_data[chunk_tx].chunk_idx == chunk_idx); + for (auto tx_idx : chunk_info.transactions) { + assert(m_tx_data[tx_idx].chunk_idx == chunk_idx); } assert(!chunk_cover.Overlaps(chunk_info.transactions)); chunk_cover |= chunk_info.transactions; @@ -1491,7 +1440,7 @@ public: while (true) { auto old = expected_chunk; size_t active_dep_count{0}; - for (const auto& [par, chl, _dep] : active_dependencies) { + for (const auto& [par, chl] : active_dependencies) { if (expected_chunk[par] || expected_chunk[chl]) { expected_chunk.Set(par); expected_chunk.Set(chl); @@ -1522,35 +1471,23 @@ public: // Verify parents/children. assert(tx_data.parents == m_depgraph.GetReducedParents(tx_idx)); assert(tx_data.children == m_depgraph.GetReducedChildren(tx_idx)); - // Verify list of child dependencies. - std::vector expected_child_deps; - for (const auto& [par_idx, chl_idx, dep_idx] : all_dependencies) { - if (tx_idx == par_idx) { - assert(tx_data.children[chl_idx]); - expected_child_deps.push_back(dep_idx); - } - } - std::sort(expected_child_deps.begin(), expected_child_deps.end()); - auto child_deps_copy = tx_data.child_deps; - std::sort(child_deps_copy.begin(), child_deps_copy.end()); - assert(expected_child_deps == child_deps_copy); } // // Verify active dependencies' top sets. // - for (const auto& [par_idx, chl_idx, dep_idx] : active_dependencies) { - const auto& dep_data = m_dep_data[dep_idx]; + for (const auto& [par_idx, chl_idx] : active_dependencies) { // Verify the top set's transactions: it must contain the parent, and for every - // active dependency, except dep_idx itself, if it contains the parent or child, it - // must contain both. It must have exactly N-1 active dependencies in it, guaranteeing - // it is acyclic. + // active dependency, except the chl_idx->par_idx dependency itself, if it contains the + // parent or child, it must contain both. It must have exactly N-1 active dependencies + // in it, guaranteeing it is acyclic. SetType expected_top = SetType::Singleton(par_idx); while (true) { auto old = expected_top; size_t active_dep_count{0}; - for (const auto& [par2_idx, chl2_idx, dep2_idx] : active_dependencies) { - if (dep2_idx != dep_idx && (expected_top[par2_idx] || expected_top[chl2_idx])) { + for (const auto& [par2_idx, chl2_idx] : active_dependencies) { + if (par_idx == par2_idx && chl_idx == chl2_idx) continue; + if (expected_top[par2_idx] || expected_top[chl2_idx]) { expected_top.Set(par2_idx); expected_top.Set(chl2_idx); ++active_dep_count; @@ -1562,7 +1499,7 @@ public: } } assert(!expected_top[chl_idx]); - auto& dep_top_info = m_set_info[dep_data.dep_top_idx]; + auto& dep_top_info = m_set_info[m_tx_data[par_idx].dep_top_idx[chl_idx]]; assert(dep_top_info.transactions == expected_top); // Verify the top set's feerate. assert(dep_top_info.feerate == m_depgraph.FeeRate(dep_top_info.transactions)); From b75574a6531ebe1f4077482705ad9e2db0ed3438 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 21 Dec 2025 16:56:14 -0500 Subject: [PATCH 09/20] clusterlin: improve TxData::dep_top_idx type (optimization) The combined size of TxData::dep_top_idx can be 16 KiB with 64 transactions and SetIdx = uint32_t. Use a smaller type where possible to reduce memory footprint and improve cache locality of m_tx_data. Also switch from an std::vector to an std::array, reducing allocation overhead and indirections. --- src/cluster_linearize.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 50606942e9f..2b935da6562 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -647,9 +647,13 @@ private: /** Data type to represent indexing into m_tx_data. */ using TxIdx = DepGraphIndex; - /** Data type to represent indexing into m_set_info. */ - using SetIdx = uint32_t; - + /** Data type to represent indexing into m_set_info. Use the smallest type possible to improve + * cache locality. */ + using SetIdx = std::conditional_t<(SetType::Size() <= 0xff), + uint8_t, + std::conditional_t<(SetType::Size() <= 0xffff), + uint16_t, + uint32_t>>; /** An invalid SetIdx. */ static constexpr SetIdx INVALID_SET_IDX = SetIdx(-1); @@ -658,7 +662,7 @@ private: /** The top set for every active child dependency this transaction has, indexed by child * TxIdx. INVALID_SET_IDX if there is no active dependency with the corresponding child. */ - std::vector dep_top_idx; + std::array dep_top_idx; /** The set of parent transactions of this transaction. Immutable after construction. */ SetType parents; /** The set of child transactions of this transaction. Immutable after construction. */ @@ -977,7 +981,7 @@ public: tx_data.chunk_idx = num_chunks; m_set_info[num_chunks++] = SetInfo(depgraph, tx_idx); // Mark all its dependencies inactive. - tx_data.dep_top_idx.assign(m_tx_data.size(), INVALID_SET_IDX); + tx_data.dep_top_idx.fill(INVALID_SET_IDX); } Assume(num_chunks == num_transactions); // Mark all chunk sets as chunks. From cbd684a4713dbe1adc77986e5924867f0e22fb6a Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 10:42:19 -0500 Subject: [PATCH 10/20] clusterlin: abstract out functions from MergeStep (refactor) This is a simple refactor to make the code more readable. --- src/cluster_linearize.h | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 2b935da6562..3864de7ec9a 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -864,12 +864,24 @@ private: return INVALID_SET_IDX; } - /** Perform an upward or downward merge step, on the specified chunk. Returns the merged chunk, - * or INVALID_SET_IDX if no merge took place. */ + /** Activate a dependency from chunk_idx to merge_chunk_idx (if !DownWard), or a dependency + * from merge_chunk_idx to chunk_idx (if DownWard). Return the index of the merged chunk. */ template - SetIdx MergeStep(SetIdx chunk_idx) noexcept + SetIdx MergeChunksDirected(SetIdx chunk_idx, SetIdx merge_chunk_idx) noexcept + { + if constexpr (DownWard) { + return MergeChunks(chunk_idx, merge_chunk_idx); + } else { + return MergeChunks(merge_chunk_idx, chunk_idx); + } + } + + /** Determine which chunk to merge chunk_idx with, or INVALID_SET_IDX if none. */ + template + SetIdx PickMergeCandidate(SetIdx chunk_idx) noexcept { /** Information about the chunk. */ + Assume(m_chunk_idxs[chunk_idx]); auto& chunk_info = m_set_info[chunk_idx]; SetType chunk_txn = chunk_info.transactions; // Iterate over all transactions in the chunk, figuring out which other chunk each @@ -913,18 +925,21 @@ private: } } } - // Stop if there are no candidate chunks to merge with. - if (best_other_chunk_idx == INVALID_SET_IDX) return INVALID_SET_IDX; - if constexpr (DownWard) { - chunk_idx = MergeChunks(chunk_idx, best_other_chunk_idx); - } else { - chunk_idx = MergeChunks(best_other_chunk_idx, chunk_idx); - } + return best_other_chunk_idx; + } + + /** Perform an upward or downward merge step, on the specified chunk. Returns the merged chunk, + * or INVALID_SET_IDX if no merge took place. */ + template + SetIdx MergeStep(SetIdx chunk_idx) noexcept + { + auto merge_chunk_idx = PickMergeCandidate(chunk_idx); + if (merge_chunk_idx == INVALID_SET_IDX) return INVALID_SET_IDX; + chunk_idx = MergeChunksDirected(chunk_idx, merge_chunk_idx); Assume(chunk_idx != INVALID_SET_IDX); return chunk_idx; } - /** Perform an upward or downward merge sequence on the specified transaction. */ template void MergeSequence(TxIdx tx_idx) noexcept From dcf458ffb99c57c31c5c53e001c98fc1a8baadec Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 23 Jan 2026 23:06:32 -0500 Subject: [PATCH 11/20] clusterlin: split up OptimizeStep (refactor) --- src/cluster_linearize.h | 102 +++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 3864de7ec9a..894a3dcfe8c 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -974,6 +974,54 @@ private: MergeSequence(child_idx); } + /** Determine the next chunk to optimize, or INVALID_SET_IDX if none. */ + SetIdx PickChunkToOptimize() noexcept + { + while (!m_suboptimal_chunks.empty()) { + // Pop an entry from the potentially-suboptimal chunk queue. + SetIdx chunk_idx = m_suboptimal_chunks.front(); + m_suboptimal_chunks.pop_front(); + if (m_chunk_idxs[chunk_idx]) return chunk_idx; + // If what was popped is not currently a chunk, continue. This may + // happen when a split chunk merges in Improve() with one or more existing chunks that + // are themselves on the suboptimal queue already. + } + return INVALID_SET_IDX; + } + + /** Find a (parent, child) dependency to deactivate in chunk_idx, or (-1, -1) if none. */ + std::pair PickDependencyToSplit(SetIdx chunk_idx) noexcept + { + Assume(m_chunk_idxs[chunk_idx]); + auto& chunk_info = m_set_info[chunk_idx]; + + // Remember the best dependency {par, chl} seen so far. + std::pair candidate_dep = {TxIdx(-1), TxIdx(-1)}; + uint64_t candidate_tiebreak = 0; + // Iterate over all transactions. + for (auto tx_idx : chunk_info.transactions) { + const auto& tx_data = m_tx_data[tx_idx]; + // Iterate over all active child dependencies of the transaction. + for (auto child_idx : tx_data.children) { + if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; + // Skip if this dependency is ineligible (the top chunk that would be created + // does not have higher feerate than the chunk it is currently part of). + auto cmp = FeeRateCompare(dep_top_info.feerate, chunk_info.feerate); + if (cmp <= 0) continue; + // Generate a random tiebreak for this dependency, and reject it if its tiebreak + // is worse than the best so far. This means that among all eligible + // dependencies, a uniformly random one will be chosen. + uint64_t tiebreak = m_rng.rand64(); + if (tiebreak < candidate_tiebreak) continue; + // Remember this as our (new) candidate dependency. + candidate_dep = {tx_idx, child_idx}; + candidate_tiebreak = tiebreak; + } + } + return candidate_dep; + } + public: /** Construct a spanning forest for the given DepGraph, with every transaction in its own chunk * (not topological). */ @@ -1077,50 +1125,20 @@ public: /** Try to improve the forest. Returns false if it is optimal, true otherwise. */ bool OptimizeStep() noexcept { - while (!m_suboptimal_chunks.empty()) { - // Pop an entry from the potentially-suboptimal chunk queue. - SetIdx chunk_idx = m_suboptimal_chunks.front(); - m_suboptimal_chunks.pop_front(); - auto& chunk_info = m_set_info[chunk_idx]; - // If what was popped is not currently a chunk, continue. This may - // happen when a split chunk merges in Improve() with one or more existing chunks that - // are themselves on the suboptimal queue already. - if (!m_chunk_idxs[chunk_idx]) continue; - // Remember the best dependency {par, chl} seen so far. - std::pair candidate_dep = {TxIdx(-1), TxIdx(-1)}; - uint64_t candidate_tiebreak = 0; - // Iterate over all transactions. - for (auto tx_idx : chunk_info.transactions) { - const auto& tx_data = m_tx_data[tx_idx]; - // Iterate over all active child dependencies of the transaction. - for (auto child_idx : tx_data.children) { - if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; - auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; - // Skip if this dependency is ineligible (the top chunk that would be created - // does not have higher feerate than the chunk it is currently part of). - auto cmp = FeeRateCompare(dep_top_info.feerate, chunk_info.feerate); - if (cmp <= 0) continue; - // Generate a random tiebreak for this dependency, and reject it if its tiebreak - // is worse than the best so far. This means that among all eligible - // dependencies, a uniformly random one will be chosen. - uint64_t tiebreak = m_rng.rand64(); - if (tiebreak < candidate_tiebreak) continue; - // Remember this as our (new) candidate dependency. - candidate_dep = {tx_idx, child_idx}; - candidate_tiebreak = tiebreak; - } - } - // If a candidate with positive gain was found, deactivate it and then make the state - // topological again with a sequence of merges. - if (candidate_dep.first != TxIdx(-1)) { - Improve(candidate_dep.first, candidate_dep.second); - } - // Stop processing for now, even if nothing was activated, as the loop above may have - // had a nontrivial cost. + auto chunk_idx = PickChunkToOptimize(); + if (chunk_idx == INVALID_SET_IDX) { + // No improvable chunk was found, we are done. + return false; + } + auto [parent_idx, child_idx] = PickDependencyToSplit(chunk_idx); + if (parent_idx == TxIdx(-1)) { + // Nothing to improve in chunk_idx. Need to continue with other chunks, if any. return !m_suboptimal_chunks.empty(); } - // No improvable chunk was found, we are done. - return false; + // Deactivate the found dependency and then make the state topological again with a + // sequence of merges. + Improve(parent_idx, child_idx); + return true; } /** Initialize data structure for minimizing the chunks. Can only be called if state is known From 6f898dbb8bfad68b3c2168471ced211222d6f7f3 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 10:50:35 -0500 Subject: [PATCH 12/20] clusterlin: simplify PickMergeCandidate (optimization) The current process consists of iterating over the transactions of the chunk one by one, and then for each figuring out which of its parents/children are in unprocessed chunks. Simplify this (and speed it up slightly) by splitting this process into two phases: first determine the union of all parents/children, and then find which chunks those belong to. --- src/cluster_linearize.h | 65 ++++++++++++++++++------------- src/test/util/cluster_linearize.h | 14 +++---- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 894a3dcfe8c..de5c6b20cc6 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -742,6 +742,22 @@ private: } } + /** Find the set of out-of-chunk transactions reachable from tx_idxs. */ + template + SetType GetReachable(const SetType& tx_idxs) const noexcept + { + SetType ret; + for (auto tx_idx : tx_idxs) { + const auto& tx_data = m_tx_data[tx_idx]; + if constexpr (DownWard) { + ret |= tx_data.children; + } else { + ret |= tx_data.parents; + } + } + return ret - tx_idxs; + } + /** Make the inactive dependency from child to parent, which must not be in the same chunk * already, active. Returns the merged chunk idx. */ SetIdx Activate(TxIdx parent_idx, TxIdx child_idx) noexcept @@ -883,16 +899,11 @@ private: /** Information about the chunk. */ Assume(m_chunk_idxs[chunk_idx]); auto& chunk_info = m_set_info[chunk_idx]; - SetType chunk_txn = chunk_info.transactions; - // Iterate over all transactions in the chunk, figuring out which other chunk each - // depends on, but only testing each other chunk once. For those depended-on chunks, + // Iterate over all chunks reachable from this one. For those depended-on chunks, // remember the highest-feerate (if DownWard) or lowest-feerate (if !DownWard) one. // If multiple equal-feerate candidate chunks to merge with exist, pick a random one // among them. - /** Which transactions have been reached from this chunk already. Initialize with the - * chunk itself, so internal dependencies within the chunk are ignored. */ - SetType explored = chunk_txn; /** The minimum feerate (if downward) or maximum feerate (if upward) to consider when * looking for candidate chunks to merge with. Initially, this is the original chunk's * feerate, but is updated to be the current best candidate whenever one is found. */ @@ -902,29 +913,29 @@ private: /** We generate random tiebreak values to pick between equal-feerate candidate chunks. * This variable stores the tiebreak of the current best candidate. */ uint64_t best_other_chunk_tiebreak{0}; - for (auto tx_idx : chunk_txn) { - auto& tx_data = m_tx_data[tx_idx]; - /** The transactions reached by following dependencies from tx that have not been - * explored before. */ - auto newly_reached = (DownWard ? tx_data.children : tx_data.parents) - explored; - explored |= newly_reached; - while (newly_reached.Any()) { - // Find a chunk inside newly_reached, and remove it from newly_reached. - auto reached_chunk_idx = m_tx_data[newly_reached.First()].chunk_idx; - auto& reached_chunk_info = m_set_info[reached_chunk_idx]; - newly_reached -= reached_chunk_info.transactions; - // See if it has an acceptable feerate. - auto cmp = DownWard ? FeeRateCompare(best_other_chunk_feerate, reached_chunk_info.feerate) - : FeeRateCompare(reached_chunk_info.feerate, best_other_chunk_feerate); - if (cmp > 0) continue; - uint64_t tiebreak = m_rng.rand64(); - if (cmp < 0 || tiebreak >= best_other_chunk_tiebreak) { - best_other_chunk_feerate = reached_chunk_info.feerate; - best_other_chunk_idx = reached_chunk_idx; - best_other_chunk_tiebreak = tiebreak; - } + + /** Which parent/child transactions we still need to process the chunks for. */ + auto todo = GetReachable(chunk_info.transactions); + unsigned steps = 0; + while (todo.Any()) { + ++steps; + // Find a chunk for a transaction in todo, and remove all its transactions from todo. + auto reached_chunk_idx = m_tx_data[todo.First()].chunk_idx; + auto& reached_chunk_info = m_set_info[reached_chunk_idx]; + todo -= reached_chunk_info.transactions; + // See if it has an acceptable feerate. + auto cmp = DownWard ? FeeRateCompare(best_other_chunk_feerate, reached_chunk_info.feerate) + : FeeRateCompare(reached_chunk_info.feerate, best_other_chunk_feerate); + if (cmp > 0) continue; + uint64_t tiebreak = m_rng.rand64(); + if (cmp < 0 || tiebreak >= best_other_chunk_tiebreak) { + best_other_chunk_feerate = reached_chunk_info.feerate; + best_other_chunk_idx = reached_chunk_idx; + best_other_chunk_tiebreak = tiebreak; } } + Assume(steps <= m_set_info.size()); + return best_other_chunk_idx; } diff --git a/src/test/util/cluster_linearize.h b/src/test/util/cluster_linearize.h index 7e98312cbbb..f14b00d5d7b 100644 --- a/src/test/util/cluster_linearize.h +++ b/src/test/util/cluster_linearize.h @@ -403,13 +403,13 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count) static constexpr uint64_t ITERS[65] = { 0, 0, 4, 10, 34, 76, 156, 229, 380, - 432, 607, 738, 896, 1037, 1366, 1464, 1711, - 2060, 2542, 3068, 3116, 4029, 3467, 5324, 5402, - 6481, 7161, 7441, 8329, 8843, 9353, 11104, 11269, - 11791, 11981, 12413, 14259, 15331, 12397, 13581, 18569, - 18737, 16581, 23217, 23271, 27350, 28591, 33636, 34486, - 34414, 26227, 35570, 38045, 40814, 29622, 37793, 32122, - 35915, 49823, 39722, 43765, 42365, 53620, 59417, 67035 + 432, 517, 678, 896, 1037, 1366, 1479, 1711, + 2060, 2542, 3068, 3116, 4029, 3467, 5324, 5512, + 6481, 7161, 7441, 8183, 8843, 9353, 11104, 11269, + 12354, 11871, 13367, 14259, 14229, 12397, 13581, 17774, + 18737, 16581, 23217, 24044, 29597, 28879, 34069, 34162, + 36028, 26227, 34471, 37212, 40814, 29554, 40305, 34019, + 36582, 55659, 39994, 41277, 42365, 52822, 60151, 67035 }; assert(cluster_count < std::size(ITERS)); // Multiply the table number by two, to account for the fact that they are not absolutes. From 7194de3f7c5324a6e719ed25ef292d162b4f5d88 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 11:39:21 -0500 Subject: [PATCH 13/20] clusterlin: precompute reachable sets (optimization) Instead of computing the set of reachable transactions inside PickMergeCandidate, make the information precomputed, and updated in Activate (by merging the two chunks' reachable sets) and Deactivate (by recomputing). This is a small performance gain on itself, but also a preparation for future optimizations that rely on quickly testing whether dependencies between chunks exist. --- src/cluster_linearize.h | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index de5c6b20cc6..58a7b8b191b 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -681,6 +681,9 @@ private: std::vector m_tx_data; /** Information about each set (chunk, or active dependency top set). Indexed by SetIdx. */ std::vector> m_set_info; + /** For each chunk, indexed by SetIdx, the set of out-of-chunk reachable transactions, in the + * upwards (.first) and downwards (.second) direction. */ + std::vector> m_reachable; /** A FIFO of chunk SetIdxs for chunks that may be improved still. */ VecDeque m_suboptimal_chunks; /** A FIFO of chunk indexes with a pivot transaction in them, and a flag to indicate their @@ -742,20 +745,17 @@ private: } } - /** Find the set of out-of-chunk transactions reachable from tx_idxs. */ - template - SetType GetReachable(const SetType& tx_idxs) const noexcept + /** Find the set of out-of-chunk transactions reachable from tx_idxs, both in upwards and + * downwards direction. */ + std::pair GetReachable(const SetType& tx_idxs) const noexcept { - SetType ret; + SetType parents, children; for (auto tx_idx : tx_idxs) { const auto& tx_data = m_tx_data[tx_idx]; - if constexpr (DownWard) { - ret |= tx_data.children; - } else { - ret |= tx_data.parents; - } + parents |= tx_data.parents; + children |= tx_data.children; } - return ret - tx_idxs; + return {parents - tx_idxs, children - tx_idxs}; } /** Make the inactive dependency from child to parent, which must not be in the same chunk @@ -807,6 +807,13 @@ private: // Merge top_info into bottom_info, which becomes the merged chunk. bottom_info |= top_info; m_cost += bottom_info.transactions.Count(); + // Compute merged sets of reachable transactions from the new chunk. There is no need to + // call GetReachable here, because they can be computed directly from the input chunks' + // reachable sets. + m_reachable[child_chunk_idx].first |= m_reachable[parent_chunk_idx].first; + m_reachable[child_chunk_idx].second |= m_reachable[parent_chunk_idx].second; + m_reachable[child_chunk_idx].first -= bottom_info.transactions; + m_reachable[child_chunk_idx].second -= bottom_info.transactions; // Make parent chunk the set for the new active dependency. parent_data.dep_top_idx[child_idx] = parent_chunk_idx; m_chunk_idxs.Reset(parent_chunk_idx); @@ -843,6 +850,9 @@ private: /*chunk_idx=*/parent_chunk_idx, /*dep_change=*/bottom_info); UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/child_idx, /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); + // Compute the new sets of reachable transactions for each new chunk. + m_reachable[child_chunk_idx] = GetReachable(bottom_info.transactions); + m_reachable[parent_chunk_idx] = GetReachable(top_info.transactions); } /** Activate a dependency from the bottom set to the top set. Return the index of the merged @@ -915,7 +925,7 @@ private: uint64_t best_other_chunk_tiebreak{0}; /** Which parent/child transactions we still need to process the chunks for. */ - auto todo = GetReachable(chunk_info.transactions); + auto todo = DownWard ? m_reachable[chunk_idx].second : m_reachable[chunk_idx].first; unsigned steps = 0; while (todo.Any()) { ++steps; @@ -1043,6 +1053,7 @@ public: auto num_transactions = m_transaction_idxs.Count(); m_tx_data.resize(depgraph.PositionRange()); m_set_info.resize(num_transactions); + m_reachable.resize(num_transactions); size_t num_chunks = 0; for (auto tx_idx : m_transaction_idxs) { // Fill in transaction data. @@ -1057,6 +1068,12 @@ public: // Mark all its dependencies inactive. tx_data.dep_top_idx.fill(INVALID_SET_IDX); } + // Set the reachable transactions for each chunk to the transactions' parents and children. + for (SetIdx chunk_idx = 0; chunk_idx < num_transactions; ++chunk_idx) { + auto& tx_data = m_tx_data[m_set_info[chunk_idx].transactions.First()]; + m_reachable[chunk_idx].first = tx_data.parents; + m_reachable[chunk_idx].second = tx_data.children; + } Assume(num_chunks == num_transactions); // Mark all chunk sets as chunks. m_chunk_idxs = SetType::Fill(num_chunks); @@ -1503,6 +1520,11 @@ public: assert(chunk_info.transactions == expected_chunk); // Verify the chunk's feerate. assert(chunk_info.feerate == m_depgraph.FeeRate(chunk_info.transactions)); + // Verify the chunk's reachable transactions. + assert(m_reachable[chunk_idx] == GetReachable(expected_chunk)); + // Verify that the chunk's reachable transactions don't include its own transactions. + assert(!m_reachable[chunk_idx].first.Overlaps(chunk_info.transactions)); + assert(!m_reachable[chunk_idx].second.Overlaps(chunk_info.transactions)); } // Verify that together, the chunks cover all transactions. assert(chunk_cover == m_depgraph.Positions()); From 3221f1a074e7b313e97f969d11ff8a13a1dc5aa6 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 14:37:28 -0500 Subject: [PATCH 14/20] clusterlin: make MergeSequence take SetIdx (simplification) Future changes will rely on knowing the chunk indexes of the two created chunks after a split. It is natural to return this information from Deactivate, which also simplifies MergeSequence. --- src/cluster_linearize.h | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 58a7b8b191b..47caf1f34b9 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -821,8 +821,9 @@ private: return child_chunk_idx; } - /** Make a specified active dependency inactive. */ - void Deactivate(TxIdx parent_idx, TxIdx child_idx) noexcept + /** Make a specified active dependency inactive. Returns the created parent and child chunk + * indexes. */ + std::pair Deactivate(TxIdx parent_idx, TxIdx child_idx) noexcept { // Gather and check information about the parent transactions. auto& parent_data = m_tx_data[parent_idx]; @@ -853,6 +854,8 @@ private: // Compute the new sets of reachable transactions for each new chunk. m_reachable[child_chunk_idx] = GetReachable(bottom_info.transactions); m_reachable[parent_chunk_idx] = GetReachable(top_info.transactions); + // Return the two new set idxs. + return {parent_chunk_idx, child_chunk_idx}; } /** Activate a dependency from the bottom set to the top set. Return the index of the merged @@ -961,11 +964,11 @@ private: return chunk_idx; } - /** Perform an upward or downward merge sequence on the specified transaction. */ + /** Perform an upward or downward merge sequence on the specified chunk. */ template - void MergeSequence(TxIdx tx_idx) noexcept + void MergeSequence(SetIdx chunk_idx) noexcept { - auto chunk_idx = m_tx_data[tx_idx].chunk_idx; + Assume(m_chunk_idxs[chunk_idx]); while (true) { auto merged_chunk_idx = MergeStep(chunk_idx); if (merged_chunk_idx == INVALID_SET_IDX) break; @@ -981,7 +984,7 @@ private: { // Deactivate the specified dependency, splitting it into two new chunks: a top containing // the parent, and a bottom containing the child. The top should have a higher feerate. - Deactivate(parent_idx, child_idx); + auto [parent_chunk_idx, child_chunk_idx] = Deactivate(parent_idx, child_idx); // At this point we have exactly two chunks which may violate topology constraints (the // parent chunk and child chunk that were produced by deactivation). We can fix @@ -990,9 +993,10 @@ private: // Merge the top chunk with lower-feerate chunks it depends on (which may be the bottom it // was just split from, or other pre-existing chunks). - MergeSequence(parent_idx); - // Merge the bottom chunk with higher-feerate chunks that depend on it. - MergeSequence(child_idx); + MergeSequence(parent_chunk_idx); + // Merge the bottom chunk with higher-feerate chunks that depend on it (if it wasn't merged + // with the top already). + if (m_chunk_idxs[child_chunk_idx]) MergeSequence(child_chunk_idx); } /** Determine the next chunk to optimize, or INVALID_SET_IDX if none. */ @@ -1242,9 +1246,7 @@ public: } // Otherwise, deactivate the dependency that was found. - Deactivate(candidate_dep.first, candidate_dep.second); - auto parent_chunk_idx = m_tx_data[candidate_dep.first].chunk_idx; - auto child_chunk_idx = m_tx_data[candidate_dep.second].chunk_idx; + auto [parent_chunk_idx, child_chunk_idx] = Deactivate(candidate_dep.first, candidate_dep.second); // Try to activate a dependency between the new bottom and the new top (opposite from the // dependency that was just deactivated). auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); From ae16485aa94dc745556615d569914b75ea855f02 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 11 Jan 2026 22:35:43 -0500 Subject: [PATCH 15/20] clusterlin: special-case self-merges (optimization) After a split, if the top part has a dependency on the bottom part, the first MergeSequence will always perform this merge and then stop. This is referred to as a self-merge. We can special case these by detecting self-merges early, and avoiding the overhead of a full MergeSequence which involves two PickMergeCandidate calls (a succesful and an unsuccesful one). --- src/cluster_linearize.h | 47 +++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 47caf1f34b9..d31cfa9127e 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -631,6 +631,11 @@ using IndexTxOrder = std::compare_three_way; * - Inside the selected chunk (see above), among the dependencies whose top feerate is strictly * higher than its bottom feerate in the selected chunk, if any, a uniformly random dependency * is deactivated. + * - After every split, it is possible that the top and the bottom chunk merge with each other + * again in the merge sequence (through a top->bottom dependency, not through the deactivated + * one, which was bottom->top). Call this a self-merge. If a self-merge does not occur after + * a split, the resulting linearization is strictly improved (the area under the convexified + * feerate diagram increases by at least gain/2), while self-merges do not change it. * * - How to decide the exact output linearization: * - When there are multiple equal-feerate chunks with no dependencies between them, output a @@ -858,8 +863,8 @@ private: return {parent_chunk_idx, child_chunk_idx}; } - /** Activate a dependency from the bottom set to the top set. Return the index of the merged - * chunk, or INVALID_SET_IDX if no merge is possible. */ + /** Activate a dependency from the bottom set to the top set, which must exist. Return the + * index of the merged chunk. */ SetIdx MergeChunks(SetIdx top_idx, SetIdx bottom_idx) noexcept { Assume(m_chunk_idxs[top_idx]); @@ -872,7 +877,7 @@ private: auto& tx_data = m_tx_data[tx_idx]; num_deps += (tx_data.children & bottom_chunk_info.transactions).Count(); } - if (num_deps == 0) return INVALID_SET_IDX; + Assume(num_deps > 0); // Uniformly randomly pick one of them and activate it. unsigned pick = m_rng.randrange(num_deps); for (auto tx_idx : top_chunk_info.transactions) { @@ -990,13 +995,25 @@ private: // parent chunk and child chunk that were produced by deactivation). We can fix // these using just merge sequences, one upwards and one downwards, avoiding the need for a // full MakeTopological. + const auto& parent_reachable = m_reachable[parent_chunk_idx].first; + const auto& child_chunk_txn = m_set_info[child_chunk_idx].transactions; + if (parent_reachable.Overlaps(child_chunk_txn)) { + // The parent chunk has a dependency on a transaction in the child chunk. In this case, + // the parent needs to merge back with the child chunk (a self-merge), and no other + // merges are needed. Special-case this, so the overhead of PickMergeCandidate and + // MergeSequence can be avoided. - // Merge the top chunk with lower-feerate chunks it depends on (which may be the bottom it - // was just split from, or other pre-existing chunks). - MergeSequence(parent_chunk_idx); - // Merge the bottom chunk with higher-feerate chunks that depend on it (if it wasn't merged - // with the top already). - if (m_chunk_idxs[child_chunk_idx]) MergeSequence(child_chunk_idx); + // In the self-merge, the roles reverse: the parent chunk (from the split) depends + // on the child chunk, so child_chunk_idx is the "top" and parent_chunk_idx is the + // "bottom" for MergeChunks. + auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); + m_suboptimal_chunks.push_back(merged_chunk_idx); + } else { + // Merge the top chunk with lower-feerate chunks it depends on. + MergeSequence(parent_chunk_idx); + // Merge the bottom chunk with higher-feerate chunks that depend on it. + MergeSequence(child_chunk_idx); + } } /** Determine the next chunk to optimize, or INVALID_SET_IDX if none. */ @@ -1247,11 +1264,15 @@ public: // Otherwise, deactivate the dependency that was found. auto [parent_chunk_idx, child_chunk_idx] = Deactivate(candidate_dep.first, candidate_dep.second); - // Try to activate a dependency between the new bottom and the new top (opposite from the + // Determine if there is a dependency from the new bottom to the new top (opposite from the // dependency that was just deactivated). - auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); - if (merged_chunk_idx != INVALID_SET_IDX) { - // A self-merge happened. + auto& parent_reachable = m_reachable[parent_chunk_idx].first; + auto& child_chunk_txn = m_set_info[child_chunk_idx].transactions; + if (parent_reachable.Overlaps(child_chunk_txn)) { + // A self-merge is needed. Note that the child_chunk_idx is the top, and + // parent_chunk_idx is the bottom, because we activate a dependency in the reverse + // direction compared to the deactivation above. + auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); // Re-insert the chunk into the queue, in the same direction. Note that the chunk_idx // will have changed. m_nonminimal_chunks.emplace_back(merged_chunk_idx, pivot_idx, flags); From 63b06d5523f1da37f76dc1f8b659959a127975f3 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 11 Jan 2026 19:23:58 -0500 Subject: [PATCH 16/20] clusterlin: keep track of active children (optimization) This means we can iterate over all active dependencies in a cluster/chunk in O(ntx) time rather than O(ndeps) (*), as the number of active dependencies in a set of transactions of size is at most ntx-1. (*) Asymptotically, this is not actually true, as for large transaction counts, iterating over a BitSet still scales with ntx. In practice however, where BitSets are represented by a constant number of integers, it holds. --- src/cluster_linearize.h | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index d31cfa9127e..eace34ea392 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -665,13 +665,14 @@ private: /** Structure with information about a single transaction. */ struct TxData { /** The top set for every active child dependency this transaction has, indexed by child - * TxIdx. INVALID_SET_IDX if there is no active dependency with the corresponding child. - */ + * TxIdx. Only defined for indexes in active_children. */ std::array dep_top_idx; /** The set of parent transactions of this transaction. Immutable after construction. */ SetType parents; /** The set of child transactions of this transaction. Immutable after construction. */ SetType children; + /** The set of child transactions reachable through an active dependency. */ + SetType active_children; /** Which chunk this transaction belongs to. */ SetIdx chunk_idx; }; @@ -733,9 +734,7 @@ private: tx_data.chunk_idx = chunk_idx; // Iterate over all active dependencies with tx_idx as parent. Combined with the outer // loop this iterates over all internal active dependencies of the chunk. - for (auto child_idx : tx_data.children) { - // Skip inactive dependencies. - if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + for (auto child_idx : tx_data.active_children) { auto& top_set_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // If this dependency's top set contains query, update it to add/remove // dep_change. @@ -771,7 +770,7 @@ private: auto& parent_data = m_tx_data[parent_idx]; auto& child_data = m_tx_data[child_idx]; Assume(parent_data.children[child_idx]); - Assume(parent_data.dep_top_idx[child_idx] == INVALID_SET_IDX); + Assume(!parent_data.active_children[child_idx]); // Get the set index of the chunks the parent and child are currently in. The parent chunk // will become the top set of the newly activated dependency, while the child chunk will be // grown to become the merged chunk. @@ -821,6 +820,7 @@ private: m_reachable[child_chunk_idx].second -= bottom_info.transactions; // Make parent chunk the set for the new active dependency. parent_data.dep_top_idx[child_idx] = parent_chunk_idx; + parent_data.active_children.Set(child_idx); m_chunk_idxs.Reset(parent_chunk_idx); // Return the newly merged chunk. return child_chunk_idx; @@ -833,7 +833,7 @@ private: // Gather and check information about the parent transactions. auto& parent_data = m_tx_data[parent_idx]; Assume(parent_data.children[child_idx]); - Assume(parent_data.dep_top_idx[child_idx] != INVALID_SET_IDX); + Assume(parent_data.active_children[child_idx]); // Get the top set of the active dependency (which will become the parent chunk) and the // chunk set the transactions are currently in (which will become the bottom chunk). auto parent_chunk_idx = parent_data.dep_top_idx[child_idx]; @@ -845,7 +845,7 @@ private: auto& bottom_info = m_set_info[child_chunk_idx]; // Remove the active dependency. - parent_data.dep_top_idx[child_idx] = INVALID_SET_IDX; + parent_data.active_children.Reset(child_idx); m_chunk_idxs.Set(parent_chunk_idx); m_cost += bottom_info.transactions.Count(); // Subtract the top_info from the bottom_info, as it will become the child chunk. @@ -1044,8 +1044,7 @@ private: for (auto tx_idx : chunk_info.transactions) { const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. - for (auto child_idx : tx_data.children) { - if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + for (auto child_idx : tx_data.active_children) { auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // Skip if this dependency is ineligible (the top chunk that would be created // does not have higher feerate than the chunk it is currently part of). @@ -1086,8 +1085,6 @@ public: // Create a singleton chunk for it. tx_data.chunk_idx = num_chunks; m_set_info[num_chunks++] = SetInfo(depgraph, tx_idx); - // Mark all its dependencies inactive. - tx_data.dep_top_idx.fill(INVALID_SET_IDX); } // Set the reachable transactions for each chunk to the transactions' parents and children. for (SetIdx chunk_idx = 0; chunk_idx < num_transactions; ++chunk_idx) { @@ -1232,9 +1229,7 @@ public: for (auto tx_idx : chunk_info.transactions) { const auto& tx_data = m_tx_data[tx_idx]; // Iterate over all active child dependencies of the transaction. - for (auto child_idx : tx_data.children) { - // Skip inactive child dependencies. - if (tx_data.dep_top_idx[child_idx] == INVALID_SET_IDX) continue; + for (auto child_idx : tx_data.active_children) { const auto& dep_top_info = m_set_info[tx_data.dep_top_idx[child_idx]]; // Skip if this dependency does not have equal top and bottom set feerates. Note // that the top cannot have higher feerate than the bottom, or OptimizeSteps would @@ -1497,7 +1492,7 @@ public: for (auto tx_idx : m_transaction_idxs) { for (auto child_idx : m_tx_data[tx_idx].children) { all_dependencies.emplace_back(tx_idx, child_idx); - if (m_tx_data[tx_idx].dep_top_idx[child_idx] != INVALID_SET_IDX) { + if (m_tx_data[tx_idx].active_children[child_idx]) { active_dependencies.emplace_back(tx_idx, child_idx); } } @@ -1564,6 +1559,13 @@ public: // Verify parents/children. assert(tx_data.parents == m_depgraph.GetReducedParents(tx_idx)); assert(tx_data.children == m_depgraph.GetReducedChildren(tx_idx)); + // Verify active_children is a subset of children. + assert(tx_data.active_children.IsSubsetOf(tx_data.children)); + // Verify each active child's dep_top_idx points to a valid non-chunk set. + for (auto child_idx : tx_data.active_children) { + assert(tx_data.dep_top_idx[child_idx] < m_set_info.size()); + assert(!m_chunk_idxs[tx_data.dep_top_idx[child_idx]]); + } } // From 1daa600c1ca82d16dd1661292949e56b4c4c6d94 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 15 Oct 2025 21:35:58 -0400 Subject: [PATCH 17/20] clusterlin: track suboptimal chunks (optimization) This avoids adding them a second time to m_suboptimal_chunks when they happen to already be there. --- src/cluster_linearize.h | 37 ++++++++++++++++++++++++++----- src/test/util/cluster_linearize.h | 10 ++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index eace34ea392..164ee0df871 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -682,6 +682,10 @@ private: /** The set of all chunk SetIdx's. This excludes the SetIdxs that refer to active * dependencies' tops. */ SetType m_chunk_idxs; + /** The set of all SetIdx's that appear in m_suboptimal_chunks. Note that they do not need to + * be chunks: some of these sets may have been converted to a dependency's top set since being + * added to m_suboptimal_chunks. */ + SetType m_suboptimal_idxs; /** Information about each transaction (and chunks). Keeps the "holes" from DepGraph during * construction. Indexed by TxIdx. */ std::vector m_tx_data; @@ -979,8 +983,11 @@ private: if (merged_chunk_idx == INVALID_SET_IDX) break; chunk_idx = merged_chunk_idx; } - // Add the chunk to the queue of improvable chunks. - m_suboptimal_chunks.push_back(chunk_idx); + // Add the chunk to the queue of improvable chunks, if it wasn't already there. + if (!m_suboptimal_idxs[chunk_idx]) { + m_suboptimal_idxs.Set(chunk_idx); + m_suboptimal_chunks.push_back(chunk_idx); + } } /** Split a chunk, and then merge the resulting two chunks to make the graph topological @@ -1007,7 +1014,10 @@ private: // on the child chunk, so child_chunk_idx is the "top" and parent_chunk_idx is the // "bottom" for MergeChunks. auto merged_chunk_idx = MergeChunks(child_chunk_idx, parent_chunk_idx); - m_suboptimal_chunks.push_back(merged_chunk_idx); + if (!m_suboptimal_idxs[merged_chunk_idx]) { + m_suboptimal_idxs.Set(merged_chunk_idx); + m_suboptimal_chunks.push_back(merged_chunk_idx); + } } else { // Merge the top chunk with lower-feerate chunks it depends on. MergeSequence(parent_chunk_idx); @@ -1022,6 +1032,8 @@ private: while (!m_suboptimal_chunks.empty()) { // Pop an entry from the potentially-suboptimal chunk queue. SetIdx chunk_idx = m_suboptimal_chunks.front(); + Assume(m_suboptimal_idxs[chunk_idx]); + m_suboptimal_idxs.Reset(chunk_idx); m_suboptimal_chunks.pop_front(); if (m_chunk_idxs[chunk_idx]) return chunk_idx; // If what was popped is not currently a chunk, continue. This may @@ -1117,6 +1129,7 @@ public: void MakeTopological() noexcept { Assume(m_suboptimal_chunks.empty()); + m_suboptimal_idxs = m_chunk_idxs; for (auto chunk_idx : m_chunk_idxs) { m_suboptimal_chunks.emplace_back(chunk_idx); // Randomize the initial order of suboptimal chunks in the queue. @@ -1129,6 +1142,8 @@ public: // Pop an entry from the potentially-suboptimal chunk queue. SetIdx chunk_idx = m_suboptimal_chunks.front(); m_suboptimal_chunks.pop_front(); + Assume(m_suboptimal_idxs[chunk_idx]); + m_suboptimal_idxs.Reset(chunk_idx); // If what was popped is not currently a chunk, continue. This may // happen when it was merged with something else since being added. if (!m_chunk_idxs[chunk_idx]) continue; @@ -1138,14 +1153,20 @@ public: // Attempt to merge the chunk upwards. auto result_up = MergeStep(chunk_idx); if (result_up != INVALID_SET_IDX) { - m_suboptimal_chunks.push_back(result_up); + if (!m_suboptimal_idxs[result_up]) { + m_suboptimal_idxs.Set(result_up); + m_suboptimal_chunks.push_back(result_up); + } break; } } else { // Attempt to merge the chunk downwards. auto result_down = MergeStep(chunk_idx); if (result_down != INVALID_SET_IDX) { - m_suboptimal_chunks.push_back(result_down); + if (!m_suboptimal_idxs[result_down]) { + m_suboptimal_idxs.Set(result_down); + m_suboptimal_chunks.push_back(result_down); + } break; } } @@ -1158,6 +1179,7 @@ public: { Assume(m_suboptimal_chunks.empty()); // Mark chunks suboptimal. + m_suboptimal_idxs = m_chunk_idxs; for (auto chunk_idx : m_chunk_idxs) { m_suboptimal_chunks.push_back(chunk_idx); // Randomize the initial order of suboptimal chunks in the queue. @@ -1603,10 +1625,13 @@ public: // // Verify m_suboptimal_chunks. // + SetType suboptimal_idxs; for (size_t i = 0; i < m_suboptimal_chunks.size(); ++i) { auto chunk_idx = m_suboptimal_chunks[i]; - assert(chunk_idx < m_set_info.size()); + assert(!suboptimal_idxs[chunk_idx]); + suboptimal_idxs.Set(chunk_idx); } + assert(m_suboptimal_idxs == suboptimal_idxs); // // Verify m_nonminimal_chunks. diff --git a/src/test/util/cluster_linearize.h b/src/test/util/cluster_linearize.h index f14b00d5d7b..d56860686e3 100644 --- a/src/test/util/cluster_linearize.h +++ b/src/test/util/cluster_linearize.h @@ -403,13 +403,13 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count) static constexpr uint64_t ITERS[65] = { 0, 0, 4, 10, 34, 76, 156, 229, 380, - 432, 517, 678, 896, 1037, 1366, 1479, 1711, + 432, 517, 678, 896, 1037, 1366, 1464, 1792, 2060, 2542, 3068, 3116, 4029, 3467, 5324, 5512, 6481, 7161, 7441, 8183, 8843, 9353, 11104, 11269, - 12354, 11871, 13367, 14259, 14229, 12397, 13581, 17774, - 18737, 16581, 23217, 24044, 29597, 28879, 34069, 34162, - 36028, 26227, 34471, 37212, 40814, 29554, 40305, 34019, - 36582, 55659, 39994, 41277, 42365, 52822, 60151, 67035 + 11791, 11871, 13367, 14259, 14229, 12397, 13581, 18152, + 18737, 16581, 23217, 24044, 29597, 29030, 34069, 34594, + 33630, 26227, 34471, 38815, 40814, 31182, 40305, 34019, + 36582, 55659, 39994, 41277, 42365, 52822, 59733, 67035 }; assert(cluster_count < std::size(ITERS)); // Multiply the table number by two, to account for the fact that they are not absolutes. From b684f954bbfcd9a18c05110b1124276554ba072e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 15 Oct 2025 17:59:09 -0400 Subject: [PATCH 18/20] clusterlin: unidirectional MakeTopological initially (optimization) It suffices to initially only attempt one direction of merges in MakeTopological(), and only try both directions on chunks that are the result of other merges. --- src/cluster_linearize.h | 16 ++++++++++++++++ src/test/util/cluster_linearize.h | 14 +++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 164ee0df871..7e20f802339 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -1129,6 +1129,16 @@ public: void MakeTopological() noexcept { Assume(m_suboptimal_chunks.empty()); + /** What direction to initially merge chunks in; one of the two directions is enough. This + * is sufficient because if a non-topological inactive dependency exists between two + * chunks, at least one of the two chunks will eventually be processed in a direction that + * discovers it - either the lower chunk tries upward, or the upper chunk tries downward. + * Chunks that are the result of the merging are always tried in both directions. */ + unsigned init_dir = m_rng.randbool(); + /** Which chunks are the result of merging, and thus need merge attempts in both + * directions. */ + SetType merged_chunks; + // Mark chunks as suboptimal. m_suboptimal_idxs = m_chunk_idxs; for (auto chunk_idx : m_chunk_idxs) { m_suboptimal_chunks.emplace_back(chunk_idx); @@ -1147,9 +1157,12 @@ public: // If what was popped is not currently a chunk, continue. This may // happen when it was merged with something else since being added. if (!m_chunk_idxs[chunk_idx]) continue; + /** What direction(s) to attempt merging in. 1=up, 2=down, 3=both. */ + unsigned direction = merged_chunks[chunk_idx] ? 3 : init_dir + 1; int flip = m_rng.randbool(); for (int i = 0; i < 2; ++i) { if (i ^ flip) { + if (!(direction & 1)) continue; // Attempt to merge the chunk upwards. auto result_up = MergeStep(chunk_idx); if (result_up != INVALID_SET_IDX) { @@ -1157,9 +1170,11 @@ public: m_suboptimal_idxs.Set(result_up); m_suboptimal_chunks.push_back(result_up); } + merged_chunks.Set(result_up); break; } } else { + if (!(direction & 2)) continue; // Attempt to merge the chunk downwards. auto result_down = MergeStep(chunk_idx); if (result_down != INVALID_SET_IDX) { @@ -1167,6 +1182,7 @@ public: m_suboptimal_idxs.Set(result_down); m_suboptimal_chunks.push_back(result_down); } + merged_chunks.Set(result_down); break; } } diff --git a/src/test/util/cluster_linearize.h b/src/test/util/cluster_linearize.h index d56860686e3..3814c3fc453 100644 --- a/src/test/util/cluster_linearize.h +++ b/src/test/util/cluster_linearize.h @@ -403,13 +403,13 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count) static constexpr uint64_t ITERS[65] = { 0, 0, 4, 10, 34, 76, 156, 229, 380, - 432, 517, 678, 896, 1037, 1366, 1464, 1792, - 2060, 2542, 3068, 3116, 4029, 3467, 5324, 5512, - 6481, 7161, 7441, 8183, 8843, 9353, 11104, 11269, - 11791, 11871, 13367, 14259, 14229, 12397, 13581, 18152, - 18737, 16581, 23217, 24044, 29597, 29030, 34069, 34594, - 33630, 26227, 34471, 38815, 40814, 31182, 40305, 34019, - 36582, 55659, 39994, 41277, 42365, 52822, 59733, 67035 + 441, 517, 678, 933, 1037, 1366, 1464, 1711, + 2111, 2542, 3068, 3116, 4029, 3467, 5324, 5402, + 6481, 7161, 7441, 8183, 8843, 9353, 11104, 11455, + 11791, 12570, 13480, 14259, 14525, 12426, 14477, 20201, + 18737, 16581, 23622, 28486, 30652, 33021, 32942, 32745, + 34046, 26227, 34662, 38019, 40814, 31113, 41448, 33968, + 35024, 59207, 42872, 41277, 42365, 51833, 63410, 67035 }; assert(cluster_count < std::size(ITERS)); // Multiply the table number by two, to account for the fact that they are not absolutes. From d90f98ab4aaaa6d524d8a2ab9fb3a8ba162ebb00 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 13:54:25 -0500 Subject: [PATCH 19/20] clusterlin: inline UpdateChunk into (De)Activate (optimization) The two calls to UpdateChunk, in Activate and Deactive each, are subtly different: the top one needs to update the chunk_idx of iterated transactions, while the bottom one leaves it unchanged. To exploit this difference, inline the four function calls, getting rid of UpdateChunks. This is also a preparation for a future improvement that inlines the recomputation of reachable sets in the same loop in Deactivate. --- src/cluster_linearize.h | 80 ++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 7e20f802339..a53edf77282 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -723,36 +723,6 @@ private: return TxIdx(-1); } - /** Update a chunk: - * - All transactions have their chunk index set to `chunk_idx`. - * - All dependencies which have `query` in their top set get `dep_change` added to it - * (if `!Subtract`) or removed from it (if `Subtract`). - */ - template - void UpdateChunk(const SetType& tx_idxs, TxIdx query, SetIdx chunk_idx, const SetInfo& dep_change) noexcept - { - // Iterate over all the chunk's transactions. - for (auto tx_idx : tx_idxs) { - auto& tx_data = m_tx_data[tx_idx]; - // Update the chunk index for this transaction. - tx_data.chunk_idx = chunk_idx; - // Iterate over all active dependencies with tx_idx as parent. Combined with the outer - // loop this iterates over all internal active dependencies of the chunk. - for (auto child_idx : tx_data.active_children) { - auto& top_set_info = m_set_info[tx_data.dep_top_idx[child_idx]]; - // If this dependency's top set contains query, update it to add/remove - // dep_change. - if (top_set_info.transactions[query]) { - if constexpr (Subtract) { - top_set_info -= dep_change; - } else { - top_set_info |= dep_change; - } - } - } - } - } - /** Find the set of out-of-chunk transactions reachable from tx_idxs, both in upwards and * downwards direction. */ std::pair GetReachable(const SetType& tx_idxs) const noexcept @@ -801,17 +771,26 @@ private: // dependency being activated (E->C here) in its top set, will have the opposite part added // to it. This is true for B->A and F->E, but not for C->A and F->D. // - // Let UpdateChunk traverse the old parent chunk top_info (ABC in example), and add - // bottom_info (DEF) to every dependency's top set which has the parent (C) in it. At the - // same time, change the chunk_idx for each to be child_chunk_idx, which becomes the set for - // the merged chunk. - UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/parent_idx, - /*chunk_idx=*/child_chunk_idx, /*dep_change=*/bottom_info); - // Let UpdateChunk traverse the old child chunk bottom_info (DEF in example), and add - // top_info (ABC) to every dependency's top set which has the child (E) in it. The chunk - // these are part of isn't being changed here (already child_chunk_idx for each). - UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/child_idx, - /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); + // Traverse the old parent chunk top_info (ABC in example), and add bottom_info (DEF) to + // every dependency's top set which has the parent (C) in it. At the same time, change the + // chunk_idx for each to be child_chunk_idx, which becomes the set for the merged chunk. + for (auto tx_idx : top_info.transactions) { + auto& tx_data = m_tx_data[tx_idx]; + tx_data.chunk_idx = child_chunk_idx; + for (auto dep_child_idx : tx_data.active_children) { + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; + if (dep_top_info.transactions[parent_idx]) dep_top_info |= bottom_info; + } + } + // Traverse the old child chunk bottom_info (DEF in example), and add top_info (ABC) to + // every dependency's top set which has the child (E) in it. + for (auto tx_idx : bottom_info.transactions) { + auto& tx_data = m_tx_data[tx_idx]; + for (auto dep_child_idx : tx_data.active_children) { + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; + if (dep_top_info.transactions[child_idx]) dep_top_info |= top_info; + } + } // Merge top_info into bottom_info, which becomes the merged chunk. bottom_info |= top_info; m_cost += bottom_info.transactions.Count(); @@ -856,10 +835,21 @@ private: bottom_info -= top_info; // See the comment above in Activate(). We perform the opposite operations here, removing // instead of adding. - UpdateChunk(/*tx_idxs=*/top_info.transactions, /*query=*/parent_idx, - /*chunk_idx=*/parent_chunk_idx, /*dep_change=*/bottom_info); - UpdateChunk(/*tx_idxs=*/bottom_info.transactions, /*query=*/child_idx, - /*chunk_idx=*/child_chunk_idx, /*dep_change=*/top_info); + for (auto tx_idx : top_info.transactions) { + auto& tx_data = m_tx_data[tx_idx]; + tx_data.chunk_idx = parent_chunk_idx; + for (auto dep_child_idx : tx_data.active_children) { + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; + if (dep_top_info.transactions[parent_idx]) dep_top_info -= bottom_info; + } + } + for (auto tx_idx : bottom_info.transactions) { + auto& tx_data = m_tx_data[tx_idx]; + for (auto dep_child_idx : tx_data.active_children) { + auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; + if (dep_top_info.transactions[child_idx]) dep_top_info -= top_info; + } + } // Compute the new sets of reachable transactions for each new chunk. m_reachable[child_chunk_idx] = GetReachable(bottom_info.transactions); m_reachable[parent_chunk_idx] = GetReachable(top_info.transactions); From c2fcf250697325636218225d578c3844ab9ca633 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 26 Dec 2025 13:58:45 -0500 Subject: [PATCH 20/20] clusterlin: inline GetReachable into Deactivate (optimization) Avoid two full iterations over all of a chunks' transactions to recompute the reachable sets, by inlining them into the dependency-updating loops. Note that there is no need to do the same for Activate, because the reachable sets after merging can be computed directly from the input chunks' reachable sets. Deactivate needs to recompute them, however. --- src/cluster_linearize.h | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index a53edf77282..1fe737a7e97 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -724,7 +724,8 @@ private: } /** Find the set of out-of-chunk transactions reachable from tx_idxs, both in upwards and - * downwards direction. */ + * downwards direction. Only used by SanityCheck to verify the precomputed reachable sets in + * m_reachable that are maintained by Activate/Deactivate. */ std::pair GetReachable(const SetType& tx_idxs) const noexcept { SetType parents, children; @@ -794,9 +795,8 @@ private: // Merge top_info into bottom_info, which becomes the merged chunk. bottom_info |= top_info; m_cost += bottom_info.transactions.Count(); - // Compute merged sets of reachable transactions from the new chunk. There is no need to - // call GetReachable here, because they can be computed directly from the input chunks' - // reachable sets. + // Compute merged sets of reachable transactions from the new chunk, based on the input + // chunks' reachable sets. m_reachable[child_chunk_idx].first |= m_reachable[parent_chunk_idx].first; m_reachable[child_chunk_idx].second |= m_reachable[parent_chunk_idx].second; m_reachable[child_chunk_idx].first -= bottom_info.transactions; @@ -834,25 +834,34 @@ private: // Subtract the top_info from the bottom_info, as it will become the child chunk. bottom_info -= top_info; // See the comment above in Activate(). We perform the opposite operations here, removing - // instead of adding. + // instead of adding. Simultaneously, aggregate the top/bottom's union of parents/children. + SetType top_parents, top_children; for (auto tx_idx : top_info.transactions) { auto& tx_data = m_tx_data[tx_idx]; tx_data.chunk_idx = parent_chunk_idx; + top_parents |= tx_data.parents; + top_children |= tx_data.children; for (auto dep_child_idx : tx_data.active_children) { auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; if (dep_top_info.transactions[parent_idx]) dep_top_info -= bottom_info; } } + SetType bottom_parents, bottom_children; for (auto tx_idx : bottom_info.transactions) { auto& tx_data = m_tx_data[tx_idx]; + bottom_parents |= tx_data.parents; + bottom_children |= tx_data.children; for (auto dep_child_idx : tx_data.active_children) { auto& dep_top_info = m_set_info[tx_data.dep_top_idx[dep_child_idx]]; if (dep_top_info.transactions[child_idx]) dep_top_info -= top_info; } } - // Compute the new sets of reachable transactions for each new chunk. - m_reachable[child_chunk_idx] = GetReachable(bottom_info.transactions); - m_reachable[parent_chunk_idx] = GetReachable(top_info.transactions); + // Compute the new sets of reachable transactions for each new chunk, based on the + // top/bottom parents and children computed above. + m_reachable[parent_chunk_idx].first = top_parents - top_info.transactions; + m_reachable[parent_chunk_idx].second = top_children - top_info.transactions; + m_reachable[child_chunk_idx].first = bottom_parents - bottom_info.transactions; + m_reachable[child_chunk_idx].second = bottom_children - bottom_info.transactions; // Return the two new set idxs. return {parent_chunk_idx, child_chunk_idx}; }