perf(operators): make CycleCrossover O(n) with per-parent position tables (59K -> 12K instr)

cx_child located each cycle's next value with an O(n) `position` scan,
making the walk O(n^2). CX operates on strict permutations of 0..n, so a
direct value-indexed position table per parent gives O(1) lookups; a
debug_assert documents the range assumption (mirroring ERX).

cycle_crossover_vary n=100: 58_762 -> 12_084 (-79%, 4.86x); n=30 -36%.
Bit-identical for valid permutations -- all 606 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 06:41:05 -06:00
co-authored by Claude Opus 4.7
parent 4aab5c7029
commit 47939fc5cb
+18 -8
View File
@@ -527,15 +527,29 @@ fn cx_child(start_parent: &[usize], other_parent: &[usize]) -> Vec<usize> {
let n = start_parent.len(); let n = start_parent.len();
let mut child = vec![0_usize; n]; let mut child = vec![0_usize; n];
let mut visited = vec![false; n]; let mut visited = vec![false; n];
// Position lookup per parent: `pos_*[value]` is the index of that value.
// CX operates on strict permutations of `0..n` (see the type docs), so a
// direct-indexed table is valid and turns the per-step value lookup from
// an O(n) `position` scan into an O(1) index.
let mut pos_start = vec![0_usize; n];
let mut pos_other = vec![0_usize; n];
for (i, (&s, &o)) in start_parent.iter().zip(other_parent.iter()).enumerate() {
debug_assert!(
s < n && o < n,
"CycleCrossover requires strict permutations of 0..n",
);
pos_start[s] = i;
pos_other[o] = i;
}
let mut cycle_index = 0_usize; let mut cycle_index = 0_usize;
for seed in 0..n { for seed in 0..n {
if visited[seed] { if visited[seed] {
continue; continue;
} }
let (from, switch_through) = if cycle_index % 2 == 0 { let (from, switch_through, from_pos) = if cycle_index % 2 == 0 {
(start_parent, other_parent) (start_parent, other_parent, &pos_start)
} else { } else {
(other_parent, start_parent) (other_parent, start_parent, &pos_other)
}; };
let mut k = seed; let mut k = seed;
loop { loop {
@@ -545,11 +559,7 @@ fn cx_child(start_parent: &[usize], other_parent: &[usize]) -> Vec<usize> {
visited[k] = true; visited[k] = true;
child[k] = from[k]; child[k] = from[k];
let next_val = switch_through[k]; let next_val = switch_through[k];
let next_pos = from k = from_pos[next_val];
.iter()
.position(|&x| x == next_val)
.expect("CycleCrossover: parents must share the same value multiset");
k = next_pos;
} }
cycle_index += 1; cycle_index += 1;
} }