perf(hypervolume): cut HSO recursion overhead by ~30× on n=100/3-D
The HSO recursion in `hypervolume_nd` had three overheads that dominated SMS-EMOA's per-generation cost on DTLZ2 (5.6 s baseline, ~30 k generations × ~40 HV calls per generation = ~1.2 M HV calls per run): 1. `active = sorted.clone()` plus `active.iter().position(...)` linear scan to remove the just-processed point each band — O(N) per band, total O(N²) per HV call. 2. Per-band re-projection `active.iter().map(|q| q[..last].to_vec())` — full Vec<Vec<f64>> rebuild for every band, O(N·M) allocations per HV call. 3. `non_dominated_projection` called even when recursing into the M=2 base case, whose sweep already filters dominated points internally. Replace (1) with prefix-slicing `projected_all[..=k]` (sort points ascending by last axis once; the active set at each band is just a prefix). Pre-project once outside the loop (2). Skip the explicit non-dominance filter when the inner recursion is M=2 (3). Bit-identical output verified by re-running the compare harness and diffing against the v0.3.0 snapshot — every quality metric matches to the last decimal. gungraun (instructions): - hypervolume_nd_3d n=30: 676 902 → 87 969 (-87 %, 7.7×) - hypervolume_nd_3d n=100: 13 523 760 → 422 767 (-97 %, 32×) Wall-clock (compare harness, 10-seed mean): - SMS-EMOA / DTLZ2: 5643 ms → 1413 ms (-4230 ms, -75 %)
This commit is contained in:
+27
-19
@@ -219,10 +219,10 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
|
|||||||
// corresponding sub-reference), multiplied by band thickness, is
|
// corresponding sub-reference), multiplied by band thickness, is
|
||||||
// the band's HV contribution.
|
// the band's HV contribution.
|
||||||
//
|
//
|
||||||
// Because boxes extend from `p[last]` UP TO `reference[last]`, every
|
// We sort points ascending by the last axis once, then iterate from
|
||||||
// point is active in the band immediately below the reference. We
|
// the largest last-axis value downward. The active set at iteration
|
||||||
// therefore start with `active = all points` and REMOVE the largest
|
// `k` is exactly the prefix `sorted[..=k]` — no allocations or
|
||||||
// remaining last-axis point each iteration.
|
// linear-scan removals needed.
|
||||||
let last = m - 1;
|
let last = m - 1;
|
||||||
let mut sorted: Vec<Vec<f64>> = points.to_vec();
|
let mut sorted: Vec<Vec<f64>> = points.to_vec();
|
||||||
sorted.sort_by(|a, b| {
|
sorted.sort_by(|a, b| {
|
||||||
@@ -231,24 +231,32 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
|
|||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
});
|
});
|
||||||
|
|
||||||
let sub_reference: Vec<f64> = reference[..last].to_vec();
|
// 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();
|
||||||
|
let sub_reference: &[f64] = &reference[..last];
|
||||||
let mut total = 0.0;
|
let mut total = 0.0;
|
||||||
let mut active: Vec<Vec<f64>> = sorted.clone();
|
|
||||||
let mut prev = reference[last];
|
let mut prev = reference[last];
|
||||||
for p in sorted.into_iter().rev() {
|
for k in (0..sorted.len()).rev() {
|
||||||
|
let p = &sorted[k];
|
||||||
let depth = prev - p[last];
|
let depth = prev - p[last];
|
||||||
if depth > 0.0 && !active.is_empty() {
|
if depth > 0.0 {
|
||||||
let projected: Vec<Vec<f64>> = active.iter().map(|q| q[..last].to_vec()).collect();
|
let active = &projected_all[..=k];
|
||||||
let nd = non_dominated_projection(&projected);
|
// The 2-D base case sweeps in sorted-x order and skips any
|
||||||
total += depth * hso_recursive(&nd, &sub_reference);
|
// point with `y >= last_y`, which is exactly the dominance
|
||||||
}
|
// filter — so for M=3 (sub_reference len 2) we can hand
|
||||||
// Remove the just-processed point (the one with the largest
|
// `active` straight to `hso_recursive` without paying for
|
||||||
// remaining last-axis value).
|
// an O(K²) `non_dominated_projection` first. For M≥4 we
|
||||||
let idx = active
|
// still need the explicit filter to keep the recursion's
|
||||||
.iter()
|
// upper levels honest.
|
||||||
.position(|q| (q[last] - p[last]).abs() < 1e-15 && q[..last] == p[..last]);
|
let inner = if sub_reference.len() == 2 {
|
||||||
if let Some(i) = idx {
|
hso_recursive(active, sub_reference)
|
||||||
active.swap_remove(i);
|
} else {
|
||||||
|
let nd = non_dominated_projection(active);
|
||||||
|
hso_recursive(&nd, sub_reference)
|
||||||
|
};
|
||||||
|
total += depth * inner;
|
||||||
}
|
}
|
||||||
prev = p[last];
|
prev = p[last];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user