Why does reading every 16th value of a contiguous capture buffer take nearly as long as reading all of them?
answer
- count lines, not loop iterations
- what one fetch actually delivers
- four-byte values, sixty-four-byte lines
- sixteen values arrive together either way
- same lines touched means same traffic
basics
~20 sMemory is fetched in whole cache lines, not single values. A 16-element stride over 4-byte values lands on a new line every step, exactly as many lines as the full scan touches, so traffic is unchanged and only the arithmetic gets cheaper.
solid answer
~50 sDoing one sixteenth of the iterations does not mean moving one sixteenth of the memory. The unit of transfer is the cache line, about 64 bytes, which holds sixteen 4-byte counters. The full scan touches `n/16` lines and uses all sixteen values in each; the stride-16 loop touches the same `n/16` lines and uses one value from each. Identical traffic, identical miss count, and both address streams are simple enough for the prefetcher — so wall-clock time is close, with the strided loop only slightly ahead because it executes fewer adds. The lesson generalises: below one line per step you save no traffic at all. Widen the stride past a line and traffic finally falls in proportion, until strides large enough to cross pages start defeating the prefetcher and address translation, at which point the strided loop can end up slower per element than the full scan.
code
pseudocode · 13 lines// buf: n contiguous 4-byte counters from a packet-capture ring
// cache line = 64 bytes, so 16 counters share one line
totalA = 0
for i in 0..n-1:
totalA = totalA + buf[i] // n reads, n/16 lines touched
totalB = 0
for i in 0..n-1 step 16:
totalB = totalB + buf[i] // n/16 reads, n/16 lines touched
// measured on a buffer far larger than cache:
// loop B is only marginally faster than loop A, not 16xgo deeper
Recall that a memory fetch delivers a whole block of neighbouring bytes, so skipping values inside one block saves nothing. Know that both loops remain O(n) and the difference is a constant factor.
Do the arithmetic aloud: sixteen 4-byte values per 64-byte line, so both loops touch the same number of lines. State that the crossover for real savings is one line per step, not one element per step.
Show you would profile before believing either loop is faster — check whether the loop is bandwidth-bound, whether the working set exceeds cache, and whether the stride still crosses page boundaries at production sizes.
Frame it as a measurement discipline: teams that reason about cost from iteration counts alone will keep shipping optimisations that move nothing, so decide what evidence a performance claim must carry before it is accepted.
## The wrong answer this aims at The instinctive reasoning is arithmetic: one sixteenth of the iterations, therefore roughly one sixteenth of the time. It is wrong because it counts *iterations* when the machine charges for *lines*. Interviewers like this scenario precisely because the wrong answer is so reasonable-sounding and the right answer forces you to say out loud what a memory system actually transfers. ## Counting lines instead of iterations Take a capture buffer of `n` contiguous 4-byte counters on hardware with 64-byte cache lines. Sixteen counters fit in one line. | Loop | Values read | Lines touched | Useful bytes per line | |---|---|---|---| | every value | n | n/16 | 64 of 64 | | every 16th value | n/16 | n/16 | 4 of 64 | | every 32nd value | n/32 | n/32 | 4 of 64 | The first two rows touch the *same number of lines*. If the loop body is a single add and the buffer is far larger than cache, the loop is bandwidth-bound: both versions move `n * 4` bytes across the memory bus, and the strided version simply wastes fifteen sixteenths of what it moved. The extra additions in the full scan are nearly free because they overlap with outstanding fetches, so the two loops land close together — often within a small percentage rather than the 16x the naive model predicts. The third row is where the picture changes. Once each step skips a whole line, lines are genuinely skipped and traffic falls proportionally. **The crossover is at exactly one cache line per step, not at one element per step.** ## Why very large strides can reverse the win Widening the stride does not improve things forever: - **Prefetch range.** Stride detectors work within a limited window and typically will not prefetch across a page boundary (commonly 4 KB). A stride of several kilobytes puts every access in a new page, so each one is an exposed miss. - **Address translation.** Every new page also needs a translation entry. Touching one value per page thrashes the translation cache, adding a second class of miss on top of the data miss. - **Bank and row behaviour.** Widely separated addresses lose the row-buffer reuse that sequential streams enjoy inside the memory devices themselves. So the cost curve as a function of stride is not monotonic in the way the naive model suggests: flat up to one line, falling for a while after that, then rising again per element once locality and prefetchability are gone. ## The generalisable rule For a memory-bound loop, estimate cost as **lines touched**, then sanity-check whether the address pattern is prefetchable. Two questions get you most of the way: 1. How many distinct cache lines does this loop touch? 2. What fraction of each fetched line does it actually use? A loop that touches few lines and uses all of each is as good as it gets. A loop that touches many lines and uses a sliver of each is paying full price for data it discards — and no amount of skipping iterations fixes it, because the skipping is happening at the wrong granularity. ## What this does not say It does not say the strided loop is *never* faster. When the loop body is expensive, cutting iterations by 16 cuts real work by 16 and dominates the memory story. It also does not say complexity changed: both loops are O(n) in the size of the buffer, and this is a constant-factor argument from start to finish. And it is a claim about a buffer much larger than cache — if the whole capture buffer fits in cache, there is no line traffic to argue about and the iteration count really does drive the time. ## Saying it well A strong answer names the transfer unit in the first sentence, does the sixteen-values-per-line arithmetic out loud, states the crossover at one line per step, and then adds the non-monotonic tail about pages and translation. A weak answer restates that "caches make sequential access fast" without ever computing how many lines each loop touches.
- At what stride does the strided loop finally start beating the full scan by a real margin?Once each step skips at least one whole cache line — beyond 16 values for 4-byte data on 64-byte lines. From there, lines touched fall roughly as the buffer size divided by the stride, and so does the time. The win keeps growing only while the pattern stays prefetchable; strides large enough to land on a new page each step lose prefetching and address-translation reuse, and per-element cost climbs again.
- Instead of a fixed stride, you read n/16 randomly chosen indexes from the same buffer. How does the cost compare?Usually the same or worse than the strided loop, despite touching a similar number of lines. Random indexes give the prefetcher no pattern, so every access is an exposed miss rather than one hidden behind an early fetch, and scattered pages add translation misses. Contiguity of the buffer buys nothing if the access order does not exploit it.
- How does the argument change if the loop body is expensive rather than a single add?It flips toward the naive model. If each visited value triggers substantial computation, the loop is compute-bound, outstanding fetches overlap with real work, and doing one sixteenth of the iterations really does approach one sixteenth of the time. The line-counting argument governs memory-bound loops; always check which regime you are in before quoting it.
saying these in an interview costs you the question
- One sixteenth the iterations means one sixteenth the time
- Assumes memory is addressed one element at a time
- Treats a bandwidth-bound loop as arithmetic-bound
- Believes any stride is prefetched equally well
- Confuses fewer instructions with less memory traffic