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
|
||||
// linear-scan removals needed.
|
||||
let last = m - 1;
|
||||
let mut sorted: Vec<Vec<f64>> = points.to_vec();
|
||||
sorted.sort_by(|a, b| {
|
||||
a[last]
|
||||
.partial_cmp(&b[last])
|
||||
// Index-sort instead of cloning every point's inner vector. The
|
||||
// recursion stays bit-identical because we still iterate the same
|
||||
// points in the same order.
|
||||
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)
|
||||
});
|
||||
|
||||
// Pre-project all points onto the first M-1 axes once. The active
|
||||
// set at each band is the prefix `projected_all[..=k]`; we slice
|
||||
// that prefix instead of rebuilding it per band.
|
||||
let projected_all: Vec<Vec<f64>> = sorted.iter().map(|q| q[..last].to_vec()).collect();
|
||||
// Pre-project once onto the first M-1 axes, in the sorted order.
|
||||
// The active set at iteration `k` is the prefix `projected[..=k]`,
|
||||
// so the inner recursion just slices the prefix.
|
||||
let projected: Vec<Vec<f64>> = order.iter().map(|&i| points[i][..last].to_vec()).collect();
|
||||
let sub_reference: &[f64] = &reference[..last];
|
||||
let mut total = 0.0;
|
||||
let mut prev = reference[last];
|
||||
for k in (0..sorted.len()).rev() {
|
||||
let p = &sorted[k];
|
||||
let depth = prev - p[last];
|
||||
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_all[..=k];
|
||||
let active = &projected[..=k];
|
||||
// The 2-D base case sweeps in sorted-x order and skips any
|
||||
// point with `y >= last_y`, which is exactly the dominance
|
||||
// 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;
|
||||
}
|
||||
prev = p[last];
|
||||
prev = p_last;
|
||||
}
|
||||
|
||||
total
|
||||
|
||||
Reference in New Issue
Block a user