Skip to content

Commit 2854979

Browse files
committed
clusterlin: permit passing in existing linearization to Linearize
This implements the LIMO algorithm for linearizing by improving an existing linearization. See https://delvingbitcoin.org/t/limo-combining-the-best-parts-of-linearization-search-and-merging for details.
1 parent 97d9871 commit 2854979

File tree

3 files changed

+53
-9
lines changed

3 files changed

+53
-9
lines changed

src/bench/cluster_linearize.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ void BenchLinearizePerIterWorstCase(ClusterIndex ntx, benchmark::Bench& bench)
109109
});
110110
}
111111

112-
/** Benchmark for linearization of a trivial linear graph using just ancestor sort.
112+
/** Benchmark for linearization improvement of a trivial linear graph using just ancestor sort.
113113
*
114114
* Its goal is measuring how much time linearization may take without any search iterations.
115115
*
@@ -124,8 +124,10 @@ void BenchLinearizeNoItersWorstCase(ClusterIndex ntx, benchmark::Bench& bench)
124124
{
125125
const auto depgraph = MakeLinearGraph<SetType>(ntx);
126126
uint64_t rng_seed = 0;
127+
std::vector<ClusterIndex> old_lin(ntx);
128+
for (ClusterIndex i = 0; i < ntx; ++i) old_lin[i] = i;
127129
bench.run([&] {
128-
Linearize(depgraph, /*max_iterations=*/0, rng_seed++);
130+
Linearize(depgraph, /*max_iterations=*/0, rng_seed++, old_lin);
129131
});
130132
}
131133

src/cluster_linearize.h

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -663,23 +663,27 @@ class SearchCandidateFinder
663663
}
664664
};
665665

666-
/** Find a linearization for a cluster.
666+
/** Find or improve a linearization for a cluster.
667667
*
668668
* @param[in] depgraph Dependency graph of the cluster to be linearized.
669669
* @param[in] max_iterations Upper bound on the number of optimization steps that will be done.
670670
* @param[in] rng_seed A random number seed to control search order. This prevents peers
671671
* from predicting exactly which clusters would be hard for us to
672672
* linearize.
673+
* @param[in] old_linearization An existing linearization for the cluster (which must be
674+
* topologically valid), or empty.
673675
* @return A pair of:
674-
* - The resulting linearization.
676+
* - The resulting linearization. It is guaranteed to be at least as
677+
* good (in the feerate diagram sense) as old_linearization.
675678
* - A boolean indicating whether the result is guaranteed to be
676679
* optimal.
677680
*
678681
* Complexity: O(N * min(max_iterations + N, 2^N)) where N=depgraph.TxCount().
679682
*/
680683
template<typename SetType>
681-
std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed) noexcept
684+
std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed, Span<const ClusterIndex> old_linearization = {}) noexcept
682685
{
686+
Assume(old_linearization.empty() || old_linearization.size() == depgraph.TxCount());
683687
if (depgraph.TxCount() == 0) return {{}, true};
684688

685689
uint64_t iterations_left = max_iterations;
@@ -690,9 +694,17 @@ std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& de
690694
linearization.reserve(depgraph.TxCount());
691695
bool optimal = true;
692696

697+
/** Chunking of what remains of the old linearization. */
698+
LinearizationChunking old_chunking(depgraph, old_linearization);
699+
693700
while (true) {
694-
// Initialize best as the best remaining ancestor set.
701+
// Find the highest-feerate prefix of the remainder of old_linearization.
702+
SetInfo<SetType> best_prefix;
703+
if (old_chunking.NumChunksLeft()) best_prefix = old_chunking.GetChunk(0);
704+
705+
// Then initialize best to be either the best remaining ancestor set, or the first chunk.
695706
auto best = anc_finder.FindCandidateSet();
707+
if (!best_prefix.feerate.IsEmpty() && best_prefix.feerate >= best.feerate) best = best_prefix;
696708

697709
// Invoke bounded search to update best, with up to half of our remaining iterations as
698710
// limit.
@@ -703,6 +715,12 @@ std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& de
703715

704716
if (iterations_done_now == max_iterations_now) {
705717
optimal = false;
718+
// If the search result is not (guaranteed to be) optimal, run intersections to make
719+
// sure we don't pick something that makes us unable to reach further diagram points
720+
// of the old linearization.
721+
if (old_chunking.NumChunksLeft() > 0) {
722+
best = old_chunking.Intersect(best);
723+
}
706724
}
707725

708726
// Add to output in topological order.
@@ -712,6 +730,9 @@ std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& de
712730
anc_finder.MarkDone(best.transactions);
713731
if (anc_finder.AllDone()) break;
714732
src_finder.MarkDone(best.transactions);
733+
if (old_chunking.NumChunksLeft() > 0) {
734+
old_chunking.MarkDone(best.transactions);
735+
}
715736
}
716737

717738
return {std::move(linearization), optimal};

src/test/fuzz/cluster_linearize.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ class ExhaustiveCandidateFinder
143143

144144
/** A simple linearization algorithm.
145145
*
146-
* This matches Linearize() in interface and behavior, though with fewer optimizations, and using
147-
* just SimpleCandidateFinder rather than AncestorCandidateFinder and SearchCandidateFinder.
146+
* This matches Linearize() in interface and behavior, though with fewer optimizations, lacking
147+
* the ability to pass in an existing linearization, and using just SimpleCandidateFinder rather
148+
* than AncestorCandidateFinder and SearchCandidateFinder.
148149
*/
149150
template<typename SetType>
150151
std::pair<std::vector<ClusterIndex>, bool> SimpleLinearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations)
@@ -614,12 +615,32 @@ FUZZ_TARGET(clusterlin_linearize)
614615
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
615616
} catch (const std::ios_base::failure&) {}
616617

618+
// Optionally construct an old linearization for it.
619+
std::vector<ClusterIndex> old_linearization;
620+
{
621+
uint8_t have_old_linearization{0};
622+
try {
623+
reader >> have_old_linearization;
624+
} catch(const std::ios_base::failure&) {}
625+
if (have_old_linearization & 1) {
626+
old_linearization = ReadLinearization(depgraph, reader);
627+
SanityCheck(depgraph, old_linearization);
628+
}
629+
}
630+
617631
// Invoke Linearize().
618632
iter_count &= 0x7ffff;
619-
auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed);
633+
auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
620634
SanityCheck(depgraph, linearization);
621635
auto chunking = ChunkLinearization(depgraph, linearization);
622636

637+
// Linearization must always be as good as the old one, if provided.
638+
if (!old_linearization.empty()) {
639+
auto old_chunking = ChunkLinearization(depgraph, old_linearization);
640+
auto cmp = CompareChunks(chunking, old_chunking);
641+
assert(cmp >= 0);
642+
}
643+
623644
// If the iteration count is sufficiently high, an optimal linearization must be found.
624645
// Each linearization step can use up to 2^k iterations, with steps k=1..n. That sum is
625646
// 2 * (2^n - 1)

0 commit comments

Comments
 (0)