perf(metrics): stop re-sorting hypervolume_nd prefixes per slice (361K -> 291K instr)
The HSO M=3 path called the generic 2-D base case for every last-axis slice, which re-sorted the active prefix by axis 0 each time -- O(n^2 log n) overall. Since `projected` is already in last-axis order, sorting the projected indices by axis 0 once and sweeping them with a `pi > k` skip gives O(n^2) with no per-slice allocation. The M>=4 path is unchanged (lifted out of the inner branch verbatim). hypervolume_nd_bench_3d n=100: 361_595 -> 291_247 (-19%, 1.24x); n=30 -16%. The sweep visits points in the same (axis-0, then last-axis) order the stable per-prefix sort produced -- output is bit-identical, all 606 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-19
@@ -279,27 +279,58 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
|
|||||||
let sub_reference: &[f64] = &reference[..last];
|
let sub_reference: &[f64] = &reference[..last];
|
||||||
let mut total = 0.0;
|
let mut total = 0.0;
|
||||||
let mut prev = reference[last];
|
let mut prev = reference[last];
|
||||||
for k in (0..order.len()).rev() {
|
|
||||||
let p_last = points[order[k]][last];
|
if sub_reference.len() == 2 {
|
||||||
let depth = prev - p_last;
|
// M == 3: the inner HV is a 2-D staircase sweep. `projected` is in
|
||||||
if depth > 0.0 {
|
// last-axis order, so the active set at step `k` is the prefix
|
||||||
let active = &projected[..=k];
|
// `projected[..=k]`. The generic recursion re-sorts that prefix by
|
||||||
// The 2-D base case sweeps in sorted-x order and skips any
|
// axis 0 on every step — O(n² log n). Instead, sort the projected
|
||||||
// point with `y >= last_y`, which is exactly the dominance
|
// indices by axis 0 once and, for each `k`, sweep them skipping any
|
||||||
// filter — so for M=3 (sub_reference len 2) we can hand
|
// whose last-axis rank exceeds `k`. The sweep visits points in the
|
||||||
// `active` straight to `hso_recursive` without paying for
|
// same (axis-0, then last-axis) order the stable per-prefix sort
|
||||||
// an O(K²) `non_dominated_projection` first. For M≥4 we
|
// produced, so the result is bit-identical.
|
||||||
// still need the explicit filter to keep the recursion's
|
let r0 = sub_reference[0];
|
||||||
// upper levels honest.
|
let r1 = sub_reference[1];
|
||||||
let inner = if sub_reference.len() == 2 {
|
let mut x_order: Vec<usize> = (0..projected.len()).collect();
|
||||||
hso_recursive(active, sub_reference)
|
x_order.sort_by(|&a, &b| {
|
||||||
} else {
|
projected[a][0]
|
||||||
|
.partial_cmp(&projected[b][0])
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
for k in (0..order.len()).rev() {
|
||||||
|
let p_last = points[order[k]][last];
|
||||||
|
let depth = prev - p_last;
|
||||||
|
if depth > 0.0 {
|
||||||
|
let mut area = 0.0;
|
||||||
|
let mut last_y = r1;
|
||||||
|
for &pi in &x_order {
|
||||||
|
if pi > k {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p = &projected[pi];
|
||||||
|
if p[1] >= last_y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
area += (r0 - p[0]) * (last_y - p[1]);
|
||||||
|
last_y = p[1];
|
||||||
|
}
|
||||||
|
total += depth * area;
|
||||||
|
}
|
||||||
|
prev = p_last;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// M >= 4: recurse generically, with the explicit non-dominated
|
||||||
|
// filter to keep the recursion's upper levels honest.
|
||||||
|
for k in (0..order.len()).rev() {
|
||||||
|
let p_last = points[order[k]][last];
|
||||||
|
let depth = prev - p_last;
|
||||||
|
if depth > 0.0 {
|
||||||
|
let active = &projected[..=k];
|
||||||
let nd = non_dominated_projection(active);
|
let nd = non_dominated_projection(active);
|
||||||
hso_recursive(&nd, sub_reference)
|
total += depth * hso_recursive(&nd, sub_reference);
|
||||||
};
|
}
|
||||||
total += depth * inner;
|
prev = p_last;
|
||||||
}
|
}
|
||||||
prev = p_last;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
total
|
total
|
||||||
|
|||||||
Reference in New Issue
Block a user