From 214f07975ae72f925f161b8ef76cf778dd03b07d Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 5 May 2026 12:03:52 -0600 Subject: [PATCH] perf(non_dominated_sort): cache oriented values + inline pareto_compare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Deb fast non-dominated sort calls `pareto_compare` twice for every (i, j) pair, and each `pareto_compare` call invokes `ObjectiveSpace::as_minimization` twice — so for an N-point population that's 4·N·(N-1) fresh `Vec` allocations per sort. At N=100 with thousands of generations across the compare harness, this dominated the per-generation cost of every Pareto-based MOEA. Cache `as_minimization`/feasibility/violation once per individual up front, then inline the dominance test against those cached arrays. The output (per-pair dominance outcome and the per-i `dominates` lists) is bit-identical to `pareto_compare`. gungraun (instructions): - non_dominated_sort_2d n=50: 852 317 → 198 574 (-77 %, 4.3×) - non_dominated_sort_2d n=200: 13 513 271 → 2 601 813 (-81 %, 5.2×) Wall-clock (compare harness, 10-seed mean): - NSGA-II / ZDT1: 268 → 65 ms (4.1×) - NSGA-II / ZDT3: 267 → 65 ms (4.1×) - NSGA-II / DTLZ2: 344 → 106 ms (3.2×) - NSGA-II / Rastrigin: 260 → 71 ms (3.7×) - NSGA-III / DTLZ2: 318 → 122 ms (2.6×) - NSGA-III / DTLZ1: 303 → 122 ms (2.5×) - SMS-EMOA / DTLZ2: 1413 → 1369 ms (small additional win on top of HV) - AGE-MOEA / DTLZ1: 430 → 229 ms (1.9×, on top of the AGE-MOEA caching) - HypE / DTLZ2: 80 → 44 ms (1.8×) --- src/pareto/sort.rs | 72 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/src/pareto/sort.rs b/src/pareto/sort.rs index 19ea0e2..d030fed 100644 --- a/src/pareto/sort.rs +++ b/src/pareto/sort.rs @@ -2,7 +2,6 @@ use crate::core::candidate::Candidate; use crate::core::objective::ObjectiveSpace; -use crate::pareto::dominance::{Dominance, pareto_compare}; /// Partition the population into Pareto fronts by dominance rank. /// @@ -19,24 +18,79 @@ pub fn non_dominated_sort( return Vec::new(); } + // Precompute the per-individual feasibility, violation, and + // minimization-oriented objective vectors. The naïve formulation + // calls `pareto_compare` (and therefore `as_minimization`) twice for + // every pair, allocating two fresh Vecs per call; doing it once + // up front cuts that to one allocation per individual. + let feasible: Vec = population + .iter() + .map(|c| c.evaluation.is_feasible()) + .collect(); + let violation: Vec = population + .iter() + .map(|c| c.evaluation.constraint_violation) + .collect(); + let oriented: Vec> = population + .iter() + .map(|c| objectives.as_minimization(&c.evaluation.objectives)) + .collect(); + let m = objectives.len(); + let mut dominates: Vec> = vec![Vec::new(); n]; let mut dominated_by_count: Vec = vec![0; n]; let mut fronts: Vec> = Vec::new(); let mut first_front: Vec = Vec::new(); for i in 0..n { + let ai_feasible = feasible[i]; + let ai_violation = violation[i]; + let ai = &oriented[i]; for j in 0..n { if i == j { continue; } - match pareto_compare( - &population[i].evaluation, - &population[j].evaluation, - objectives, - ) { - Dominance::Dominates => dominates[i].push(j), - Dominance::DominatedBy => dominated_by_count[i] += 1, - _ => {} + let bi_feasible = feasible[j]; + let bi_violation = violation[j]; + // Inline the body of `pareto_compare`. We only care about + // `Dominates` vs `DominatedBy`; `Equal` and `NonDominated` + // are no-ops here. + let dominates_outcome = match (ai_feasible, bi_feasible) { + (true, false) => Some(true), // i dominates j + (false, true) => Some(false), // i is dominated + (false, false) => { + if ai_violation < bi_violation { + Some(true) + } else if ai_violation > bi_violation { + Some(false) + } else { + None + } + } + (true, true) => { + let bj = &oriented[j]; + let mut a_better_anywhere = false; + let mut b_better_anywhere = false; + for k in 0..m { + let av = ai[k]; + let bv = bj[k]; + if av < bv { + a_better_anywhere = true; + } else if av > bv { + b_better_anywhere = true; + } + } + match (a_better_anywhere, b_better_anywhere) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => None, + } + } + }; + match dominates_outcome { + Some(true) => dominates[i].push(j), + Some(false) => dominated_by_count[i] += 1, + None => {} } } if dominated_by_count[i] == 0 {