Skip to content

Commit 926097c

Browse files
feschberclaude
andcommitted
add --repeat, to profile a 100ms computation without process noise
`calculate-all` runs in ~100ms, which is a thin window to sample: at 9999 Hz over 16 threads one run yields ~14k samples. Repeating the *process* to get more charges every iteration for startup, first-touch faulting ~40 MB, and mimalloc's teardown purge - and those are not free, they are ~2-3% of a run and all of it noise relative to the loops one is trying to measure. `process exit` alone was 0.86% of the last profile, essentially all of it the purge. Repeating in-process keeps the allocator and page tables warm instead. Measured steady state is ~2-3% under separate processes of the same binary, 93-99 vs 98-105 ms, which is that overhead going away. Warm-up is two iterations, not one - worth stating because "discard the first" is the natural assumption and it is wrong here. Across repeated 10-12 iteration runs the opening pair came in at 111/134 and 101/120 ms against a 93-99 steady state, so the second is reliably the slowest, not the first. The per-iteration internal timings are logged at `RUST_LOG=info` so they can be checked rather than assumed. `black_box` on each iteration's result so nothing about the repetition lets the optimizer decide later iterations are redundant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8593f38 commit 926097c

1 file changed

Lines changed: 27 additions & 2 deletions

File tree

src/main.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ struct Args {
1515
/// number of threads to use for all solutions
1616
#[arg(short, long)]
1717
threads: Option<NonZero<usize>>,
18+
/// repeat `calculate-all` this many times in one process
19+
///
20+
/// For profiling and benchmarking a computation that takes ~100ms: sampling one
21+
/// run yields few samples, and repeating the *process* instead charges every
22+
/// iteration for startup, first-touch faulting ~40MB, and mimalloc's teardown
23+
/// purge - which together are a couple of percent of a run, all of it noise
24+
/// relative to the loops one is usually trying to measure. Repeating in-process
25+
/// keeps the allocator and page tables warm, so iterations after the first
26+
/// measure steady state.
27+
///
28+
/// Each iteration logs its own internal timing at `RUST_LOG=info`. Discard the
29+
/// first *two*: measured over repeated 10-12 iteration runs, they come in around
30+
/// 111/134 and 101/120 ms against a steady state of 93-99, so the warm-up is
31+
/// two iterations rather than one. Steady state does come out ~2-3% under
32+
/// separate processes of the same binary (93-99 vs 98-105 ms), which is the
33+
/// startup and teardown this exists to stop paying.
34+
#[arg(short, long, default_value_t = 1)]
35+
repeat: usize,
1836
/// subcommands
1937
#[command(subcommand)]
2038
command: Option<Command>,
@@ -93,8 +111,15 @@ fn main() {
93111
}
94112
match command {
95113
Command::CalculateAll => {
96-
let vec = solitaire_solver::calculate_feasible_set(args.threads);
97-
println!("solutions: {}", vec.len());
114+
// `black_box` so nothing about the repetition lets the optimizer
115+
// conclude that later iterations are redundant, and so the result
116+
// has to be materialized rather than folded into a length
117+
let mut solutions = 0;
118+
for _ in 0..args.repeat.max(1) {
119+
let vec = solitaire_solver::calculate_feasible_set(args.threads);
120+
solutions = std::hint::black_box(&vec).len();
121+
}
122+
println!("solutions: {solutions}");
98123
}
99124
Command::CalculateAllNaive => {
100125
solitaire_solver::calculate_all_solutions_naive();

0 commit comments

Comments
 (0)