Memoryprefetchstridesequentialpointer chasinglatency hiding

Prefetching: The Hardware Guesses What You Will Read Next

A cache miss costs far more than an instruction, so the hardware tries not to take one: it watches your access stream, predicts the next addresses and fetches them early. Predictable patterns get their data before they ask. Pointer chasing does not — which is most of the answer to why an array beats a linked list at the same complexity.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
If a miss to main memory costs the equivalent of many arithmetic operations, how does sequential code manage to run fast at all?
What you wrote
A loop reads `arr[0]`, `arr[1]`, `arr[2]`… Each read is a memory access, and memory is slow, so the loop should be memory-bound and painfully so.
What the hardware does
A prefetcher notices the ascending pattern after a couple of accesses and starts issuing loads for lines you have not requested. By the time the loop reaches them the data is already resident, and the misses are hidden.
Prefetching is why sequential access is so much faster than random access even though both are one memory read per element — and why data structures that hide their next address behind a pointer forfeit the benefit entirely.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Predicting the stream

MICROARCH-SPECIFICNumber of tracked streams, training thresholds, direction support and page-crossing behaviour are per-design and generally undocumented. Treat the categories as durable and the boundaries as machine-dependent.

The simplest useful heuristic is next-line: on a miss for line L, also fetch L+1. That alone covers a large fraction of real access patterns, because sequential traversal is everywhere. More capable prefetchers detect a constant stride — noticing that accesses are separated by a fixed distance and continuing the arithmetic progression — and track several such streams at once.

The crucial property is that prefetching hides latency without reducing it. The miss still happens and the line still travels from wherever it lives; what changes is that it happens concurrently with useful work instead of blocking it. That is why a sequential loop can approach the machine's bandwidth limit rather than its latency limit, and why the two limits are worth distinguishing (Latency and Bandwidth Are Different Resources).

It follows that prefetching only works when the address is knowable in advance. A stride is computable from history. A pointer is not: you must load the current node before you can know the address of the next one, and no amount of pattern detection can shortcut a dependency. That single asymmetry drives most of the practical advice in this module.

What a stride-detecting prefetcher can and cannot do with a given pattern
Access patternPredictable?WhyEffective cost
arr[i], ascendingYesConstant stride of one elementNear a hit, once the stream is trained
arr[i * 4], fixed strideUsuallyStill an arithmetic progressionNear a hit while the stride is tracked
arr[i] descendingUsuallyConstant negative strideNear a hit on prefetchers that track both directions
arr[rand()]NoNo relationship between successive addressesFull miss cost, every access
node = node->nextNoThe address is not known until the load returnsFull miss cost, and serialised (Pointer Chasing: The Address You Do Not Have Yet)
Several arrays walked togetherPartlyMultiple streams, but the tracker is finiteGood until the stream count exceeds capacity

Why the linked list loses

Both loops below visit n elements and do one addition each. Complexity analysis calls them identical and is not wrong about operation counts. The hardware treats them completely differently, because one exposes its future addresses and the other conceals them.

In the array version the addresses form an arithmetic progression that the prefetcher locks onto immediately. Several lines are in flight at once, and the loop runs at roughly the rate memory can stream. In the list version each next must arrive before the following address is even known, so misses are strictly serialised: the machine issues one, waits the full latency, then issues the next. There is no overlap to exploit (Misses That Overlap Are Nearly Free).

This is the concrete answer to the interview question about arrays versus linked lists, and it is worth stating precisely: the array wins not because it has fewer operations but because it has fewer *stalls*, and it has fewer stalls because its addresses are computable in advance. Change the linked list so nodes are allocated contiguously in traversal order and much of the gap closes — which is itself evidence that layout, not the data structure's abstract shape, is doing the work.

Pointer chasing — every address depends on the previous load
1total = 0
2node = head
3while node != null:
4 total += node.value
5 node = node.next // address unknown until
6 // this load completes
7
8// Misses cannot overlap. The loop advances at one
9// full memory latency per node, and the prefetcher
10// has nothing to predict from.
Contiguous array — addresses are an arithmetic progression
1total = 0
2for i in 0 .. n-1:
3 total += arr[i] // address computable from i
4 // long before the load issues
5
6// The prefetcher trains after a couple of accesses
7// and keeps several lines in flight, so the loop
8// runs near streaming bandwidth rather than latency.

Identical operation counts, identical asymptotic complexity. The difference is whether the next address can be computed without waiting for the current load — which decides whether misses overlap or serialise.

Where prefetching stops helping

ISA-SPECIFICSoftware prefetch instructions and their semantics differ between x86-64, AArch64 and RISC-V, and some platforms treat them as hints that may be ignored entirely.

Prefetchers are finite and occasionally counter-productive. They track a bounded number of streams, so a loop walking many arrays at once eventually exceeds the tracker and the extra streams get no benefit. They generally will not follow a stream across a page boundary without a valid translation, which is one reason large pages sometimes help streaming workloads (Huge Pages: More Coverage per Entry, and What It Costs).

They can also be actively harmful. A pattern that looks like a stride but is not causes the prefetcher to fetch lines nobody wants, and those lines occupy the cache and evict data that was useful — turning a mild miss problem into a worse one. On a workload already saturating memory bandwidth, speculative prefetches compete with demand loads for the resource that is already the constraint (When the Memory Bus Is the Bottleneck).

Software prefetch instructions exist on many ISAs and look like an obvious lever, but they are hard to use well: issue too early and the line is evicted before use, too late and you have not hidden anything, and the right distance depends on the machine and on how fast the loop consumes data. They also compete with the hardware prefetcher rather than cooperating. Measure before and after, and expect a fair number of attempts to make things worse.

The limits, and what each one does to you
LimitWhat happensSymptomWhat helps
Finite tracked streamsSurplus streams get no prefetching at allAdding an array to a hot loop degrades the othersFuse passes, or process fewer streams at a time
Page boundariesThe stream typically stops at a page edgePeriodic stalls at regular intervals through a large arrayLarger pages, where the platform offers them (Huge Pages: More Coverage per Entry, and What It Costs)
Cache pollutionMispredicted lines evict useful dataMiss rate rises after adding prefetch hintsRemove the hints; trust the hardware prefetcher
Bandwidth contentionSpeculation competes with demand loadsPrefetching makes a saturated workload slowerCheck bandwidth first (When the Memory Bus Is the Bottleneck)
Software prefetch distanceToo early evicts, too late hides nothingA hint that helps on one machine and hurts on anotherSweep the distance and re-measure per target

Key points

  • Prefetching hides miss latency by fetching predicted lines early; it does not make memory faster.
  • Sequential and fixed-stride patterns are predictable; random access and pointer chasing are not.
  • A pointer dependency serialises misses, because the next address is unknown until the current load returns.
  • This is the mechanism behind arrays outperforming linked lists at identical asymptotic complexity.
  • Prefetchers are finite and can hurt: limited streams, page boundaries, cache pollution and bandwidth contention.

Progressive depth

Overview

The CPU watches your access pattern and fetches ahead. Sequential and fixed-stride patterns are predictable and get prefetched; random access and pointer chasing are not, and pay full miss cost every time.

Practical

Structure hot data so the access pattern is predictable: contiguous arrays walked in order, fixed strides, batched work. This is usually a bigger win than reducing the operation count, because it converts full-latency misses into hits (Both Are O(n). One Is Far Slower.).

Advanced

Prefetchers are finite: they track a bounded number of streams, will not cross a page boundary without a translation, and can be counter-productive on patterns that look predictable but are not — fetching lines that evict useful data. More outstanding prefetches also consume bandwidth, which matters when a workload is already bandwidth-bound (When the Memory Bus Is the Bottleneck).

Internals

Multiple prefetchers typically operate at different levels with different heuristics — next-line, stride detection, and stream tracking — and their aggressiveness, stream counts and training thresholds are microarchitecture-specific and generally undocumented. Software prefetch instructions exist on many ISAs but are difficult to tune and easy to make harmful; they compete with the hardware prefetcher rather than cooperating with it.

Cache Simulator

Change an input and watch which number moves — and which one refuses to.

Cache simulator
SIMULATED
Access pattern
hit rate
87.5%
misses
250
evictions
186
over-fetch
1.0×
compulsory250
capacity0
conflict0
16 sets × 4 ways × 64 B

Almost all hits. Either the working set is resident or the pattern has enough spatial locality that each line pays for many accesses.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Access stream → prefetcher: the hardware observes the addresses of recent misses and hits.
  2. 2
    Observations → stride detection: a consistent difference between successive addresses trains a stream.
  3. 3
    Stream → speculative fill: the prefetcher issues loads for lines ahead of the program's current position.
  4. 4
    Lines → cache: the data arrives before the demand access, converting what would have been a miss into a hit.
  5. 5
    Dependency in the stream → no prediction: when the next address requires a completed load, there is nothing to extrapolate and every access pays full latency.
What people conclude from this — wrongly
  • "Both are O(n), so they cost the same" — complexity counts operations, not stalls, and stalls are what differ here.
  • "The prefetcher will handle it" — it handles predictable patterns. Indirection and randomness defeat it completely.
  • "More prefetching is better" — mispredicted prefetches pollute the cache and consume the bandwidth a saturated workload needs.
  • "Software prefetch is an easy win" — distance tuning is machine-specific and getting it wrong is common and harmful.

Consequences, controls and cost

What it causes
  • • Sequential traversal runs near streaming bandwidth while random traversal runs at memory latency — often an order of magnitude apart.
  • • Linked structures underperform arrays far beyond what operation counts predict, and the gap grows with memory latency.
  • • Adding a stream to a hot loop can degrade the others once the tracker is exhausted.
  • • Bandwidth-saturated workloads can be made slower by aggressive prefetching, because speculation competes with demand.
What you can do
  • • Make hot data contiguous and walk it in layout order — the single largest lever, and it works on every machine ([[array-vs-linked-list]]).
  • • Prefer fixed strides over indirection; where indirection is required, consider an index into a dense array rather than a pointer.
  • • Allocate linked structures in traversal order when they must exist, so the pointers happen to run forward through memory.
  • • Reserve software prefetch for measured cases with a tuned distance, and re-measure on every target machine.
How to see it
  • • Compare sequential against randomly permuted traversal of the same array; the ratio isolates prefetching from every other factor.
  • • Watch miss counters at each level while adding streams to a loop — a jump when the stream count rises indicates tracker exhaustion.
  • • Check whether the workload is already bandwidth-bound before adding prefetches ([[bandwidth-bound-workloads]], [[performance-counters]]).
  • • For software prefetch, sweep the distance rather than guessing, and keep the version that measures better on the target.
What it costs
  • • Contiguous layouts constrain data-structure choices — insertion and removal get more expensive in exchange for traversal speed.
  • • Allocating linked nodes in traversal order requires control over allocation and degrades as the structure is mutated.
  • • Software prefetch adds instructions and machine-specific tuning to the hot path for a benefit that may not survive a hardware change.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • MICROARCH-SPECIFICStream counts, training thresholds, direction support, page-crossing rules and aggressiveness are per-design and generally undocumented; they differ between vendors and between generations.
  • ISA-SPECIFICSoftware prefetch instructions and their semantics differ across x86-64, AArch64 and RISC-V, and some implementations may treat them as ignorable hints.

Misconceptions

Claim
“Prefetching makes memory faster.”
Reality
It hides latency rather than reducing it. The line still travels the same distance at the same speed; the difference is that the journey overlaps useful work instead of blocking it.
Claim
“A linked list is fine because traversal is O(n) just like an array.”
Reality
Operation counts match; stall counts do not. Each next must complete before the following address is known, so misses serialise instead of overlapping — a difference complexity analysis cannot express.
Claim
“Adding software prefetch instructions will speed up my loop.”
Reality
Sometimes, but the useful distance depends on the machine and on how fast the loop consumes data, and a bad distance either evicts the line before use or hides nothing. It also competes with the hardware prefetcher.

Apply it

Where the rest of this lives

Programming Languages & Runtime Internals
Allocator placement of linked nodes

Whether consecutively allocated nodes end up adjacent in memory is an allocator decision, and it determines how much of the array-versus-list gap a linked structure actually suffers.