DRAMlatencybandwidthmemorydiagnosisparallelism

Latency and Bandwidth Are Different Resources

A workload can saturate memory bandwidth while barely being affected by latency, or be crippled by latency while using a fraction of available bandwidth. Conflating the two sends people to the wrong fix — and "the memory is slow" is almost never a complete diagnosis.

▶ 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
Is this workload limited by how long one memory access takes, or by how many bytes per second the memory system can deliver?
What you wrote
"Memory is slow" — one property, one number, one fix.
What the hardware does
Latency is the time from issuing a request to receiving the data. Bandwidth is the aggregate bytes per second the memory system can sustain. A machine can have plenty of one while you are starved of the other.
The two have opposite remedies. Latency problems are solved by having more requests in flight or needing fewer dependent accesses; bandwidth problems are solved by moving fewer bytes. Applying the wrong remedy wastes effort and sometimes makes things worse.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Two independent limits

Consider two loops over the same amount of data. The first sums a large contiguous array. The second walks a linked list scattered across that same footprint. Both touch a similar number of bytes. The first can have many loads outstanding simultaneously because the addresses are all computable in advance, so it pushes the memory system toward its bandwidth ceiling. The second cannot issue the next load until the current one returns, so it has exactly one request in flight and sits at the mercy of latency.

The result is that the streaming loop may be running at ninety percent of peak memory bandwidth while the pointer-chasing loop uses a tiny fraction of it — and yet the pointer-chasing loop takes longer. Adding memory channels helps the first and does essentially nothing for the second. Reducing the dependency chain helps the second and does nothing for the first.

This is why "the memory is slow" is not a diagnosis. The actionable question is whether you are bandwidth-bound or latency-bound, and the two have their own lessons: When the Memory Bus Is the Bottleneck and When You Cannot Ask the Next Question Yet.

Same memory system, two different limits
Latency-boundBandwidth-bound
Limiting quantityTime for one dependent accessBytes per second delivered
Typical shapePointer chasing, dependent loads, tree walksStreaming, scanning, large copies
Outstanding requestsFew — often oneMany
Memory bandwidth usedLowNear peak
Adding coresHelps if work is independentDoes not help; bandwidth is already saturated
What actually helpsShorten dependency chains, prefetch, restructure to arraysMove fewer bytes: compression, smaller types, better layout

The dependency chain is the difference

The mechanism separating the two cases is memory-level parallelism — how many misses the core can have outstanding at once. Modern cores can track a number of concurrent misses, and this is what turns a sequence of high-latency accesses into a high-bandwidth stream: if ten misses are in flight together, the effective cost per access is a tenth of the latency.

A dependency chain destroys this. If the address of the next load is the *result* of the current load, then by construction only one can be in flight. No amount of hardware parallelism helps, because the hardware does not know the next address yet. This is the whole story of Pointer Chasing: The Address You Do Not Have Yet.

The comparison below is the canonical demonstration. Both loops read the same number of elements from the same array; only the address dependency differs.

Latency-bound: each address depends on the previous load
1// idx[] has been shuffled, so each load's address
2// comes from the value the previous load returned
3i = 0
4for (n = 0; n < N; n++) {
5 i = idx[i] // must wait for this load
6 sum += data[i] // before this one can even be addressed
7}
8
9// outstanding misses: 1
10// bandwidth used: a fraction of peak
11// limited by: DRAM latency x N
Bandwidth-bound: all addresses known in advance
1for (i = 0; i < N; i++) {
2 sum += data[i] // address is i, computable immediately
3}
4
5// the core can issue many loads before the first returns,
6// and the prefetcher runs ahead of them
7
8// outstanding misses: many
9// bandwidth used: near peak
10// limited by: bytes/second the memory system delivers

Identical byte counts, identical instruction counts, wildly different runtimes. The difference is not how much memory is touched but whether the addresses are known early enough to overlap the accesses. This is the clearest demonstration in the domain that data *movement* and data *dependency* are separate costs.

Diagnosing which one you have

The diagnosis is usually quick. Measure achieved memory bandwidth during the hot phase and compare it against what the machine can sustain for a pure streaming benchmark. If you are near that ceiling, you are bandwidth-bound and the fix is to move fewer bytes. If you are far below it while stalling on memory, you are latency-bound and the fix is to get more requests in flight.

A second, cruder test: add cores. A bandwidth-bound workload stops scaling once the memory system saturates, and can even regress as contention rises. A latency-bound workload with independent work per core scales roughly linearly, because each core independently has its one outstanding request.

This diagnosis is the hardware-level counterpart of the reasoning in Busy Is Not the Same as Working, and it feeds directly into the When the Memory Bus Is the Bottleneck and When You Cannot Ask the Next Question Yet lessons.

  • Near-peak bandwidth plus high stall time → bandwidth-bound. Move fewer bytes.
  • Low bandwidth plus high stall time → latency-bound. Get more accesses in flight.
  • Low bandwidth plus low stall time → not a memory problem at all; look at Busy Is Not the Same as Working.
  • Scaling stops when cores are added → bandwidth or another shared resource has saturated.

Key points

  • Latency is time-per-access; bandwidth is bytes-per-second in aggregate. They are separate resources with separate limits.
  • Memory-level parallelism converts high-latency accesses into a high-bandwidth stream — but only when addresses are known in advance.
  • A dependency chain forces one outstanding access at a time, which is why pointer chasing is latency-bound no matter how fast the memory is.
  • Bandwidth-bound workloads stop scaling with more cores; latency-bound workloads with independent work usually keep scaling.
  • The two conditions have opposite fixes, so distinguishing them before acting is the whole value of the diagnosis.

Progressive depth

Overview

Latency is how long one memory access takes. Bandwidth is how much data the memory system can move per second. "Slow memory" could mean either, and they need different fixes.

Practical

Measure achieved bandwidth against the machine's streaming ceiling. Near the ceiling means bandwidth-bound: move fewer bytes with smaller types, better packing or compression. Far below it while stalling means latency-bound: shorten dependency chains, use arrays instead of pointer structures, or prefetch explicitly.

Advanced

The bridge between them is memory-level parallelism. A core can sustain a limited number of outstanding misses; with k misses in flight the effective per-access cost approaches latency/k. Streaming code saturates that capability and becomes bandwidth-limited; dependent code cannot use it at all. This is why unrolling a pointer chase does not help — unrolling exposes instruction-level parallelism, not memory-level parallelism, because the addresses still are not known.

Internals

At the DRAM level the two limits have different physical origins. Latency is dominated by the activate-read-precharge protocol described in How DRAM Is Organised, plus queueing delay in the memory controller. Bandwidth is set by bus width and clock, degraded by refresh, row conflicts and read-write turnaround. Because the controller reorders requests to maximise row hits, a workload with many independent outstanding requests also gets *better* row-buffer behaviour than one issuing them serially, so the two effects compound in favour of parallel access.

Where the Data Is

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

Where a value can be, and roughly what each costs relative to a register — 1 unit ≈ one register accessSIMPLIFIED
Register×1
L1 cache×4
L2 cache×14
L3 cache×45
DRAM×200
NVMe storage×100000
Network round trip×10000000
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
RegisterAlready in the core. Effectively free.
L3 cacheUsually shared between cores, so other work affects your hit rate.
DRAMTwo orders of magnitude past L1. This is the cliff.
NVMe storageAnother three orders of magnitude, and the OS gets involved.
Network round tripDifferent universe. Included to keep the earlier rows in perspective.

The exact ratios vary by machine and the absolute times vary far more, which is why none are shown. What is stable enough to build intuition on is the shape: each level is several times the one above, and the gap between the last cache level and memory is the one that decides most program performance.

Follow the mechanism

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

  1. 1
    Core → load-store unit: a load issues and, if it misses, occupies one of a limited number of miss-tracking entries.
  2. 2
    Miss entries → memory controller: several independent misses can be outstanding simultaneously, overlapping their latencies.
  3. 3
    Dependency → parallelism: if the next address depends on a returning value, no second miss can be issued, so only one entry is ever occupied.
  4. 4
    Controller → DRAM: many concurrent requests let the controller batch row hits and use multiple banks, approaching bandwidth limits.
  5. 5
    Bandwidth ceiling → stall: once the bus saturates, additional requests simply queue, and adding cores stops helping.
What people conclude from this — wrongly
  • "We are memory-bound, so we need faster RAM." Faster RAM raises bandwidth and barely moves latency; if you are latency-bound this buys almost nothing.
  • "Bandwidth usage is low, so memory is not the problem." Low bandwidth with high stall time is the *signature* of a latency problem.
  • "Adding threads will fix it." It will if you are latency-bound with independent work, and will not if bandwidth is already saturated.

Consequences, controls and cost

What it causes
  • • Two loops touching identical amounts of data can differ several-fold in runtime purely because of address dependencies.
  • • Bandwidth-bound code stops scaling with core count, sometimes regressing as contention grows.
  • • Optimisations that reduce instruction count can leave a memory-bound loop completely unchanged.
What you can do
  • • Diagnose first: compare achieved bandwidth against the machine's streaming ceiling before choosing a remedy.
  • • For bandwidth limits, move fewer bytes — smaller types, tighter packing, avoiding fields you do not read (see [[aos-vs-soa]]).
  • • For latency limits, break dependency chains so multiple accesses can be outstanding, or convert pointer structures into arrays.
  • • Use software prefetching only after the simpler restructurings, and only with measurement — it is easy to make things worse.
How to see it
  • • Read achieved memory bandwidth from performance counters during the hot phase and compare against a pure streaming benchmark on the same machine.
  • • Count outstanding misses or measure stall cycles attributed to memory; low bandwidth with high stalls indicates latency-bound behaviour.
  • • Run a core-count scaling sweep: flat scaling implicates a saturated shared resource, near-linear scaling suggests per-core latency limits.
What it costs
  • • Reducing bytes moved often means compression or narrower types, spending CPU cycles to save bandwidth — a good trade only when bandwidth is genuinely the limit.
  • • Breaking dependency chains typically means restructuring data layout, which costs abstraction and code clarity.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALThe latency-versus-bandwidth distinction holds on every machine with a cache hierarchy. The number of outstanding misses a core can track is MICROARCH-SPECIFIC and differs substantially between designs.

Misconceptions

Claim
“Bandwidth and latency are two ways of describing the same speed.”
Reality
They are independent. A memory system can deliver enormous aggregate bandwidth while any single dependent access remains slow, and vice versa.
Claim
“If memory bandwidth usage is low, memory is not the bottleneck.”
Reality
Latency-bound code uses very little bandwidth precisely because it cannot issue enough concurrent requests. Low bandwidth plus stalls is a memory problem.
Claim
“Unrolling the loop will expose more memory parallelism.”
Reality
Unrolling exposes instruction-level parallelism. If the addresses depend on returned values, the hardware still cannot issue the next access early.

Apply it