perf(pareto): precompute oriented buffers in pareto_front

`pareto_front` was still the naive O(n²) formulation: a raw double loop
calling `pareto_compare` for every ordered pair, re-deriving feasibility
and the minimization-oriented objective values on every comparison.

Apply the same precompute pattern `non_dominated_sort` already uses:
hoist per-individual `feasible` / `violation` / `oriented` (a flat n*m
buffer) out of the loop, then run a branchless inlined dominance check
over the contiguous buffer. The kept set and its order are unchanged.

Whole-program callgrind Ir for the `compare_profile` benchmark:
221,836,742,708 -> 173,803,642,945 (-21.65%); `pareto_front` self-Ir
92.1B -> 44.1B.
This commit is contained in:
2026-05-14 12:15:04 -06:00
parent 50501bb01e
commit 2bd8f8fc11
+55 -8
View File
@@ -2,7 +2,6 @@
use crate::core::candidate::Candidate; use crate::core::candidate::Candidate;
use crate::core::objective::ObjectiveSpace; use crate::core::objective::ObjectiveSpace;
use crate::pareto::dominance::{Dominance, pareto_compare};
/// Return all candidates that are not dominated by any other candidate. /// Return all candidates that are not dominated by any other candidate.
/// ///
@@ -31,20 +30,68 @@ pub fn pareto_front<D: Clone>(
population: &[Candidate<D>], population: &[Candidate<D>],
objectives: &ObjectiveSpace, objectives: &ObjectiveSpace,
) -> Vec<Candidate<D>> { ) -> Vec<Candidate<D>> {
let n = population.len();
if n == 0 {
return Vec::new();
}
// Precompute per-individual feasibility, violation, and the
// minimization-oriented objective vectors once, mirroring
// `non_dominated_sort`. The naïve formulation called `pareto_compare`
// (and therefore `as_minimization`) for every ordered pair, re-deriving
// all of this on every comparison; precomputing turns the O(n²) inner
// loop into a branchless scan over a contiguous buffer.
let feasible: Vec<bool> = population
.iter()
.map(|c| c.evaluation.is_feasible())
.collect();
let violation: Vec<f64> = population
.iter()
.map(|c| c.evaluation.constraint_violation)
.collect();
let m = objectives.len();
let mut oriented: Vec<f64> = Vec::with_capacity(n * m);
for c in population {
oriented.extend_from_slice(&objectives.as_minimization(&c.evaluation.objectives));
}
let mut out = Vec::new(); let mut out = Vec::new();
'outer: for (i, a) in population.iter().enumerate() { 'outer: for i in 0..n {
for (j, b) in population.iter().enumerate() { let ai_feasible = feasible[i];
let ai_violation = violation[i];
let ai = &oriented[i * m..i * m + m];
for j in 0..n {
if i == j { if i == j {
continue; continue;
} }
if matches!( // `i` is kept only if no `j` dominates it — i.e. no `j` for which
pareto_compare(&a.evaluation, &b.evaluation, objectives), // `pareto_compare(a_i, a_j)` would be `DominatedBy`. This inlines
Dominance::DominatedBy // exactly that one outcome of `pareto_compare`.
) { let dominated_by_j = match (ai_feasible, feasible[j]) {
(true, false) => false,
(false, true) => true,
(false, false) => ai_violation > violation[j],
(true, true) => {
let aj = &oriented[j * m..j * m + m];
let mut a_better_anywhere = false;
let mut b_better_anywhere = false;
for k in 0..m {
let av = ai[k];
let bv = aj[k];
if av < bv {
a_better_anywhere = true;
} else if av > bv {
b_better_anywhere = true;
}
}
b_better_anywhere && !a_better_anywhere
}
};
if dominated_by_j {
continue 'outer; continue 'outer;
} }
} }
out.push(a.clone()); out.push(population[i].clone());
} }
out out
} }