TRT
Sign in
Operating Systems

Why Memory Locality Matters

Processors use a hierarchy of storage because small, nearby storage can be accessed faster than large, distant storage.

The Runtime Theory Team8 min read#cache#memory#locality

Processors use a hierarchy of storage because small, nearby storage can be accessed faster than large, distant storage. Registers and L1 are built from the same six-transistor cells as the ALU sits next to; DRAM is a different technology on a different chip, reached over a bus. The gap is not subtle:

WhereTypical latencyReached in
L1 cache (32–64 KB)~1 ns (4 cycles)on-die
L2 (256 KB–1 MB)~3–4 ns (~12 cycles)on-die, shared path
L3 (8–64 MB)~10–20 ns (~40–70 cycles)shared between cores
DRAM~80–100 nsmemory controller
Local SSD~10–100 µsPCIe/NVMe
Network round trip~100 µs–10 msthe wire

A 100 ns memory access is 250 cycles of a 2.5 GHz core doing nothing but waiting. Your program's arithmetic is almost never the bottleneck; its patience is.

Hits, misses, and the cache line

Memory is transferred in cache lines — 64 bytes on every mainstream x86 and ARM core. The cache does not fetch the byte you asked for; it fetches the 64-byte block containing it and keeps the neighboring 63 bytes for free. This is spatial locality: one request serves eight doubles. Temporal locality is the other half: data read recently is likely to be read again before it is evicted.

A miss is not one cost but a cascade: the line must be located in L2/L3, and on a final miss the memory controller must refresh a row, burst 64 bytes across the bus, and fill the line — after which the waiting load resumes. Misses also evict something else, so one miss often costs a later miss too.

The same arithmetic, two different speeds

c
// row-major: contiguous, one line serves 8 doubles
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += a[i][j];
 
// column-major on the same data: jumps N*8 bytes per element
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += a[i][j];

Both execute N² additions. On a 4096×4096 double matrix (134 MB, larger than any L3), the column-major loop reads each 64-byte line once and uses one eighth of it — eight times the DRAM traffic — plus it thrashes the TLB. The measured difference on a typical laptop is 5–10× slower, with perf stat showing miss counts rising by the same order. The arithmetic never changed. The address stream did.

What goes wrong in production

Three patterns cause most cache-driven slowdowns:

  • Pointer chasing. A linked list, a tree of heap objects, a graph of allocations scattered by malloc: every next address depends on the previous load, so the memory-level parallelism that hides latency disappears and each step pays the full ~100 ns. A hash table beats a tree at equal O(log n) partly because its probe sequence is two dependent loads, not eight.
  • Working set bigger than L3. An in-memory index that fits in 30 MB runs at L3 speed; one that grows to 200 MB runs at DRAM speed. There is no gradual degradation — there is a cliff when the hot set stops fitting.
  • False sharing. Two threads updating adjacent 8-byte counters on the same 64-byte line ping-pong the line between cores through the coherence protocol (MESI), costing ~40–100 ns per ownership transfer. Both counters are "in cache", and throughput still collapses.

Measuring instead of guessing

bash
$ perf stat -e cache-references,cache-misses,cycles ./matrix
 12,481,902  cache-references
  3,992,110  cache-misses        # 32.0% of all cache refs

A miss rate above a few percent on a compute loop means the loop is memory-bound: add nothing, remove a branch, or change the layout. The trace of a cache hit and miss walks one access through the levels step by step.

Key takeaways

  • Latency grows by two orders of magnitude from L1 to DRAM; locality is the only lever that keeps data in the fast tiers.
  • Caches move 64-byte lines, so contiguous access amortizes one miss over eight doubles while strided access throws away seven eighths of each fetch.
  • Pointer chasing, oversized working sets and false sharing are the three production failure modes; layouts fix all three.
  • Confirm with perf stat cache-miss counters before optimizing arithmetic — the arithmetic is usually not what is slow.

Production cost model and latency numbers

To apply Why Memory Locality Matters effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Why Memory Locality Matters, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save