From 023cd5a5469ad61205bf7bb1135895f2b4a20ea9 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 5 Sep 2025 16:06:35 -0400 Subject: [PATCH] txgraph: add SingletonClusterImpl (mem optimization) This adds a specialized Cluster implementation for singleton clusters, saving a significant amount of memory by avoiding the need for m_depgraph, m_mapping, and m_linearization, and their overheads. --- src/txgraph.cpp | 344 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 341 insertions(+), 3 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index f66b470779b..015bdd34582 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -183,7 +183,7 @@ public: /** Mark all the Entry objects belonging to this staging Cluster as missing. The Cluster must be * deleted immediately after. */ virtual void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept = 0; - /** Remove all transactions from a Cluster. */ + /** Remove all transactions from a (non-empty) Cluster. */ virtual void Clear(TxGraphImpl& graph, int level) noexcept = 0; /** Change a Cluster's level from 1 (staging) to 0 (main). */ virtual void MoveToMain(TxGraphImpl& graph) noexcept = 0; @@ -251,7 +251,7 @@ class GenericClusterImpl final : public Cluster public: /** The smallest number of transactions this Cluster implementation is intended for. */ - static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{1}; + static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{2}; /** The largest number of transactions this Cluster implementation supports. */ static constexpr DepGraphIndex MAX_TX_COUNT{SetType::Size()}; @@ -293,6 +293,61 @@ public: void SanityCheck(const TxGraphImpl& graph, int level) const final; }; +/** An implementation of Cluster that only supports 1 transaction. */ +class SingletonClusterImpl final : public Cluster +{ + friend class TxGraphImpl; + + /** The feerate of the (singular) transaction in this Cluster. */ + FeePerWeight m_feerate; + /** Constant to indicate that this Cluster is empty. */ + static constexpr auto NO_GRAPH_INDEX = GraphIndex(-1); + /** The GraphIndex of the transaction. NO_GRAPH_INDEX if this Cluster is empty. */ + GraphIndex m_graph_index = NO_GRAPH_INDEX; + +public: + /** The smallest number of transactions this Cluster implementation is intended for. */ + static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{1}; + /** The largest number of transactions this Cluster implementation supports. */ + static constexpr DepGraphIndex MAX_TX_COUNT{1}; + + SingletonClusterImpl() noexcept = delete; + /** Construct an empty SingletonClusterImpl. */ + explicit SingletonClusterImpl(uint64_t sequence) noexcept : Cluster(sequence) {} + + size_t TotalMemoryUsage() const noexcept final; + constexpr DepGraphIndex GetMinIntendedTxCount() const noexcept final { return MIN_INTENDED_TX_COUNT; } + constexpr DepGraphIndex GetMaxTxCount() const noexcept final { return MAX_TX_COUNT; } + LinearizationIndex GetTxCount() const noexcept final { return m_graph_index != NO_GRAPH_INDEX; } + DepGraphIndex GetDepGraphIndexRange() const noexcept final { return GetTxCount(); } + uint64_t GetTotalTxSize() const noexcept final { return GetTxCount() ? m_feerate.size : 0; } + GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept final { Assume(index == 0); Assume(GetTxCount()); return m_graph_index; } + DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept final; + void AddDependencies(SetType parents, DepGraphIndex child) noexcept final; + void ExtractTransactions(const std::function& visit_fn) noexcept final; + int GetLevel(const TxGraphImpl& graph) const noexcept final; + void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept final { Assume(cluster_idx == 0); m_graph_index = graph_idx; } + void Updated(TxGraphImpl& graph, int level) noexcept final; + Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept final; + void GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept final; + void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept final; + void Clear(TxGraphImpl& graph, int level) noexcept final; + void MoveToMain(TxGraphImpl& graph) noexcept final; + void Compact() noexcept final; + void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept final; + [[nodiscard]] bool Split(TxGraphImpl& graph, int level) noexcept final; + void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept final; + void ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept final; + std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept final; + void AppendChunkFeerates(std::vector& ret) const noexcept final; + uint64_t AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept final; + void GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + void GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + bool GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept final; + FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept final; + void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept final; + void SanityCheck(const TxGraphImpl& graph, int level) const final; +}; /** The transaction graph, including staged changes. * @@ -320,6 +375,7 @@ public: class TxGraphImpl final : public TxGraph { friend class Cluster; + friend class SingletonClusterImpl; friend class GenericClusterImpl; friend class BlockBuilderImpl; private: @@ -594,10 +650,18 @@ public: { return std::make_unique(m_next_sequence_counter++); } + /** Create an empty SingletonClusterImpl object. */ + std::unique_ptr CreateEmptySingletonCluster() noexcept + { + return std::make_unique(m_next_sequence_counter++); + } /** Create an empty Cluster of the appropriate implementation for the specified (maximum) tx * count. */ std::unique_ptr CreateEmptyCluster(DepGraphIndex tx_count) noexcept { + if (tx_count >= SingletonClusterImpl::MIN_INTENDED_TX_COUNT && tx_count <= SingletonClusterImpl::MAX_TX_COUNT) { + return CreateEmptySingletonCluster(); + } if (tx_count >= GenericClusterImpl::MIN_INTENDED_TX_COUNT && tx_count <= GenericClusterImpl::MAX_TX_COUNT) { return CreateEmptyGenericCluster(); } @@ -805,6 +869,14 @@ size_t GenericClusterImpl::TotalMemoryUsage() const noexcept sizeof(std::unique_ptr); } +size_t SingletonClusterImpl::TotalMemoryUsage() const noexcept +{ + return // Memory usage of the allocated SingletonClusterImpl itself. + memusage::MallocUsage(sizeof(SingletonClusterImpl)) + + // Memory usage of the ClusterSet::m_clusters entry. + sizeof(std::unique_ptr); +} + uint64_t GenericClusterImpl::GetTotalTxSize() const noexcept { uint64_t ret{0}; @@ -823,11 +895,26 @@ DepGraphIndex GenericClusterImpl::AppendTransaction(GraphIndex graph_idx, FeePer return ret; } +DepGraphIndex SingletonClusterImpl::AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept +{ + Assume(!GetTxCount()); + m_graph_index = graph_idx; + m_feerate = feerate; + return 0; +} + void GenericClusterImpl::AddDependencies(SetType parents, DepGraphIndex child) noexcept { m_depgraph.AddDependencies(parents, child); } +void SingletonClusterImpl::AddDependencies(SetType parents, DepGraphIndex child) noexcept +{ + // Singletons cannot have any dependencies. + Assume(child == 0); + Assume(parents == SetType{} || parents == SetType::Fill(0)); +} + void GenericClusterImpl::ExtractTransactions(const std::function& visit_fn) noexcept { for (auto pos : m_linearization) { @@ -839,6 +926,14 @@ void GenericClusterImpl::ExtractTransactions(const std::function& visit_fn) noexcept +{ + if (GetTxCount()) { + visit_fn(0, m_graph_index, m_feerate, SetType{}); + m_graph_index = NO_GRAPH_INDEX; + } +} + int GenericClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept { // GetLevel() does not work for empty Clusters. @@ -856,6 +951,23 @@ int GenericClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept return -1; } +int SingletonClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept +{ + // GetLevel() does not work for empty Clusters. + if (!Assume(GetTxCount())) return -1; + + // Get the Entry in this Cluster. + const auto& entry = graph.m_entries[m_graph_index]; + // See if there is a level whose Locator matches this Cluster, if so return that level. + for (int level = 0; level < MAX_LEVELS; ++level) { + if (entry.m_locator[level].cluster == this) return level; + } + // Given that we started with an Entry that occurs in this Cluster, one of its Locators must + // point back to it. + assert(false); + return -1; +} + void TxGraphImpl::ClearLocator(int level, GraphIndex idx, bool oversized_tx) noexcept { auto& entry = m_entries[idx]; @@ -931,6 +1043,27 @@ void GenericClusterImpl::Updated(TxGraphImpl& graph, int level) noexcept } } +void SingletonClusterImpl::Updated(TxGraphImpl& graph, int level) noexcept +{ + // Don't do anything if this is empty. + if (GetTxCount() == 0) return; + + auto& entry = graph.m_entries[m_graph_index]; + // Discard any potential ChunkData prior to modifying the Cluster (as that could + // invalidate its ordering). + if (level == 0) graph.ClearChunkData(entry); + entry.m_locator[level].SetPresent(this, 0); + // If this is for the main graph (level = 0), compute its chunking and store its information in + // the Entry's m_main_lin_index and m_main_chunk_feerate. + if (level == 0 && IsAcceptable()) { + entry.m_main_lin_index = 0; + entry.m_main_chunk_feerate = m_feerate; + // Always use the special LinearizationIndex(-1), indicating singleton chunk at end of + // Cluster, here. + graph.CreateChunkData(m_graph_index, LinearizationIndex(-1)); + } +} + void GenericClusterImpl::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept { for (auto i : m_linearization) { @@ -943,6 +1076,19 @@ void GenericClusterImpl::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept +{ + // Empty clusters have no conflicts. + if (GetTxCount() == 0) return; + + auto& entry = graph.m_entries[m_graph_index]; + // If the transaction in this Cluster also exists in a lower-level Cluster, then that Cluster + // conflicts. + if (entry.m_locator[0].IsPresent()) { + out.push_back(entry.m_locator[0].cluster); + } +} + std::vector TxGraphImpl::GetConflicts() const noexcept { Assume(GetTopLevel() == 1); @@ -973,7 +1119,7 @@ Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept // Construct an empty Cluster. auto ret = graph.CreateEmptyGenericCluster(); auto ptr = ret.get(); - // Copy depgraph, mapping, and linearization/ + // Copy depgraph, mapping, and linearization. ptr->m_depgraph = m_depgraph; ptr->m_mapping = m_mapping; ptr->m_linearization = m_linearization; @@ -986,6 +1132,23 @@ Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept return ptr; } +Cluster* SingletonClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept +{ + // Construct an empty Cluster. + auto ret = graph.CreateEmptySingletonCluster(); + auto ptr = ret.get(); + // Copy data. + ptr->m_graph_index = m_graph_index; + ptr->m_feerate = m_feerate; + // Insert the new Cluster into the graph. + graph.InsertCluster(/*level=*/1, std::move(ret), m_quality); + // Update its Locators. + ptr->Updated(graph, /*level=*/1); + // Update memory usage. + graph.GetClusterSet(/*level=*/1).m_cluster_usage += ptr->TotalMemoryUsage(); + return ptr; +} + void GenericClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept { // Iterate over the prefix of to_remove that applies to this cluster. @@ -1048,8 +1211,28 @@ void GenericClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span< Updated(graph, level); } +void SingletonClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept +{ + // We can only remove the one transaction this Cluster has. + Assume(!to_remove.empty()); + Assume(GetTxCount()); + Assume(to_remove.front() == m_graph_index); + // Pop all copies of m_graph_index from the front of to_remove (at least one, but there may be + // multiple). + do { + to_remove = to_remove.subspan(1); + } while (!to_remove.empty() && to_remove.front() == m_graph_index); + // Clear this cluster. + graph.ClearLocator(level, m_graph_index, m_quality == QualityLevel::OVERSIZED_SINGLETON); + m_graph_index = NO_GRAPH_INDEX; + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::NEEDS_SPLIT); + // No need to account for m_cluster_usage changes here, as SingletonClusterImpl has constant + // memory usage. +} + void GenericClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept { + Assume(GetTxCount()); graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); for (auto i : m_linearization) { graph.ClearLocator(level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON); @@ -1059,6 +1242,14 @@ void GenericClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept m_mapping.clear(); } +void SingletonClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept +{ + Assume(GetTxCount()); + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); + graph.ClearLocator(level, m_graph_index, m_quality == QualityLevel::OVERSIZED_SINGLETON); + m_graph_index = NO_GRAPH_INDEX; +} + void GenericClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept { for (auto i : m_linearization) { @@ -1076,6 +1267,20 @@ void GenericClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept Updated(graph, /*level=*/0); } +void SingletonClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept +{ + if (GetTxCount()) { + auto& entry = graph.m_entries[m_graph_index]; + entry.m_locator[1].SetMissing(); + } + auto quality = m_quality; + graph.GetClusterSet(/*level=*/1).m_cluster_usage -= TotalMemoryUsage(); + auto cluster = graph.ExtractCluster(/*level=*/1, quality, m_setindex); + graph.InsertCluster(/*level=*/0, std::move(cluster), quality); + graph.GetClusterSet(/*level=*/0).m_cluster_usage += TotalMemoryUsage(); + Updated(graph, /*level=*/0); +} + void GenericClusterImpl::Compact() noexcept { m_linearization.shrink_to_fit(); @@ -1083,6 +1288,11 @@ void GenericClusterImpl::Compact() noexcept m_depgraph.Compact(); } +void SingletonClusterImpl::Compact() noexcept +{ + // Nothing to compact; SingletonClusterImpl is constant size. +} + void GenericClusterImpl::AppendChunkFeerates(std::vector& ret) const noexcept { auto chunk_feerates = ChunkLinearization(m_depgraph, m_linearization); @@ -1090,6 +1300,13 @@ void GenericClusterImpl::AppendChunkFeerates(std::vector& ret) const no ret.insert(ret.end(), chunk_feerates.begin(), chunk_feerates.end()); } +void SingletonClusterImpl::AppendChunkFeerates(std::vector& ret) const noexcept +{ + if (GetTxCount()) { + ret.push_back(m_feerate); + } +} + uint64_t GenericClusterImpl::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept { const LinearizationChunking linchunking(m_depgraph, m_linearization); @@ -1121,6 +1338,16 @@ uint64_t GenericClusterImpl::AppendTrimData(std::vector& ret, std::v return size; } +uint64_t SingletonClusterImpl::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept +{ + if (!GetTxCount()) return 0; + auto& entry = ret.emplace_back(); + entry.m_chunk_feerate = m_feerate; + entry.m_index = m_graph_index; + entry.m_tx_size = m_feerate.size; + return m_feerate.size; +} + bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept { // This function can only be called when the Cluster needs splitting. @@ -1205,6 +1432,21 @@ bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept return true; } +bool SingletonClusterImpl::Split(TxGraphImpl& graph, int level) noexcept +{ + Assume(NeedsSplitting()); + if (GetTxCount() == 0) { + // The cluster is now empty. + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); + return true; + } else { + // Nothing changed. + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::OPTIMAL); + Updated(graph, level); + return false; + } +} + void GenericClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept { /** Vector to store the positions in this Cluster for each position in other. */ @@ -1238,6 +1480,13 @@ void GenericClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other) no }); } +void SingletonClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other_abstract) noexcept +{ + // Nothing can be merged into a singleton; it should have been converted to GenericClusterImpl + // first. + Assume(false); +} + void GenericClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept { // This function is invoked by TxGraphImpl::ApplyDependencies after merging groups of Clusters @@ -1286,6 +1535,15 @@ void GenericClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::s Updated(graph, level); } +void SingletonClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept +{ + // Nothing can actually be applied. + for (auto& [par, chl] : to_apply) { + Assume(par == m_graph_index); + Assume(chl == m_graph_index); + } +} + TxGraphImpl::~TxGraphImpl() noexcept { // If Refs outlive the TxGraphImpl they refer to, unlink them, so that their destructor does not @@ -1861,6 +2119,14 @@ std::pair GenericClusterImpl::Relinearize(TxGraphImpl& graph, in return {cost, improved}; } +std::pair SingletonClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept +{ + // All singletons are optimal, oversized, or need splitting. Each of these precludes + // Relinearize from being called. + assert(false); + return {0, false}; +} + void TxGraphImpl::MakeAcceptable(Cluster& cluster, int level) noexcept { // Relinearize the Cluster if needed. @@ -1990,6 +2256,18 @@ void GenericClusterImpl::GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +{ + Assume(GetTxCount()); + while (!args.empty()) { + if (args.front().first != this) break; + args = args.subspan(1); + } + const auto& entry = graph.m_entries[m_graph_index]; + Assume(entry.m_ref != nullptr); + output.push_back(entry.m_ref); +} + void GenericClusterImpl::GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept { /** The union of all descendants to be returned. */ @@ -2009,6 +2287,12 @@ void GenericClusterImpl::GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +{ + // In a singleton cluster, the ancestors or descendants are always just the entire cluster. + GetAncestorRefs(graph, args, output); +} + bool GenericClusterImpl::GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept { // Translate the transactions in the Cluster (in linearization order, starting at start_pos in @@ -2023,11 +2307,29 @@ bool GenericClusterImpl::GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept +{ + Assume(!range.empty()); + Assume(GetTxCount()); + Assume(start_pos == 0); + const auto& entry = graph.m_entries[m_graph_index]; + Assume(entry.m_ref != nullptr); + range[0] = entry.m_ref; + return true; +} + FeePerWeight GenericClusterImpl::GetIndividualFeerate(DepGraphIndex idx) noexcept { return FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(idx)); } +FeePerWeight SingletonClusterImpl::GetIndividualFeerate(DepGraphIndex idx) noexcept +{ + Assume(GetTxCount()); + Assume(idx == 0); + return m_feerate; +} + void GenericClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept { // Mark all transactions of a Cluster missing, needed when aborting staging, so that the @@ -2039,6 +2341,14 @@ void GenericClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noex } } +void SingletonClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept +{ + if (GetTxCount()) { + auto& entry = graph.m_entries[m_graph_index]; + entry.m_locator[1].SetMissing(); + } +} + std::vector TxGraphImpl::GetAncestors(const Ref& arg, Level level_select) noexcept { // Return the empty vector if the Ref is empty. @@ -2347,6 +2657,14 @@ void GenericClusterImpl::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx Updated(graph, level); } +void SingletonClusterImpl::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept +{ + Assume(GetTxCount()); + Assume(idx == 0); + m_feerate.fee = fee; + Updated(graph, level); +} + void TxGraphImpl::SetTransactionFee(const Ref& ref, int64_t fee) noexcept { // Don't do anything if the passed Ref is empty. @@ -2494,6 +2812,26 @@ void GenericClusterImpl::SanityCheck(const TxGraphImpl& graph, int level) const assert(m_done == m_depgraph.Positions()); } +void SingletonClusterImpl::SanityCheck(const TxGraphImpl& graph, int level) const +{ + // All singletons are optimal, oversized, or need splitting. + Assume(IsOptimal() || IsOversized() || NeedsSplitting()); + if (GetTxCount()) { + const auto& entry = graph.m_entries[m_graph_index]; + // Check that the Entry has a locator pointing back to this Cluster & position within it. + assert(entry.m_locator[level].cluster == this); + assert(entry.m_locator[level].index == 0); + // For main-level entries, check linearization position and chunk feerate. + if (level == 0 && IsAcceptable()) { + assert(entry.m_main_lin_index == 0); + assert(entry.m_main_chunk_feerate == m_feerate); + assert(entry.m_main_chunkindex_iterator != graph.m_main_chunkindex.end()); + auto& chunk_data = *entry.m_main_chunkindex_iterator; + assert(chunk_data.m_chunk_count == LinearizationIndex(-1)); + } + } +} + void TxGraphImpl::SanityCheck() const { /** Which GraphIndexes ought to occur in m_unlinked, based on m_entries. */