Skip to content

Commit 2c8d19f

Browse files
feschberclaude
andcommitted
pre-size the pagoda parallel filter instead of growing via push
A profile with symbols/frame-pointers preserved and LTO disabled (RUSTFLAGS="-C target-cpu=native -C force-frame-pointers=yes", CARGO_PROFILE_RELEASE_{STRIP,DEBUG,LTO} overridden for the build - the committed profile strips symbols and enables thin LTO, both of which make perf traces hard to trust: stripped binaries collapse everything into one opaque module frame, and LTO's cross-crate inlining can misattribute samples to the wrong function) showed finish_grow as a real, avoidable cost in the two par::parallel(...).filter(...).collect() calls added for pagoda pruning: each chunk's output Vec grew via repeated reallocation as matches were found, instead of being sized once up front - the same category of fix already applied in board.rs's possible_moves/possible_reverse_moves and keyset.rs's extraction. Added par::par_filter, a parallel filter that counts matches per chunk first (cheap now that pagoda() is O(1)) and allocates each chunk's buffer at its exact final size. Re-profiled the same way after the fix: finish_grow no longer appears in the profile at all. Wall-clock effect (hyperfine, native-cpu build, 50 runs): pagoda pruning is now genuinely neutral rather than a net loss (mean ratio 1.00 +/- 0.03 vs the pre-pagoda commit) - correctness-preserving and essentially free, even though it doesn't deliver a net win on this implementation's current bottleneck (see the previous commit). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent f4f7fc4 commit 2c8d19f

2 files changed

Lines changed: 25 additions & 4 deletions

File tree

solitaire-solver/src/feasible.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,8 @@ pub fn calculate_feasible_set(threads: Option<NonZero<usize>>) -> Vec<Board> {
281281
// filter instead of Vec::retain (single-threaded) - up to ~2.6M elements
282282
// on the biggest rounds is enough for that gap to matter on its own.
283283
let solved_weight = crate::pagoda::pagoda(Board::solved());
284-
let constellations = par::parallel(&constellations, threads, |chunk| {
285-
chunk.iter().copied().filter(|&b| crate::pagoda::pagoda(b.inverse()) >= solved_weight).collect()
284+
let constellations = par::par_filter(&constellations, threads, |&b| {
285+
crate::pagoda::pagoda(b.inverse()) >= solved_weight
286286
});
287287

288288
let deduped = constellations.len();
@@ -347,8 +347,8 @@ pub fn calculate_feasible_set(threads: Option<NonZero<usize>>) -> Vec<Board> {
347347
// Parallel filter instead of Vec::retain (single-threaded) - this is
348348
// exactly the biggest round in the whole algorithm, up to ~3M elements.
349349
let solved_weight = crate::pagoda::pagoda(Board::solved());
350-
let constellations = par::parallel(&constellations, threads, |chunk| {
351-
chunk.iter().copied().filter(|&b| crate::pagoda::pagoda(b) >= solved_weight).collect()
350+
let constellations = par::par_filter(&constellations, threads, |&b| {
351+
crate::pagoda::pagoda(b) >= solved_weight
352352
});
353353
let deduped = constellations.len();
354354

solitaire-solver/src/par.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ where
9696
par_join(&par_map_chunks(states, nthreads, f))
9797
}
9898

99+
/// filters `items` in parallel, pre-sizing each chunk's output buffer exactly
100+
/// once instead of growing it via repeated reallocation as matches are found -
101+
/// same reasoning as the capacity precounting already used in `board.rs`'s
102+
/// `possible_moves`/`possible_reverse_moves` and `keyset.rs`'s extraction: a
103+
/// plain `chunk.iter().filter(pred).collect()` per chunk showed up as real,
104+
/// avoidable allocator cost (`finish_grow`) once `pred` got cheap enough that
105+
/// the buffer growth was no longer negligible next to it.
106+
pub(crate) fn par_filter<T, F>(items: &[T], nthreads: usize, pred: F) -> Vec<T>
107+
where
108+
T: Copy + Send + Sync,
109+
F: Fn(&T) -> bool + Send + Sync,
110+
{
111+
let chunks: Vec<Vec<T>> = par_map_chunks(items, nthreads, |chunk| {
112+
let count = chunk.iter().filter(|x| pred(x)).count();
113+
let mut out = Vec::with_capacity(count);
114+
out.extend(chunk.iter().copied().filter(|x| pred(x)));
115+
out
116+
});
117+
par_join(&chunks)
118+
}
119+
99120
pub(crate) trait ParDedup {
100121
fn par_dedup(self, n_threads: usize) -> Self;
101122
}

0 commit comments

Comments
 (0)