perf(hypervolume): index-sort instead of cloning point vectors
The M≥3 branch of `hso_recursive` cloned every input point into `sorted: Vec<Vec<f64>>` solely so it could sort. Each clone is M f64s allocated; with N points per call and ~30 HV calls per SMS-EMOA generation × 30 k generations, that's millions of small Vec<f64> allocations. Sort indices into a `Vec<usize>` instead, then iterate the original points by index. The pre-projection step still produces a Vec<Vec<f64>> (which the active-prefix slicing requires), but we save the outer N inner-Vec clones per call. gungraun (instructions): - hypervolume_nd_3d n=30: 87 969 → 70 334 (-20 %, 1.25×) - hypervolume_nd_3d n=100: 422 767 → 367 767 (-13 %, 1.15×) Cumulative vs the v0.3.0 baseline: - hypervolume_nd_3d n=30: 676 902 → 70 334 (9.6×) - hypervolume_nd_3d n=100: 13 523 760 → 367 767 (37×) Wall-clock impact is in the noise on the compare harness because the SMS-EMOA worst-front HV calls operate on small fronts (5–10 points once converged). The win is most visible in synthetic dense-front HV benchmarks.
This commit is contained in:
+16
-13
@@ -224,25 +224,28 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
|
|||||||
// `k` is exactly the prefix `sorted[..=k]` — no allocations or
|
// `k` is exactly the prefix `sorted[..=k]` — no allocations or
|
||||||
// linear-scan removals needed.
|
// linear-scan removals needed.
|
||||||
let last = m - 1;
|
let last = m - 1;
|
||||||
let mut sorted: Vec<Vec<f64>> = points.to_vec();
|
// Index-sort instead of cloning every point's inner vector. The
|
||||||
sorted.sort_by(|a, b| {
|
// recursion stays bit-identical because we still iterate the same
|
||||||
a[last]
|
// points in the same order.
|
||||||
.partial_cmp(&b[last])
|
let mut order: Vec<usize> = (0..points.len()).collect();
|
||||||
|
order.sort_by(|&i, &j| {
|
||||||
|
points[i][last]
|
||||||
|
.partial_cmp(&points[j][last])
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pre-project all points onto the first M-1 axes once. The active
|
// Pre-project once onto the first M-1 axes, in the sorted order.
|
||||||
// set at each band is the prefix `projected_all[..=k]`; we slice
|
// The active set at iteration `k` is the prefix `projected[..=k]`,
|
||||||
// that prefix instead of rebuilding it per band.
|
// so the inner recursion just slices the prefix.
|
||||||
let projected_all: Vec<Vec<f64>> = sorted.iter().map(|q| q[..last].to_vec()).collect();
|
let projected: Vec<Vec<f64>> = order.iter().map(|&i| points[i][..last].to_vec()).collect();
|
||||||
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..sorted.len()).rev() {
|
for k in (0..order.len()).rev() {
|
||||||
let p = &sorted[k];
|
let p_last = points[order[k]][last];
|
||||||
let depth = prev - p[last];
|
let depth = prev - p_last;
|
||||||
if depth > 0.0 {
|
if depth > 0.0 {
|
||||||
let active = &projected_all[..=k];
|
let active = &projected[..=k];
|
||||||
// The 2-D base case sweeps in sorted-x order and skips any
|
// The 2-D base case sweeps in sorted-x order and skips any
|
||||||
// point with `y >= last_y`, which is exactly the dominance
|
// point with `y >= last_y`, which is exactly the dominance
|
||||||
// filter — so for M=3 (sub_reference len 2) we can hand
|
// filter — so for M=3 (sub_reference len 2) we can hand
|
||||||
@@ -258,7 +261,7 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
|
|||||||
};
|
};
|
||||||
total += depth * inner;
|
total += depth * inner;
|
||||||
}
|
}
|
||||||
prev = p[last];
|
prev = p_last;
|
||||||
}
|
}
|
||||||
|
|
||||||
total
|
total
|
||||||
|
|||||||
Reference in New Issue
Block a user