Skip to content

Commit 2a41f15

Browse files
committed
clusterlin: add SearchCandidateFinder class
Similar to AncestorCandidateFinder, this encapsulates the state needed for finding good candidate sets using a search algorithm.
1 parent 4828079 commit 2a41f15

File tree

2 files changed

+392
-0
lines changed

2 files changed

+392
-0
lines changed

src/cluster_linearize.h

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#ifndef BITCOIN_CLUSTER_LINEARIZE_H
66
#define BITCOIN_CLUSTER_LINEARIZE_H
77

8+
#include <algorithm>
9+
#include <numeric>
810
#include <optional>
911
#include <stdint.h>
1012
#include <vector>
@@ -176,13 +178,23 @@ struct SetInfo
176178
/** Their combined fee and size. */
177179
FeeFrac feerate;
178180

181+
/** Construct a SetInfo for the empty set. */
182+
SetInfo() noexcept = default;
183+
179184
/** Construct a SetInfo for a specified set and feerate. */
180185
SetInfo(const SetType& txn, const FeeFrac& fr) noexcept : transactions(txn), feerate(fr) {}
181186

182187
/** Construct a SetInfo for a set of transactions in a depgraph. */
183188
explicit SetInfo(const DepGraph<SetType>& depgraph, const SetType& txn) noexcept :
184189
transactions(txn), feerate(depgraph.FeeRate(txn)) {}
185190

191+
/** Construct a new SetInfo equal to this, with more transactions added (which may overlap
192+
* with the existing transactions in the SetInfo). */
193+
[[nodiscard]] SetInfo Add(const DepGraph<SetType>& depgraph, const SetType& txn) const noexcept
194+
{
195+
return {transactions | txn, feerate + depgraph.FeeRate(txn - transactions)};
196+
}
197+
186198
/** Permit equality testing. */
187199
friend bool operator==(const SetInfo&, const SetInfo&) noexcept = default;
188200
};
@@ -283,6 +295,165 @@ class AncestorCandidateFinder
283295
}
284296
};
285297

298+
/** Class encapsulating the state needed to perform search for good candidate sets.
299+
*
300+
* It is initialized for an entire DepGraph, and parts of the graph can be dropped by calling
301+
* MarkDone().
302+
*
303+
* As long as any part of the graph remains, FindCandidateSet() can be called to perform a search
304+
* over the set of topologically-valid subsets of that remainder, with a limit on how many
305+
* combinations are tried.
306+
*/
307+
template<typename SetType>
308+
class SearchCandidateFinder
309+
{
310+
/** Internal dependency graph for the cluster. */
311+
const DepGraph<SetType>& m_depgraph;
312+
/** Which transactions are left to do (sorted indices). */
313+
SetType m_todo;
314+
315+
public:
316+
/** Construct a candidate finder for a graph.
317+
*
318+
* @param[in] depgraph Dependency graph for the to-be-linearized cluster.
319+
*
320+
* Complexity: O(1).
321+
*/
322+
SearchCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
323+
m_depgraph(depgraph),
324+
m_todo(SetType::Fill(depgraph.TxCount())) {}
325+
326+
/** Check whether any unlinearized transactions remain. */
327+
bool AllDone() const noexcept
328+
{
329+
return m_todo.None();
330+
}
331+
332+
/** Find a high-feerate topologically-valid subset of what remains of the cluster.
333+
* Requires !AllDone().
334+
*
335+
* @param[in] max_iterations The maximum number of optimization steps that will be performed.
336+
* @param[in] best A set/feerate pair with an already-known good candidate. This may
337+
* be empty.
338+
* @return A pair of:
339+
* - The best (highest feerate, smallest size as tiebreaker)
340+
* topologically valid subset (and its feerate) that was
341+
* encountered during search. It will be at least as good as the
342+
* best passed in (if not empty).
343+
* - The number of optimization steps that were performed. This will
344+
* be <= max_iterations. If strictly < max_iterations, the
345+
* returned subset is optimal.
346+
*
347+
* Complexity: O(N * min(max_iterations, 2^N)) where N=depgraph.TxCount().
348+
*/
349+
std::pair<SetInfo<SetType>, uint64_t> FindCandidateSet(uint64_t max_iterations, SetInfo<SetType> best) noexcept
350+
{
351+
Assume(!AllDone());
352+
353+
/** Type for work queue items. */
354+
struct WorkItem
355+
{
356+
/** Set of transactions definitely included (and its feerate). This must be a subset
357+
* of m_todo, and be topologically valid (includes all in-m_todo ancestors of
358+
* itself). */
359+
SetInfo<SetType> inc;
360+
/** Set of undecided transactions. This must be a subset of m_todo, and have no overlap
361+
* with inc. The set (inc | und) must be topologically valid. */
362+
SetType und;
363+
364+
/** Construct a new work item. */
365+
WorkItem(SetInfo<SetType>&& i, SetType&& u) noexcept :
366+
inc(std::move(i)), und(std::move(u)) {}
367+
};
368+
369+
/** The queue of work items. */
370+
std::vector<WorkItem> queue;
371+
372+
// Create an initial entry with m_todo as undecided. Also use it as best if not provided,
373+
// so that during the work processing loop below, and during the add_fn/split_fn calls, we
374+
// do not need to deal with the best=empty case.
375+
if (best.feerate.IsEmpty()) best = SetInfo(m_depgraph, m_todo);
376+
queue.emplace_back(SetInfo<SetType>{}, SetType{m_todo});
377+
378+
/** Local copy of the iteration limit. */
379+
uint64_t iterations_left = max_iterations;
380+
381+
/** Internal function to add an item to the queue of elements to explore if there are any
382+
* transactions left to split on, and to update best.
383+
*
384+
* - inc: the "inc" value for the new work item (must be topological).
385+
* - und: the "und" value for the new work item ((inc | und) must be topological).
386+
*/
387+
auto add_fn = [&](SetInfo<SetType> inc, SetType und) noexcept {
388+
if (!inc.feerate.IsEmpty()) {
389+
// If inc's feerate is better than best's, remember it as our new best.
390+
if (inc.feerate > best.feerate) {
391+
best = inc;
392+
}
393+
} else {
394+
Assume(inc.transactions.None());
395+
}
396+
397+
// Make sure there are undecided transactions left to split on.
398+
if (und.None()) return;
399+
400+
// Actually construct a new work item on the queue.
401+
queue.emplace_back(std::move(inc), std::move(und));
402+
};
403+
404+
/** Internal process function. It takes an existing work item, and splits it in two: one
405+
* with a particular transaction (and its ancestors) included, and one with that
406+
* transaction (and its descendants) excluded. */
407+
auto split_fn = [&](WorkItem&& elem) noexcept {
408+
// Any queue element must have undecided transactions left, otherwise there is nothing
409+
// to explore anymore.
410+
Assume(elem.und.Any());
411+
// The included and undecided set are all subsets of m_todo.
412+
Assume(elem.inc.transactions.IsSubsetOf(m_todo) && elem.und.IsSubsetOf(m_todo));
413+
// Included transactions cannot be undecided.
414+
Assume(!elem.inc.transactions.Overlaps(elem.und));
415+
416+
// Pick the first undecided transaction as the one to split on.
417+
const ClusterIndex split = elem.und.First();
418+
419+
// Add a work item corresponding to exclusion of the split transaction.
420+
const auto& desc = m_depgraph.Descendants(split);
421+
add_fn(/*inc=*/elem.inc,
422+
/*und=*/elem.und - desc);
423+
424+
// Add a work item corresponding to inclusion of the split transaction.
425+
const auto anc = m_depgraph.Ancestors(split) & m_todo;
426+
add_fn(/*inc=*/elem.inc.Add(m_depgraph, anc),
427+
/*und=*/elem.und - anc);
428+
429+
// Account for the performed split.
430+
--iterations_left;
431+
};
432+
433+
// Work processing loop.
434+
while (!queue.empty()) {
435+
if (!iterations_left) break;
436+
auto elem = queue.back();
437+
queue.pop_back();
438+
split_fn(std::move(elem));
439+
}
440+
441+
// Return the found best set and the number of iterations performed.
442+
return {std::move(best), max_iterations - iterations_left};
443+
}
444+
445+
/** Remove a subset of transactions from the cluster being linearized.
446+
*
447+
* Complexity: O(N) where N=done.Count().
448+
*/
449+
void MarkDone(const SetType& done) noexcept
450+
{
451+
Assume(done.Any());
452+
Assume(done.IsSubsetOf(m_todo));
453+
m_todo -= done;
454+
}
455+
};
456+
286457
} // namespace cluster_linearize
287458

288459
#endif // BITCOIN_CLUSTER_LINEARIZE_H

0 commit comments

Comments
 (0)