From 47939fc5cb3780e71d23a63e3a348800e480afb2 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 14 May 2026 06:39:50 -0600 Subject: [PATCH] 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) --- src/operators/permutation.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/operators/permutation.rs b/src/operators/permutation.rs index 8c11db4..31eb652 100644 --- a/src/operators/permutation.rs +++ b/src/operators/permutation.rs @@ -527,15 +527,29 @@ fn cx_child(start_parent: &[usize], other_parent: &[usize]) -> Vec { let n = start_parent.len(); let mut child = vec![0_usize; 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; for seed in 0..n { if visited[seed] { continue; } - let (from, switch_through) = if cycle_index % 2 == 0 { - (start_parent, other_parent) + let (from, switch_through, from_pos) = if cycle_index % 2 == 0 { + (start_parent, other_parent, &pos_start) } else { - (other_parent, start_parent) + (other_parent, start_parent, &pos_other) }; let mut k = seed; loop { @@ -545,11 +559,7 @@ fn cx_child(start_parent: &[usize], other_parent: &[usize]) -> Vec { visited[k] = true; child[k] = from[k]; let next_val = switch_through[k]; - let next_pos = from - .iter() - .position(|&x| x == next_val) - .expect("CycleCrossover: parents must share the same value multiset"); - k = next_pos; + k = from_pos[next_val]; } cycle_index += 1; }