5 · Shaping the data Draft
A worked example of bounds checks
On this page
Worked example: a loop over positions with and without bounds checks
At 14:30:00.000, after the batch, the strategy on core 3 sums its exposure, quantity times average price, over the 64 positions in Positions::by_id, each the 32-byte #[repr(C)] Position with qty at offset 8 and avg_price at 16: 2 KB, 32 lines, all in L1. The natural version indexes; the other iterates:
pub struct Strategy { positions: Positions, n: usize, exposure: i64 } // n: live ids, 64
impl Strategy {
pub fn exposure_indexed(&mut self) {
let p = &self.positions.by_id;
self.exposure = 0;
for i in 0..self.n {
self.exposure += p[i].qty * p[i].avg_price;
}
}
pub fn exposure_iter(&mut self) {
let p = &self.positions.by_id[..self.n];
self.exposure = p.iter().map(|x| x.qty * x.avg_price).sum();
}
}cargo asm on rustc 1.97, at the default target, shows the two loops:
exposure_indexed: one position per pass, ten instructions
.loop:
cmp rsi, rdx ; i == len? the bounds check
je .panic ; yes: call panic_bounds_check
inc rdx
mov r9, [rcx] ; one field of p[i]
imul r9, [rcx - 8] ; times the other
add r8, r9 ; running total
mov [rdi + 32], r8 ; stored to self.exposure on every pass
add rcx, 32 ; next Position
cmp rax, rdx ; i == n?
jne .loop
exposure_iter: one check before the loop, then four positions per pass
.loop:
mov r10, [r9 - 96] ; one field of the first Position
imul r10, [r9 - 104] ; times the other
... ; three more load-multiply pairs, four adds
add r8, 4
sub r9, -128 ; 4 × 32 bytes
cmp rsi, r8
jne .loop ; self.exposure is stored once, after the loopThe indexed loop compares and jumps on every pass, and stores the total on every pass too, which is the check’s doing, since whoever catches a panic at pass 37 could read self.exposure. The iterator version checks n once, when it slices, then does four positions per pass in sixteen instructions with the total in registers. Built with -C target-cpu=native, the iterator’s loop becomes vector multiplies on ymm, the 32-byte AVX registers, and the compiler vectorises the indexed loop over the range it can prove safe, leaving a checked remainder. And with a local total in place of self.exposure the compiler removes the per-pass check unaided: nothing a panic could reveal changes inside the loop, so it compares n - 1 with the length once, before the loop, and emits the iterator’s loop exactly. Look before removing a check; it may be gone already.
On the same 2.1 GHz Xeon virtual machine, the indexed version measured 32–34 ns for the 64 positions and the iterator 22–24 ns; on a 4 GHz part expect 20–60 ns, 80–240 cycles, against 10–30 ns, 40–120 cycles: 1–6% of a budget either way. The check is predicted on all 64 passes, since it never fails, so its branch costs next to nothing; the difference is what it stopped the compiler doing: unrolling, holding the total in a register and, on a wider target, vectorising.
Seeing it
Run cargo asm --lib on both, each marked #[inline(never)] while looking (rustc emits exposure_indexed, being small, only where it is called), with and without --target-cpu=native: the indexed one jumps to panic_bounds_check from inside its loop, while the iterator’s only panic, for a slice shorter than n, is reached before its loop starts. Call each ten million times from a small benchmark binary under perf stat -e branch-misses,instructions -- taskset -c 3, subtract an empty loop, and divide by the calls. The listings predict about 650 instructions a call against about 280, and branch misses near zero for both: the check was never mispredicted, and the difference is instructions, not branches. perf annotate on exposure_indexed should show the samples spread over the loop’s body, not gathered on the compare.
Read more: the Rust project’s Rustonomicon, pages “repr(Rust)” (doc.rust-lang.org/nomicon/repr-rust.html), “Alternative representations” (doc.rust-lang.org/nomicon/other-reprs.html) and “Working With Uninitialized Memory” (doc.rust-lang.org/nomicon/uninitialized.html). Nicholas Nethercote and others, The Rust Performance Book (first published 2020, nnethercote.github.io/perf-book), chapters “Inlining”, “Bounds Checks”, “Build Configuration” and “Machine Code”. Agner Fog, Optimizing software in C++ (agner.org/optimize, revised September 2026), on what compilers do with calls, inlining and loops.