On Zen 5, a hashtable benchmark's result depends on where its timed loop
lands in memory. With a fixed hash seed, `BM_MapContainsMiss<Map<int,
int>>/64` takes 5.5-6.3 cycles per lookup at 8 of the 16 possible loop
positions within 256 bytes, and 6.7-7.1 at the other 8, through effects
in the op cache, the BTB, and the branch predictor. Which position a
build gets is an accident of the surrounding code, so comparing two
builds conflates a change's effect with where its code landed.
Each timed loop now runs from one of 16 out-of-line copies, aligned to
256 bytes and padded in 16-byte steps, which place the loop at each of
those 16 positions. A process picks a copy at random, or the one named
in `CARBON_BENCH_LAYOUT`, and reports it in the `Layout` counter.
Running `bench_runner.py` with enough runs to sample every position, 48
for example, makes its medians and confidence intervals account for
layout.
In a sweep over 1024 bytes, performance depended only on the loop's
address modulo 256, and loop heads are 16-byte aligned, so finer or
wider padding found nothing more on this CPU.
`CARBON_BENCH_LAYOUT_PAD_STEP` and `CARBON_BENCH_LAYOUT_PAD_RANGE`
adjust it. The copies grow `map_benchmark` from 4.8 MB to 7.6 MB.
On AArch64, LLVM doesn't align loop heads for generic or Apple CPUs, so
they land on any 4-byte boundary and 16-byte steps only reach a quarter
of the positions. AArch64 builds pad in 4-byte steps instead, giving 64
layouts and growing `map_benchmark` there to 16.6 MB. Every copy,
including the unpadded one, also emits the padding `asm` statement,
because its presence changes how the surrounding code is compiled.
Assisted-by: Claude Code
---------
Co-authored-by: Nicholas Bishop <nbishop@nbishop.net>