perf(pareto_archive): cache oriented + inline dominance checks in insert

`ParetoArchive::insert` calls `pareto_compare` twice per existing
member (once per pass), and each call re-allocates two Vec<f64>s
via `as_minimization` — 4N allocations per insert. Cache the
candidate's oriented + feasibility/violation once, build each
member's oriented vector once for the call, then inline the
dominance test against those cached arrays.

Used by PESA-II (per offspring per generation), PAES (per child),
ε-MOEA, and any user code working through the archive directly.

Wall-clock (compare harness, 10-seed mean):
- PESA-II / DTLZ2: 498 → 426 ms (-14 %)
- PESA-II / ZDT1:   87 →  75 ms (-14 %)

Smaller wins on PAES / MOPSO / IBEA / HypE / ε-MOEA where the
archive isn't the dominant per-generation cost.

Bit-identical via the compare harness.
This commit is contained in:
2026-05-05 13:28:38 -06:00
parent 060841d011
commit d60a3c3fe8
+115 -10
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};
/// A growable, dominance-pruned archive of candidates. /// A growable, dominance-pruned archive of candidates.
/// ///
@@ -33,19 +32,63 @@ impl<D: Clone> ParetoArchive<D> {
/// - Otherwise, drop existing members that the new candidate dominates, /// - Otherwise, drop existing members that the new candidate dominates,
/// then keep the new candidate. /// then keep the new candidate.
pub fn insert(&mut self, candidate: Candidate<D>) { pub fn insert(&mut self, candidate: Candidate<D>) {
for m in &self.members { // The naïve formulation calls `pareto_compare` twice per member
if matches!( // (once each pass), and `pareto_compare` re-allocates two
pareto_compare(&candidate.evaluation, &m.evaluation, &self.objectives), // Vec<f64>s via `as_minimization` per call → 4N allocations per
Dominance::DominatedBy | Dominance::Equal // insert. Cache the candidate's oriented + feasibility once, and
// each member's oriented once, then inline the dominance checks.
let n = self.members.len();
let m_dim = self.objectives.len();
let cand_oriented = self
.objectives
.as_minimization(&candidate.evaluation.objectives);
let cand_feasible = candidate.evaluation.is_feasible();
let cand_violation = candidate.evaluation.constraint_violation;
let member_oriented: Vec<Vec<f64>> = self
.members
.iter()
.map(|c| self.objectives.as_minimization(&c.evaluation.objectives))
.collect();
// First pass: bail if any existing member dominates-or-equals
// the candidate.
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let m_eval = &self.members[i].evaluation;
if member_dominates_or_equals(
&member_oriented[i],
m_eval.is_feasible(),
m_eval.constraint_violation,
&cand_oriented,
cand_feasible,
cand_violation,
m_dim,
) { ) {
return; return;
} }
} }
self.members.retain(|m| {
!matches!( // Second pass: drop existing members the candidate dominates.
pareto_compare(&candidate.evaluation, &m.evaluation, &self.objectives), let mut keep_mask = Vec::with_capacity(n);
Dominance::Dominates #[allow(clippy::needless_range_loop)]
) for i in 0..n {
let m_eval = &self.members[i].evaluation;
let cand_dominates_member = candidate_dominates_member(
&cand_oriented,
cand_feasible,
cand_violation,
&member_oriented[i],
m_eval.is_feasible(),
m_eval.constraint_violation,
m_dim,
);
keep_mask.push(!cand_dominates_member);
}
let mut idx = 0;
self.members.retain(|_| {
let keep = keep_mask[idx];
idx += 1;
keep
}); });
self.members.push(candidate); self.members.push(candidate);
} }
@@ -81,6 +124,68 @@ impl<D: Clone> ParetoArchive<D> {
} }
} }
/// Inline `pareto_compare(member, candidate, objectives) ∈ {Dominates, Equal}`
/// against the cached oriented + feasibility/violation values, returning the
/// boolean directly.
#[inline]
fn member_dominates_or_equals(
m_oriented: &[f64],
m_feasible: bool,
m_violation: f64,
c_oriented: &[f64],
c_feasible: bool,
c_violation: f64,
m_dim: usize,
) -> bool {
match (m_feasible, c_feasible) {
(true, false) => true,
(false, true) => false,
(false, false) => m_violation <= c_violation,
(true, true) => {
let mut c_better = false;
for k in 0..m_dim {
if c_oriented[k] < m_oriented[k] {
c_better = true;
break;
}
}
!c_better
}
}
}
/// Inline `pareto_compare(candidate, member, objectives) == Dominates`.
#[inline]
fn candidate_dominates_member(
c_oriented: &[f64],
c_feasible: bool,
c_violation: f64,
m_oriented: &[f64],
m_feasible: bool,
m_violation: f64,
m_dim: usize,
) -> bool {
match (c_feasible, m_feasible) {
(true, false) => true,
(false, true) => false,
(false, false) => c_violation < m_violation,
(true, true) => {
let mut c_better_anywhere = false;
let mut m_better_anywhere = false;
for k in 0..m_dim {
let cv = c_oriented[k];
let mv = m_oriented[k];
if cv < mv {
c_better_anywhere = true;
} else if cv > mv {
m_better_anywhere = true;
}
}
c_better_anywhere && !m_better_anywhere
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;