77 Commits

Author SHA1 Message Date
Pieter Wuille
744d47fcee clusterlin: adopt trained cost model (feature)
See the comments for the SFLDefaultCostModel class for details on how
the numbers were obtained.
2026-02-24 12:05:17 -05:00
Pieter Wuille
4eefdfc5b7 clusterlin: rescale costs (preparation) 2026-02-24 10:45:49 -05:00
Pieter Wuille
ecc9a84f85 clusterlin: use 'cost' terminology instead of 'iters' (refactor) 2026-02-24 10:08:47 -05:00
Pieter Wuille
9e7129df29 clusterlin: introduce CostModel class (preparation)
This parametrizes the cost model for the SFL algorithm with another
class. Right now, the behavior of that class matches the naive cost
model so far, but it will be replaced with a more advanced on in a
future commit.

The reason for abstracting this out is that it makes benchmarking for
creating such cost models easy, by instantiating the cost model class
with one that tracks time.
2026-02-24 10:08:47 -05:00
Pieter Wuille
c2fcf25069 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
d90f98ab4a 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
b684f954bb 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
1daa600c1c clusterlin: track suboptimal chunks (optimization)
This avoids adding them a second time to m_suboptimal_chunks when they
happen to already be there.
2026-02-17 09:04:36 -05:00
Pieter Wuille
63b06d5523 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
ae16485aa9 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).
2026-02-17 09:04:36 -05:00
Pieter Wuille
3221f1a074 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
7194de3f7c 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
6f898dbb8b 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
dcf458ffb9 clusterlin: split up OptimizeStep (refactor) 2026-02-17 09:04:36 -05:00
Pieter Wuille
cbd684a471 clusterlin: abstract out functions from MergeStep (refactor)
This is a simple refactor to make the code more readable.
2026-02-17 09:04:36 -05:00
Pieter Wuille
b75574a653 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
73cbd15d45 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
7c6f63a8a9 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
20e2f3e96d 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-
2026-02-17 09:04:36 -05:00
Pieter Wuille
268fcb6a53 clusterlin: add more Assumes and sanity checks (tests) 2026-02-17 09:04:36 -05:00
Pieter Wuille
d69c9f56ea 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().
2026-02-17 09:04:36 -05:00
Pieter Wuille
f66fa69ce0 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
900e459778 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.
2026-02-17 09:04:36 -05:00
Pieter Wuille
666b37970f clusterlin: fix type to count dependencies 2026-02-17 09:04:36 -05:00
Pieter Wuille
39d0052cbf clusterlin: make optimal linearizations deterministic (feature)
This allows passing in a fallback order comparator to Linearize(), which
is used as final tiebreak when deciding the order of chunks and
transactions within a chunk, rather than a random tiebreak.

The order of transactions within a chunk becomes:
1. Topology (parents before children)
2. Individual transaction feerate (high to low)
3. Weight (small to large)
4. Fallback (low to high fallback order)

The order of chunks within a cluster becomes:
1. Topology (chunks after their dependencies)
2. Feerate (high to low)
3. Weight (small to large)
4. Max-fallback (chunk with lowest maximum-fallback-tx first)

For now, txgraph passes a naive comparator to Linearize(), which makes
the cluster order deterministic when treating the input transactions as
identified by the DepGraphIndex. However, since DepGraphIndexes are the
result of possibly-randomized operations inside txgraph, this doesn't
actually make txgraph's per-cluster ordering deterministic. That will be
changed in a later commit, by using a txid-based fallback instead.
2026-02-09 15:55:58 -05:00
Pieter Wuille
e0bc73ba92 clusterlin: sort tx in chunk by feerate and size (feature)
This changes the order of transactions within a chunk to be:
1. Topology (parents before children)
2. Individual transaction feerate (high to low)
3. Individual transaction weight (small to large)
4. Random tiebreak (will be changed in a future commit)

To do so, use a heap of topology-ready transactions within
GetLinearization(), sorted by (2), (3), and (4).

This is analogous to the order of chunks within a cluster, which is
unchanged:
1. Topology (chunks after chunks they depend on)
2. Chunk feerate (high to low)
3. Chunk weight (small to large)
4. Random tiebreak (will be changed in a future commit)
2026-02-09 15:55:58 -05:00
Pieter Wuille
da56ef239b clusterlin: minimize chunks (feature)
After the normal optimization process finishes, and finds an optimal
spanning forest, run a second process (while computation budget remains)
to split chunks into minimal equal-feerate chunks.
2026-01-12 17:38:30 -05:00
Pieter Wuille
1808b5aaf7 clusterlin: remove unused FixLinearization (cleanup) 2026-01-05 11:48:34 -05:00
Pieter Wuille
01ffcf464a clusterlin: support fixing linearizations (feature)
This also updates FixLinearization to just be a thin wrapper around Linearize.
In a future commit, FixLinearization will be removed entirely.
2026-01-05 11:48:16 -05:00
bensig
08ed802bab doc: fix double-word typos in comments 2025-12-30 12:12:26 -08:00
Pieter Wuille
75bdb925f4 clusterlin: drop support for improvable chunking (simplification)
With MergeLinearizations() gone and the LIMO-based Linearize() replaced by SFL, we do not
need a class (LinearizationChunking) that can maintain an incrementally-improving chunk
set anymore.

Replace it with a function (ChunkLinearizationInfo) that just computes the chunks as
SetInfos once, and returns them as a vector. This simplifies several call sites too.
2025-12-18 16:01:31 -05:00
Pieter Wuille
91399a7912 clusterlin: remove unused MergeLinearizations (cleanup)
This ended up never being used in txgraph.
2025-12-18 16:01:31 -05:00
Pieter Wuille
5ce2800745 clusterlin: randomize equal-feerate parts of linearization (privacy)
This places equal-feerate chunks (with no dependencies between them) in random
order in the linearization output, hiding information about DepGraph insertion
order from the output. Likewise, it randomizes the order of transactions within
chunks for the same reason.
2025-12-18 16:01:31 -05:00
Pieter Wuille
13aad26b78 clusterlin: randomize various decisions in SFL (feature)
This introduces a local RNG inside the SFL state, which is used to randomize
various decisions inside the algorithm, in order to make it hard to create
pathological clusters which predictably have bad performance.

The decisions being randomized are:
* When deciding what chunk to attempt to split, the queue order is
  randomized.
* When deciding which dependency to split on, a uniformly random one is
  chosen among those with higher top feerate than bottom feerate within
  the chosen chunk.
* When deciding which chunks to merge, a uniformly random one among those
  with the higher feerate difference is picked.
* When merging two chunks, a uniformly random dependency between them is
  now activated.
* When making the state topological, the queue of chunks to process is
  randomized.
2025-12-18 16:01:31 -05:00
Pieter Wuille
ddbfa4dfac clusterlin: keep FIFO queue of improvable chunks (preparation)
This introduces a queue of chunks that still need processing, in both
MakeTopological() and OptimizationStep(). This is simultaneously:
* A preparation for introducing randomization, by allowing permuting the
  queue.
* An improvement to the fairness of suboptimal solutions, by distributing
  the work more fairly over chunks.
* An optimization, by avoiding retrying chunks over and over again which
  are already known to be optimal.
2025-12-18 16:01:31 -05:00
Pieter Wuille
3efc94d656 clusterlin: replace cluster linearization with SFL (feature)
This replaces the existing LIMO linearization algorithm (which internally uses
ancestor set finding and candidate set finding) with the much more performant
spanning-forest linearization algorithm.

This removes the old candidate-set search algorithm, and several of its tests,
benchmarks, and needed utility code.

The worst case time per cost is similar to the previous algorithm, so
ACCEPTABLE_ITERS is unchanged.
2025-12-18 16:01:31 -05:00
Pieter Wuille
6a8fa821b8 clusterlin: add support for loading existing linearization (feature) 2025-12-18 16:01:22 -05:00
Pieter Wuille
c461259fb6 clusterlin: add class implementing SFL state (preparation)
This adds a data structure representing the optimization state for the spanning-forest
linearization algorithm (SFL), plus a fuzz test for its correctness.

This is preparation for switching over Linearize() to use this algorithm.

See https://delvingbitcoin.org/t/spanning-forest-cluster-linearization/1419 for
a description of the algorithm.
2025-12-18 15:49:01 -05:00
Pieter Wuille
95bfe7d574 clusterlin: replace benchmarks with SFL-hard ones (bench)
This also adds a per-cost variant of each.
2025-12-18 14:17:28 -05:00
Pieter Wuille
bb5cb222ae depgraph: add memory usage control (feature)
Co-Authored-By: Lőrinc <pap.lorinc@gmail.com>
2025-10-11 17:25:09 -04:00
Pieter Wuille
cfe9958852 txgraph: track amount of work done in linearization (preparation) 2025-07-14 09:41:17 -04:00
MarcoFalke
fa9ca13f35
refactor: Sort includes of touched source files 2025-06-03 19:56:55 +02:00
MarcoFalke
fae71d30f7
clang-tidy: Apply modernize-deprecated-headers
This can be reproduced according to the developer notes with something
like

( cd ./src/ && ../contrib/devtools/run-clang-tidy.py -p ../bld-cmake -fix -j $(nproc) )

Also, the header related changes were done manually.
2025-06-03 15:13:54 +02:00
Pieter Wuille
a52b53926b clusterlin: add GetConnectedComponent
This abstracts out the finding of the connected component that includes
a given element from FindConnectedComponent (which just finds any connected
component).

Use this in the txgraph fuzz test, which was effectively reimplementing this
logic. At the same time, improve its performance by replacing a vector with a
set.
2025-03-27 15:48:44 -04:00
Pieter Wuille
c7d5dcaa61 clusterlin: fix typos 2025-03-27 12:41:24 -04:00
Pieter Wuille
d449773899 scripted-diff: (refactor) ClusterIndex -> DepGraphIndex
Since cluster_linearize.h does not actually have a Cluster type anymore, it is more
appropriate to rename the index type to DepGraphIndex.

-BEGIN VERIFY SCRIPT-
sed -i 's/Data type to represent transaction indices in clusters./Data type to represent transaction indices in DepGraphs and the clusters they represent./' $(git grep -l 'using ClusterIndex')
sed -i 's|\<ClusterIndex\>|DepGraphIndex|g' $(git grep -l 'ClusterIndex')
-END VERIFY SCRIPT-
2025-03-24 09:34:54 -04:00
Pieter Wuille
bfeb69f6e0 clusterlin: Make IsAcyclic() a DepGraph member function
... instead of being a separate test-only function.

Also add a fuzz test for it returning false.
2025-03-24 09:34:54 -04:00
Pieter Wuille
0aa874a357 clusterlin: Add FixLinearization function + fuzz test
This function takes an existing ordering for transactions in a DepGraph, and
makes it a valid linearization for it (i.e., topological). Any topological
prefix of the input remains untouched.
2025-03-24 09:34:54 -04:00
MarcoFalke
fa0c6b7179
refactor: Remove unused Span alias
Also, fixup some wording.
2025-03-12 19:45:49 +01:00
MarcoFalke
fade0b5e5e
scripted-diff: Use std::span over Span
-BEGIN VERIFY SCRIPT-

 ren() { sed -i "s!\<$1\>!$2!g" $( git grep -l "$1" -- "./src" ":(exclude)src/span.h" ":(exclude)src/leveldb/db/log_test.cc" ) ; }

 ren Span            std::span
 ren AsBytes         std::as_bytes
 ren AsWritableBytes std::as_writable_bytes

 sed -i 's!SpanPopBack(Span!SpanPopBack(std::span!g' ./src/span.h

-END VERIFY SCRIPT-
2025-03-12 19:45:37 +01:00