Skip to content

Commit 85a285a

Browse files
committed
clusterlin: separate initial search entries per component (optimization)
Before this commit, the worst case for linearization involves clusters which break apart in several smaller components after the first candidate is included in the output linearization. Address this by never considering work items that span multiple components of what remains of the cluster.
1 parent e4faea9 commit 85a285a

File tree

2 files changed

+67
-16
lines changed

2 files changed

+67
-16
lines changed

src/cluster_linearize.h

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -613,11 +613,20 @@ class SearchCandidateFinder
613613
VecDeque<WorkItem> queue;
614614
queue.reserve(std::max<size_t>(256, 2 * m_todo.Count()));
615615

616-
// Create an initial entry with m_todo as undecided. Also use it as best if not provided,
617-
// so that during the work processing loop below, and during the add_fn/split_fn calls, we
618-
// do not need to deal with the best=empty case.
619-
if (best.feerate.IsEmpty()) best = SetInfo(m_depgraph, m_todo);
620-
queue.emplace_back(SetInfo<SetType>{}, SetType{m_todo});
616+
// Create initial entries per connected component of m_todo. While clusters themselves are
617+
// generally connected, this is not necessarily true after some parts have already been
618+
// removed from m_todo. Without this, effort can be wasted on searching "inc" sets that
619+
// span multiple components.
620+
auto to_cover = m_todo;
621+
do {
622+
auto component = m_depgraph.FindConnectedComponent(to_cover);
623+
to_cover -= component;
624+
// If best is not provided, set it to the first component, so that during the work
625+
// processing loop below, and during the add_fn/split_fn calls, we do not need to deal
626+
// with the best=empty case.
627+
if (best.feerate.IsEmpty()) best = SetInfo(m_depgraph, component);
628+
queue.emplace_back(/*inc=*/SetInfo<SetType>{}, /*und=*/std::move(component));
629+
} while (to_cover.Any());
621630

622631
/** Local copy of the iteration limit. */
623632
uint64_t iterations_left = max_iterations;
@@ -645,7 +654,7 @@ class SearchCandidateFinder
645654
// space runs out (see below), we know that no reallocation of the queue should ever
646655
// occur.
647656
Assume(queue.size() < queue.capacity());
648-
queue.emplace_back(std::move(inc), std::move(und));
657+
queue.emplace_back(/*inc=*/std::move(inc), /*und=*/std::move(und));
649658
};
650659

651660
/** Internal process function. It takes an existing work item, and splits it in two: one

src/test/fuzz/cluster_linearize.cpp

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ std::pair<std::vector<ClusterIndex>, bool> SimpleLinearize(const DepGraph<SetTyp
165165
return {std::move(linearization), optimal};
166166
}
167167

168+
/** Stitch connected components together in a DepGraph, guaranteeing its corresponding cluster is connected. */
169+
template<typename BS>
170+
void MakeConnected(DepGraph<BS>& depgraph)
171+
{
172+
auto todo = BS::Fill(depgraph.TxCount());
173+
auto comp = depgraph.FindConnectedComponent(todo);
174+
Assume(depgraph.IsConnected(comp));
175+
todo -= comp;
176+
while (todo.Any()) {
177+
auto nextcomp = depgraph.FindConnectedComponent(todo);
178+
Assume(depgraph.IsConnected(nextcomp));
179+
depgraph.AddDependency(comp.Last(), nextcomp.First());
180+
todo -= nextcomp;
181+
comp = nextcomp;
182+
}
183+
}
184+
168185
/** Given a dependency graph, and a todo set, read a topological subset of todo from reader. */
169186
template<typename SetType>
170187
SetType ReadTopologicalSet(const DepGraph<SetType>& depgraph, const SetType& todo, SpanReader& reader)
@@ -369,6 +386,20 @@ FUZZ_TARGET(clusterlin_components)
369386
assert(depgraph.FindConnectedComponent(todo).None());
370387
}
371388

389+
FUZZ_TARGET(clusterlin_make_connected)
390+
{
391+
// Verify that MakeConnected makes graphs connected.
392+
393+
SpanReader reader(buffer);
394+
DepGraph<TestBitSet> depgraph;
395+
try {
396+
reader >> Using<DepGraphFormatter>(depgraph);
397+
} catch (const std::ios_base::failure&) {}
398+
MakeConnected(depgraph);
399+
SanityCheck(depgraph);
400+
assert(depgraph.IsConnected());
401+
}
402+
372403
FUZZ_TARGET(clusterlin_chunking)
373404
{
374405
// Verify the correctness of the ChunkLinearization function.
@@ -468,13 +499,17 @@ FUZZ_TARGET(clusterlin_search_finder)
468499
// and comparing with the results from SimpleCandidateFinder, ExhaustiveCandidateFinder, and
469500
// AncestorCandidateFinder.
470501

471-
// Retrieve an RNG seed and a depgraph from the fuzz input.
502+
// Retrieve an RNG seed, a depgraph, and whether to make it connected, from the fuzz input.
472503
SpanReader reader(buffer);
473504
DepGraph<TestBitSet> depgraph;
474505
uint64_t rng_seed{0};
506+
uint8_t make_connected{1};
475507
try {
476-
reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
508+
reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected;
477509
} catch (const std::ios_base::failure&) {}
510+
// The most complicated graphs are connected ones (other ones just split up). Optionally force
511+
// the graph to be connected.
512+
if (make_connected) MakeConnected(depgraph);
478513

479514
// Instantiate ALL the candidate finders.
480515
SearchCandidateFinder src_finder(depgraph, rng_seed);
@@ -513,9 +548,11 @@ FUZZ_TARGET(clusterlin_search_finder)
513548
assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo));
514549
}
515550

516-
// At most 2^N-1 iterations can be required: the number of non-empty subsets a graph with N
517-
// transactions has.
518-
assert(iterations_done <= ((uint64_t{1} << todo.Count()) - 1));
551+
// At most 2^(N-1) iterations can be required: the maximum number of non-empty topological
552+
// subsets a (connected) cluster with N transactions can have. Even when the cluster is no
553+
// longer connected after removing certain transactions, this holds, because the connected
554+
// components are searched separately.
555+
assert(iterations_done <= (uint64_t{1} << (todo.Count() - 1)));
519556

520557
// Perform quality checks only if SearchCandidateFinder claims an optimal result.
521558
if (iterations_done < max_iterations) {
@@ -685,14 +722,19 @@ FUZZ_TARGET(clusterlin_linearize)
685722
{
686723
// Verify the behavior of Linearize().
687724

688-
// Retrieve an RNG seed, an iteration count, and a depgraph from the fuzz input.
725+
// Retrieve an RNG seed, an iteration count, a depgraph, and whether to make it connected from
726+
// the fuzz input.
689727
SpanReader reader(buffer);
690728
DepGraph<TestBitSet> depgraph;
691729
uint64_t rng_seed{0};
692730
uint64_t iter_count{0};
731+
uint8_t make_connected{1};
693732
try {
694-
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
733+
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected;
695734
} catch (const std::ios_base::failure&) {}
735+
// The most complicated graphs are connected ones (other ones just split up). Optionally force
736+
// the graph to be connected.
737+
if (make_connected) MakeConnected(depgraph);
696738

697739
// Optionally construct an old linearization for it.
698740
std::vector<ClusterIndex> old_linearization;
@@ -721,10 +763,10 @@ FUZZ_TARGET(clusterlin_linearize)
721763
}
722764

723765
// If the iteration count is sufficiently high, an optimal linearization must be found.
724-
// Each linearization step can use up to 2^k iterations, with steps k=1..n. That sum is
725-
// 2 * (2^n - 1)
766+
// Each linearization step can use up to 2^(k-1) iterations, with steps k=1..n. That sum is
767+
// 2^n - 1.
726768
const uint64_t n = depgraph.TxCount();
727-
if (n <= 18 && iter_count > 2U * ((uint64_t{1} << n) - 1U)) {
769+
if (n <= 19 && iter_count > (uint64_t{1} << n)) {
728770
assert(optimal);
729771
}
730772

0 commit comments

Comments
 (0)