Quant developer HFT course · Module 1: Reading hot code
Benchmarking without fooling yourself
The question
You rewrote a function and your benchmark says it is three times faster. What would make you doubt that number?
The benchmark that measures nothing
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < 1'000'000; ++i) {
double r = price_of(i); // result never used
}
auto t1 = std::chrono::steady_clock::now();
With optimisation on, the compiler sees that r is never used and price_of has no side effects, and deletes the loop. The benchmark reports a time close to zero, and a rewrite that happens to be harder for the compiler to delete looks slower. This is the most common way a benchmark lies, and it is the first thing an interviewer will look for in yours.
The fix is to make the result observable. Benchmark libraries provide a barrier for this: benchmark::DoNotOptimize(r) in Google Benchmark tells the compiler the value is used without adding real work. Accumulating results into a value you print at the end works too, if less precisely.
Other ways to measure the wrong thing
Cold start. The first iterations pay for page faults on freshly allocated memory, cold caches and an untrained branch predictor. Time only a few runs and you measure start-up.
Timer overhead. Reading a clock costs tens of nanoseconds through the usual system interfaces. Timing a single 30-nanosecond operation measures mostly the clock. Time a batch and divide, or use the processor's cycle counter carefully (Module 10).
Constant inputs. Calling with the same input every time lets the branch predictor learn it perfectly and keeps everything in L1. Real input is not like that. Use realistic data: a recorded feed, or a generated distribution with the same shape.
Frequency scaling and noise. CPU clock speed changes with load and temperature. Other processes share the cores and caches. Pin the benchmark to an isolated core, fix the frequency if you can, and run several repetitions.
Reporting one number. A mean hides the distribution. For latency work the percentiles are the result (Module 8): median, 99th, 99.9th, and the maximum.
Comparing different work. If the new version computes something slightly different, or skips a case, the speed-up is not a speed-up.
A benchmark you can defend
static void BM_Scan(benchmark::State& state) {
auto quotes = load_recorded_quotes("feed_sample.bin"); // realistic data, loaded once
Strategy s;
for (auto _ : state) {
scan(quotes, 4711, s);
benchmark::DoNotOptimize(s);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(state.iterations() * quotes.size());
}
BENCHMARK(BM_Scan)->Repetitions(10)->ReportAggregatesOnly(true);
The library handles warm-up and runs enough iterations for a stable figure. You supply realistic data, keep the result alive, and report the spread across repetitions.
Then check the result some other way. Look at the generated assembly to confirm the work is still there. Run under perf stat to see whether the instruction count, cache misses or branch misses changed in the direction your explanation says they should. A speed-up you can explain from the counters is one an interviewer will believe.
Microbenchmarks and the real system
A microbenchmark puts one function in ideal conditions: hot caches, no other threads, no interrupts. In production the same function runs between network reads, after other code has evicted its data. A change can win in isolation and lose in place. The final word comes from measuring the system end to end on realistic traffic, which is Module 8's subject.
Check yourself
Question 1 of 3
Your benchmark of an optimised build reports 0.3 ns per call for a function that does a hash lookup. What is the most likely explanation?
A hash lookup cannot complete in about one cycle. A result that implausible almost always means the work was eliminated as dead code.
Question 2 of 3 · choose every correct option
Which of these makes a benchmark more trustworthy? Choose all that apply.
Warm-up removes start-up costs, realistic input keeps caches and predictors honest, and repetitions show the noise. One call is below the resolution of what the clock read can tell you.
Question 3 of 3
A change is 30% faster in a microbenchmark but no faster in the full system. Which explanation is plausible?
In isolation the function runs with hot caches and trained predictors. In place, other code competes for both, so a gain in pure computation can disappear behind memory stalls.
In the interview
Say your own answer out loud first, then open the one we would give.
How would you benchmark a small C++ function?
With a benchmark library that handles warm-up and iteration counts, on realistic input, with the result kept alive so the compiler cannot delete the work. I would pin it to a quiet core, run several repetitions, report the spread, and check the generated code and the performance counters to confirm the work and the explanation.
What is the most common mistake in microbenchmarks?
Letting the optimiser remove the code being measured, because the result is unused or the input is a constant it can fold. After that: timing cold start, measuring single calls with a clock that costs more than the call, and reporting only the mean.
Your benchmark shows a speed-up. How do you convince a sceptical colleague?
Show that the work is the same, show the distribution rather than one number, and explain the gain with counters: fewer instructions, fewer cache misses, or fewer branch misses, matching what the change was meant to do. Then confirm it in an end-to-end measurement on recorded traffic.