if (m.m_dst_end <= region.m_begin
|| m.m_dst_begin >= region.m_end) continue;
const uint64_t end = min(m.m_dst_end, region.m_end);
- if (end > best_end) {
+ // Farther reach wins; equal reach breaks toward the
+ // higher-ref src (see the DP reach precompute) so a
+ // whole-extent cover consolidates onto the canonical
+ // extent. Src choice does not change the edge's debt.
+ if (end > best_end
+ || (end == best_end
+ && best_idx != in.m_matches.size()
+ && m.m_src_ref_count
+ > in.m_matches[best_idx].m_src_ref_count)) {
best_end = end;
best_idx = idx;
}
// O(M) covering-match rescan that previously sat in the innermost
// loop and made the search O(V^2 * S * M); M=100 fell from ~1.5 s to
// milliseconds (test/bench-bees-plan.cc).
+ //
+ // Reach is the primary key (a farther reach lets a later covering use
+ // fewer fragments — a real debt term); among equal-reach matches the
+ // argmax breaks ties toward the higher-ref src, so a whole-extent
+ // dedupe (every src reaches the full extent, all tied on reach)
+ // consolidates onto the canonical high-ref extent instead of an
+ // arbitrary one. Since the src choice never changes this edge's cost,
+ // the tie-break is free and leaves the covering's debt untouched.
vector<uint64_t> reach_end(nverts, 0);
vector<size_t> cover_at(nverts, in.m_matches.size());
{
const uint64_t a = verts[vi];
while (mp < m_by_begin.size()
&& in.m_matches[m_by_begin[mp]].m_dst_begin <= a) {
- const auto &m = in.m_matches[m_by_begin[mp]];
- if (m.m_dst_end > best_end) {
+ const size_t idx = m_by_begin[mp];
+ const auto &m = in.m_matches[idx];
+ if (m.m_dst_end > best_end
+ || (m.m_dst_end == best_end
+ && best_idx != in.m_matches.size()
+ && m.m_src_ref_count
+ > in.m_matches[best_idx].m_src_ref_count)) {
best_end = m.m_dst_end;
- best_idx = m_by_begin[mp];
+ best_idx = idx;
}
++mp;
}
};
/// One candidate dedupe edge: a match's dst span plus an opaque group id
-/// the caller uses to recover the CandidateGroup / source extent. Only the
-/// dst geometry drives the search (the cost model charges the dst's
-/// fragmentation, not the src), so the src extent is not needed here.
+/// the caller uses to recover the CandidateGroup / source extent. The dst
+/// geometry drives the search's *cost* (the model charges the dst's
+/// fragmentation, not the src), so the src extent's bytes are not needed
+/// here. m_src_ref_count is carried only as a tie-break: among matches that
+/// reach equally far (equal dst cost) the search prefers the src with the
+/// most references, consolidating dedupe onto the already-canonical extent
+/// rather than an arbitrary one. This is the "prefer more-ref src when other
+/// costs are equal" rule — it never overrides reach, only orders equal-reach
+/// ties, so it costs nothing in the O(M+V) reach sweep. 0 (the default, and
+/// what a single-src Plan B leaves unset) makes every tie resolve as before.
struct PlanSearchMatch {
- uint64_t m_dst_begin = 0;
- uint64_t m_dst_end = 0;
- size_t m_group_id = 0; ///< caller-defined match identity
+ uint64_t m_dst_begin = 0;
+ uint64_t m_dst_end = 0;
+ size_t m_group_id = 0; ///< caller-defined match identity
+ uint64_t m_src_ref_count = 0; ///< src refs()->size(), tie-break only
};
/// Fixed per-dst inputs to the search, mirroring scan_next_plan_init's
for (const auto &group : candidate_groups) {
for (const auto &match : group.m_matches) {
if (match.size() == 0) continue;
+ // Carry the src ref count as the search's equal-reach tie-break
+ // (prefer consolidating onto the higher-ref, canonical src). The
+ // refs() lookup is layer-cached; the src extents here were already
+ // resolved to produce these matches, so this is warm.
+ const uint64_t src_refs = match.m_src.refs(layer)->size();
in.m_matches.push_back(PlanSearchMatch{
- match.m_dst_begin, match.m_dst_end, flat.size() });
+ match.m_dst_begin, match.m_dst_end, flat.size(), src_refs });
flat.push_back(FlatMatch{ &group, &match });
}
}
assert(q1 - q0 == 3000.0 * 100.0);
}
+// Feature #1: among matches that reach equally far (equal dst cost) the search
+// consolidates onto the higher-ref src — the "prefer more-ref src when other
+// costs are equal" rule that converges dedupe onto a canonical extent instead
+// of chaining through arbitrary ones. Reach stays the primary key, so a
+// farther-reaching low-ref src still beats a short high-ref one.
+static void
+test_search_prefers_high_ref_src()
+{
+ auto policy = default_policy();
+
+ // Two srcs both cover the whole extent [0,10): equal reach, equal dst
+ // cost. The search must pick the higher-ref src. Try both input orders
+ // to prove the tie-break is order-independent (not just "keep first").
+ for (const bool high_first : { false, true }) {
+ auto in = make_inputs(10, { {0, 10}, {0, 10} }, /*ref=*/1);
+ const size_t hi = high_first ? 0 : 1;
+ const size_t lo = high_first ? 1 : 0;
+ in.m_matches[hi].m_src_ref_count = 5000;
+ in.m_matches[lo].m_src_ref_count = 5;
+
+ // Exact DP path.
+ policy.m_plan_max_vertices = 16384;
+ const auto dp = scan_next_plan_shortest_path(in, policy);
+ assert(dp.has_value());
+ assert(dp->m_selected_matches.size() == 1);
+ assert(dp->m_selected_matches[0] == hi); // canonical src chosen
+ assert(dp->m_debt == result_recomputed_debt(in, policy, *dp));
+
+ // Greedy fallback (tiny V budget forces it): same decision, and the
+ // tie-break costs nothing on the debt (unchanged reconciliation).
+ policy.m_plan_max_vertices = 1;
+ const auto gr = scan_next_plan_shortest_path(in, policy);
+ assert(gr.has_value());
+ assert(gr->m_selected_matches.size() == 1);
+ assert(gr->m_selected_matches[0] == hi);
+ assert(gr->m_debt == result_recomputed_debt(in, policy, *gr));
+ }
+
+ // Reach stays primary: a full-span low-ref src beats a short high-ref src
+ // even though the tie-break alone would prefer the latter. Choosing the
+ // short src would leave a gap needing an extra op — a real debt increase,
+ // not an equal-cost tie — so the tie-break must not fire here.
+ {
+ auto in = make_inputs(10, { {0, 10}, {0, 6} }, /*ref=*/1);
+ in.m_matches[0].m_src_ref_count = 5; // full span, few refs
+ in.m_matches[1].m_src_ref_count = 5000; // short span, many refs
+ policy.m_plan_max_vertices = 16384;
+ const auto dp = scan_next_plan_shortest_path(in, policy);
+ assert(dp.has_value());
+ assert(dp->m_selected_matches.size() == 1);
+ assert(dp->m_selected_matches[0] == 0); // full-span src wins on reach
+ }
+}
+
int
main(int, char **)
{
RUN_A_TEST(test_search_v_budget_greedy_abandons_over_limit());
RUN_A_TEST(test_refs_ceiling_gate());
RUN_A_TEST(test_ref_op_cost_prices_whole_extent_dedupe());
+ RUN_A_TEST(test_search_prefers_high_ref_src());
return 0;
}