perf(age_moea): cache lp_norm + maintain nearest-neighbor incrementally
The splitting-front survival selection in AGE-MOEA recomputed two expensive things per while-iteration: * `lp_norm(translated[i], p)` for every remaining i — even though the value is constant across iterations. * `nearest_neighbor_distance(i, …, &keep, p)` — a fresh full scan over the keep list, even though only one new candidate was added since the last scan. Both are `powf`-heavy in the L_p frame. Compute lp_norm once per candidate at function entry. Maintain a `nearest[]` array seeded from the initial keep set and updated on every pick by a single `min(nearest[i], lp_distance(i, pick, p))` per remaining i. That cuts the score loop from O(R · K · M) to O(R · M) per iteration, with the dominant powf calls in lp_distance counted once per (remaining, pick) pair instead of per (remaining, full-keep). Wall-clock (compare harness, 10-seed mean): - AGE-MOEA / DTLZ1: 2266 → 430 ms on top of v0.3.0 baseline (5.3×) - AGE-MOEA / ZDT3: 935 → 376 ms (2.5×)
This commit is contained in:
@@ -187,17 +187,34 @@ fn environmental_selection<D: Clone>(
|
||||
// proximity = ||translated||_p
|
||||
// diversity = nearest-neighbor distance in the same L_p frame
|
||||
// among already-selected + splitting members.
|
||||
//
|
||||
// Two caches make this much cheaper than the textbook formulation:
|
||||
// * `prox[i]` — `lp_norm(translated[i], p)` is constant across
|
||||
// iterations, so compute it once per splitting-front member.
|
||||
// * `nearest[i]` — the nearest-keep distance only ever decreases
|
||||
// when a new candidate is picked, so we maintain it
|
||||
// incrementally: seed it from `selected`, then on every pick
|
||||
// update each remaining `i`'s nearest by taking
|
||||
// `min(nearest[i], lp_distance(translated[i], translated[pick], p))`.
|
||||
//
|
||||
// That cuts the score loop from O(R · K · M) per iteration (where
|
||||
// R = remaining count, K = current keep count) to O(R · M) per
|
||||
// iteration, with the dominant `powf` calls in lp_distance counted
|
||||
// once per (remaining, pick) pair instead of per (remaining, all-keep).
|
||||
let mut keep = selected.clone();
|
||||
let mut remaining: Vec<usize> = splitting.clone();
|
||||
let prox: Vec<f64> = (0..combined.len())
|
||||
.map(|i| lp_norm(&translated[i], p))
|
||||
.collect();
|
||||
let mut nearest: Vec<f64> = (0..combined.len())
|
||||
.map(|i| nearest_neighbor_distance(i, &translated, &keep, p))
|
||||
.collect();
|
||||
while keep.len() < n {
|
||||
// Compute scores for every remaining candidate; pick the one with
|
||||
// the largest combined score.
|
||||
// Pick the remaining candidate with the largest score.
|
||||
let mut best_idx: Option<usize> = None;
|
||||
let mut best_score = f64::NEG_INFINITY;
|
||||
for &i in &remaining {
|
||||
let prox = lp_norm(&translated[i], p);
|
||||
let div = nearest_neighbor_distance(i, &translated, &keep, p);
|
||||
let score = div / (prox.max(1e-12));
|
||||
let score = nearest[i] / (prox[i].max(1e-12));
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_idx = Some(i);
|
||||
@@ -208,6 +225,14 @@ fn environmental_selection<D: Clone>(
|
||||
Some(pick) => {
|
||||
keep.push(pick);
|
||||
remaining.retain(|&i| i != pick);
|
||||
// Update each surviving remaining's nearest-keep using
|
||||
// just the distance to the new pick.
|
||||
for &i in &remaining {
|
||||
let d = lp_distance(&translated[i], &translated[pick], p);
|
||||
if d < nearest[i] {
|
||||
nearest[i] = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user