Performance Sprint & Report
Hit the targets, then write it up like a pro.
Performance · Measure, Profile, Then Go Fast
Why this week
For thirteen weeks you've collected techniques: honest measurement, profiling, allocation-free paths, cache-friendly books, SIMD, pinning, static dispatch, hot/cold splitting and latency budgets. This week you use them the way a performance team does, in a time-boxed sprint against fixed targets. You run one hypothesis per experiment, log every result including the failures, take measurements someone else could reproduce, put a regression gate in place so the gains survive next month's feature work, and finish with a report.
The report matters as much as the speedup. "I made the engine 2× faster" is a claim. A report with the environment, the methodology, distributions, what was tried, what didn't work and how to reproduce it is evidence. It's what you'll walk an interviewer through in weeks 49–51, and it's how professional teams decide whether to trust a number at all.
Time-box the sprint. Fix the targets and the end date on Day 1, then stop when either runs out. Optimization has no natural end, and the last 5% usually costs more than the first 50%. Knowing when to stop is part of the skill.
Day 1 — Choose targets from evidence
What to attack, and how much it can possibly give
Profiles, budgets and Amdahl's ceiling
Begin with three inputs, not ideas. First, last week's budget table: which stages are over budget, at which percentile. Second, fresh profiles of the current build under realistic load (week 26): on-CPU flame graphs plus the counters that explain them (IPC, cache misses, branch misses). Third, the targets: the curriculum's numbers below, plus any of your own.
Then do the arithmetic before the work. Amdahl's law: if a part takes fraction f of the time and you make it s times faster, the whole speeds up by 1 / ((1 − f) + f/s). The ceiling, if that part became free, is 1 / (1 − f). A stage that takes 35% of the path can never give more than a 1.54× overall speedup, however clever the change.
Be careful about which time f is a fraction of:
- A CPU flame graph is weighted by samples, so it shows where time goes on average. That's the right f for throughput.
- For p99 latency, f is the stage's share of the slow orders' time, which is what your week 39 attribution measures. Rare stalls such as page faults, interrupts and queue waits can dominate the tail while barely showing up in a sampled profile, and off-CPU waits don't show up in an on-CPU profile at all.
- In a pipeline where stages run concurrently on different cores, throughput is set by the slowest stage and latency is the sum. Speeding up a stage that isn't the bottleneck reduces latency but leaves throughput exactly where it was.
Chapter 2 for methodology: the USE method, workload characterization, drill-down analysis. Chapter 12 (Benchmarking) for the failure modes of benchmarks. Read the sections on benchmarking mistakes and active benchmarking slowly; they're your checklist for Day 3.
Revisit the CPU and off-CPU flame graph pages. Your sprint needs both: on-CPU for throughput, off-CPU (or week 39's stage stamps) for the waits that make up tail latency.
Your flame graph says matching is 60% of CPU samples. Why might making matching 2× faster barely move the end-to-end p99?
Write your sprint charter: the 2–4 metrics you will move (with percentile and load), the current value, the target, the theoretical ceiling for each from Amdahl, and the end date. Then list the first three hypotheses in priority order, with the evidence behind each.
Day 2 — Amdahl, in code
Speedup arithmetic
Serial fractions, required speedups and pipeline bottlenecks
Encode the arithmetic you'll use all week. Two questions come up constantly. "How much faster must this part get for the whole to hit the target?" Sometimes the honest answer is no finite amount. And "if I speed up this stage, does throughput move at all?"
Amdahl's law for your pipeline
#[derive(Clone, Copy, Debug)]
pub struct Part {
/// Fraction of the ORIGINAL time spent in this part (0.0..=1.0).
pub fraction: f64,
/// How much faster this part became (2.0 = twice as fast).
pub speedup: f64,
}
/// Amdahl's law: overall = 1 / ((1 − Σ fraction) + Σ fraction / speedup).
/// Time not covered by any part is unchanged.
pub fn amdahl(parts: &[Part]) -> f64 {
todo!()
}
/// The ceiling: overall speedup if a part with `fraction` became
/// infinitely fast. fraction == 1.0 → f64::INFINITY.
pub fn max_speedup(fraction: f64) -> f64 {
todo!()
}
/// How much faster must the part with `fraction` become for the whole to
/// reach `target` overall? None if no finite speedup gets there —
/// including exactly at the ceiling (compare with a small epsilon).
pub fn required_speedup(fraction: f64, target: f64) -> Option<f64> {
todo!()
}
/// Ops/s of a pipeline whose stages run concurrently, one per core,
/// each taking `stage_ns` per item: the slowest stage sets the rate.
pub fn pipeline_throughput(stage_ns: &[f64]) -> f64 {
todo!()
}
/// Latency of one item through the same pipeline (no queueing): the sum.
pub fn pipeline_latency(stage_ns: &[f64]) -> f64 {
todo!()
}
Look closely at one case in the drill: a stage that is 35% of the path needs a 21× speedup for the whole to reach 1.5×. When the required speedup comes out like that, the honest conclusion is that this stage won't get you there and you need to change the architecture or the target.
Matching is 40% of the per-order time and you want 2× overall from optimizing matching alone. Is that possible?
Day 3 — The experiment log and reproducible benchmarks
One hypothesis per experiment
Log everything, especially the failures
Each experiment gets an entry before you write any code:
# example entry: the numbers are illustrative
E-07 2026-10-12 commit a1b2c3d (base) → e4f5g6h
hypothesis order-id → slot lookup via slab index instead of HashMap
removes ~40 ns per cancel at p99 (profile: 18% in hashing)
change book::index uses OrderId as slab key; ids are dense per session
command cargo xtask bench --week 27 (20 runs per variant, ABAB)
environment EPYC 7543P, isolcpus=2-7, governor=performance, boost off
result w27.add_cancel_slab p99: 212 → 168 ns (median of 20 runs each)
w40.engine_throughput: 5.1M → 5.4M ops/s
verdict KEEP
notes p99.9 unchanged — tail is elsewhere (see E-05 attribution)Change one thing at a time. Revert the losers, and record that you tried them. A sprint log where every entry says KEEP is a log someone edited afterwards.
Reproducibility checklist. Most benchmark disagreements come from the setup, not the code:
- Build. Same release profile every time (
lto,codegen-units = 1,panic = "abort"from week 22). If you use-C target-cpu=native, say so: the binary won't run on older CPUs, and the comparison is only valid on the same microarchitecture. - Machine state. Pinned threads on isolated cores (week 30), a fixed frequency governor, and turbo either disabled or noted. With turbo on, a short benchmark runs faster than a long one and results drift with temperature and with load on other cores. Nothing else running. Laptops plugged in.
- Runs, not samples. Samples within one run are correlated: same heap layout, same code alignment, same thermal state. The unit of evidence is the run. Take 10–30 runs per variant and interleave them A, B, A, B so slow drift hits both equally.
- Fixed seeds and warmup. The same order stream every time (
types::Rngwith a fixed seed), and discard the warmup. - Layout luck. Moving unrelated code can shift a hot loop's alignment and swing results by several percent. If a 3% "win" disappears when you add a function somewhere else, it was never a win.
Re-read the Benchmarking and Build Configuration chapters with your sprint in mind. Every build setting you change belongs in the experiment log as its own experiment, not bundled with a code change.
Shows that seemingly irrelevant setup details, such as the size of the UNIX environment and the link order, can bias performance measurements enough to flip conclusions. This is the paper to cite when someone asks why you randomize or vary your setup and report spread across runs.
Set up your experiment log (a file in the repo is fine) and write entries for your first three hypotheses before touching code. For each: the evidence, the predicted effect with a number, and the measurement that would prove you wrong.
Day 4 — Is it really slower?
Regression detection
Two gates: statistically real and practically large
Without a regression gate, a sprint's gains wear away within months. The gate has to answer a hard question on noisy data: did this change make the benchmark worse, or is this just run-to-run variation? Two separate tests have to pass before you call something a regression:
- Statistical: the candidate's runs really are shifted relative to the baseline's. Benchmark distributions are skewed and often bimodal, so use a test that doesn't assume normality. Mann–Whitney U counts how often a candidate run beats a baseline run. Under "no change" that's about half the pairs, and the normal approximation turns the count into a z-score.
- Practical: the shift is big enough to matter, say ≥2% on the median. With very quiet runs, a 0.3% change can be statistically certain and still not worth anyone's attention.
Criterion.rs uses the same two ideas. It has a configurable significance level (default 0.05) and a noise threshold (default 1%), and reports a change only when both are crossed.
Detect a regression statistically
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verdict {
Regression,
Improvement,
NoChange,
}
/// Median of an UNSORTED slice (mean of the two middle values when the
/// length is even). Empty → 0.0.
pub fn median(xs: &[f64]) -> f64 {
todo!()
}
/// Mann–Whitney U for `a` versus `b`: the number of pairs (a_i, b_j) with
/// a_i > b_j, counting ties as 0.5. Returns (U, z) where z is the normal
/// approximation (no tie correction):
/// z = (U − n_a·n_b / 2) / sqrt(n_a·n_b·(n_a + n_b + 1) / 12)
/// z > 0 means `a` tends to be LARGER than `b`.
pub fn mann_whitney(a: &[f64], b: &[f64]) -> (f64, f64) {
todo!()
}
/// Compare per-run results of one benchmark (e.g. 30 runs each).
/// `lower_is_better`: true for latency, false for throughput.
/// Regression ⇔ the candidate is worse on BOTH gates:
/// statistically: |z| ≥ z_crit in the bad direction, and
/// practically: the median moved the bad way by ≥ min_effect
/// (relative to the baseline median; 0.02 = 2%).
/// Improvement ⇔ the same two gates in the good direction.
/// Otherwise NoChange.
pub fn verdict(
baseline: &[f64],
candidate: &[f64],
lower_is_better: bool,
min_effect: f64,
z_crit: f64,
) -> Verdict {
todo!()
}Look at the "very noisy" case in the drill. The medians differ by about 5%, yet the test says no change, because with that much spread a 5% gap in medians happens by chance all the time. If you compare two single runs, you'll "find" regressions like this every week.
Your report checks 20 benchmarks, each at a 5% significance level, and nothing actually changed. How likely is at least one false regression alarm?
Day 5 — Write the report
A performance report someone else can trust
Structure, honesty, reproducibility
Write the report so that a sceptical colleague could reproduce every number in it. A structure that works:
- Summary. One table: metric (with percentile and load), before, after, target, met or missed. Then two sentences on what made the difference.
- Environment. CPU model and microcode, core count and SMT state, kernel and boot parameters (isolation,
nohz_full), governor and turbo state, compiler version and profile, commit hashes. - Methodology. Open- or closed-loop and at what offered rate, warmup, run length, number of runs, how runs were interleaved, the statistical gate.
- Results. Distributions, not single numbers: percentile plots (p50 → p99.99) or histograms for before and after, with the max and sample counts.
- What changed. The KEEP experiments in order, each with its measured contribution. If contributions overlap, say so rather than adding them up.
- What didn't work. The REVERT experiments and what you learned from them. Reviewers trust this section most.
- Threats to validity. What could make these numbers wrong: a single machine, synthetic order flow, a benchmark shape that flatters the ladder book, turbo.
- How to reproduce. Exact commands.
Make every comparison like for like: the same machine, load, percentile and build settings. Never put your p50 next to someone else's p99, or a closed-loop number next to an open-loop one. Don't quote your best run. Give the median run, the spread, and N.
Write the report's summary table and the two-sentence conclusion now, with placeholders for numbers. Writing the conclusion first forces you to decide what the sprint is trying to show.
Day 6 — Close the sprint
Retrospective
What you'd do differently with another week
Sprint retrospective: which hypothesis had the best evidence-to-result ratio, which looked promising and failed, and what you would measure first if you had one more week. Name one thing about your measurement setup you no longer trust.
Why should throughput and tail latency come from separate benchmark runs?
Capstone
Assemble engine v3 in crates/engine. Use the fastest configuration you can justify from weeks 25–39: slab storage for orders (week 27), the ladder book behind the same trait (week 28), static dispatch over book and risk policy (week 37), and rejects and rare order types moved to cold functions (week 38). Keep the choices selectable at compile time so you can still A/B each one. A configuration that can't be switched back can't be measured against the alternative.
In the repo this lives in engine::perf (src/perf.rs). FastestEngine is FastEngine<LadderBook, NoRisk>: week 38's split engine over week 28's ladder, statically dispatched, with no risk policy. fastest_engine(config) builds one with a ladder window 4,096 ticks wide per side and room for config.expected_orders. Both are provided, as the configuration that measured fastest on the reference machine, so change them if your own measurements disagree. There's no week 27 slab in it: the ladder keeps its orders in plain arenas with free lists, because it hands out no keys, and a generation check would only add a cache line to every random cancel. Since FastEngine is generic over book and policy, each alternative is still one type parameter away.
Correctness before speed. Before any benchmark counts, show that v3 is behaviourally identical to the engine you trusted. Feed both the same seeded order streams and compare the full event sequences and the final state hash (week 9's replay hash and week 12's differential test). A faster engine that emits a different event sequence has a bug, however good its numbers are. In the repo, w40_fastest_configuration_is_the_same_engine runs fastest_engine against Engine with the differential on 300 randomized week 12 streams. It then pushes 300,000 mixed commands through both and requires no allocations after warm-up and the same final state hash.
The regression gate. Extend your report tooling to compare the current results with the previous report, benchmark by benchmark. Use per-run values (not one aggregate) and today's two-gate rule. Fail loudly on a regression. Make accepting a new baseline an explicit act that's recorded in the report, never something that happens quietly.
The repo's piece of this is perf::regressions(old, new, tolerance), and it implements the practical gate only. A report holds one result per benchmark, not per-run values, so there's nothing for Mann–Whitney to rank. It reads the JSON that cargo xtask report writes or the BENCHJSON lines a bench binary prints (parse_benches is provided). For each benchmark with a result in old, it compares p50 and p99 for latencies (p99.9 is too noisy for an automatic gate) and the headline value for everything else. Lower is better for ns, us and bytes, higher for rates and ratios.
- A relative change worse than
toleranceisRegression::Worse { id, metric, old, new, by }. - A benchmark that failed or disappeared in
newisRegression::Missing { id }. - Benchmarks only in
new, benchmarks whose unit changed and old values ≤ 0 are skipped. - The result is sorted by id, then metric.
Set the tolerance above the run-to-run spread you measured on Day 3. For the statistical gate on top, keep several reports per side and apply the drill's test to each metric across them.
Reading the targets. The throughput target is a statement about mean cost per order on one thread. At 15M orders/s, each order gets about 67 ns on average, including the book update and event emission. That leaves no room for a hash map lookup on a cold line or for an allocation. The latency target is a p99 for submit → events, which is a statement about the tail. You can meet the throughput target with an engine whose p99 still suffers from rare slow paths, and the reverse. Treat them as two problems with two measurement setups. In the repo both w40 benchmarks run fastest_engine on week 7's mixed flow on one thread. w40.engine_throughput runs a million commands through it, and w40.match_latency times one command at a time. That makes them directly comparable with w07.* and w38.match_latency.
Pitfalls:
- Tuning for the benchmark: a ladder sized exactly to the bench's price range, or a seed that happens to avoid deep sweeps. Run at least one benchmark with a different seed and price range, and report it.
- Short runs with turbo on. They flatter whatever runs first.
- Missing
black_box, so the optimizer deletes the work you think you're measuring. - Improving one metric while quietly regressing another. The gate has to cover every benchmark id, not just the one you're working on.
crates/engineEngine v3 and the Phase 3 performance report
The fastest engine configuration from weeks 25–39, behaviourally identical to the reference engine on seeded order streams, guarded by a tolerance-based regression check against your previous report, and written up in a reproducible performance report.
cargo xtask test --week 40 # tests named w40_*
cargo xtask bench --week 40 # p50 / p99 / p99.9 on your machine
cargo xtask report --upload # update this page and your lab- All week 40 tests pass w40_*
- Final engine throughput w40.engine_throughputtarget ≥ 15.0M ops/s
- Final submit→events latency w40.match_latencytarget p99 ≤ 300 ns
Feeds into
- Week 49 draws on this sprint for system design interviews: you'll present latency budgets and capacity estimates backed by numbers you measured yourself.
- Week 51 makes this performance report the centrepiece of your portfolio. The environment, methodology and "what didn't work" sections are what make it credible.