What is cache locality, and why does iterating a Java array sequentially tend to be much faster than jumping around it randomly or chasing pointers through linked structures?
answer
- RAM is ~100+ cycles; caches (L1/L2/L3) hide it
- Memory moves in 64-byte cache lines, not bytes
- Spatial + temporal locality = why caches pay off
- Sequential stride → prefetcher loads ahead; random kills it
- int[] beats LinkedList: contiguous + prefetch vs pointer chasing
basics
~20 sMemory is slow, so the CPU keeps recently-used data in small fast caches, and it loads memory in chunks (cache lines). Reading an array in order uses each loaded chunk fully and lets the CPU prefetch the next, so it's fast. Random jumps or pointer-chasing keep missing the cache and waiting on slow memory.
solid answer
~60 sMain memory (RAM) is far slower than the CPU, so CPUs sit behind a hierarchy of small fast caches (L1/L2/L3). Memory is moved in fixed-size blocks called **cache lines** (typically 64 bytes), not single bytes. **Locality** is the property that makes caches work: **spatial locality** (you soon access data near what you just accessed) and **temporal locality** (you reuse the same data soon). Iterating a primitive array in order has excellent spatial locality — one cache-line load brings in several consecutive elements, and the hardware **prefetcher** sees the stride and fetches ahead, so most accesses hit cache. Random access or **pointer chasing** through a linked list / scattered object graph defeats this: each node may be on a different, not-yet-cached line, so you incur a **cache miss** and stall on RAM for each step (and the prefetcher can't predict the address). That's why a contiguous `int[]` usually crushes a `LinkedList<Integer>` for traversal — contiguity and prefetch-friendly access, not big-O, dominate. You don't tune cache lines by hand; you favor contiguous, sequentially-accessed data structures and predictable access patterns.
go deeper
Knows memory is slower than the CPU and that reading an array in order is faster than random access or using a linked list; can use ArrayList over LinkedList by default.
Explains cache lines, spatial/temporal locality and prefetching, and why a contiguous array beats pointer-chasing structures despite identical big-O.
Reasons about access order (row-major iteration), structure choice for hot paths, when locality dominates over algorithmic constants, and validates with cache-miss profiling rather than guessing.
Drives data-oriented design decisions (struct-of-arrays vs array-of-objects, avoiding false sharing, memory layout for hot paths) as an architectural concern, balancing locality against clarity and measuring with hardware counters.
## Why caches exist A CPU core can execute instructions far faster than **main memory (RAM)** can supply data — a fetch from RAM can cost on the order of **100+ cycles**, during which the core would otherwise sit idle. To hide this, CPUs have a **cache hierarchy**: small, very fast **L1** (per-core, tens of KB), larger/slower **L2**, and a big shared **L3**. Data the CPU needs is searched for in L1 first, then L2, then L3, then RAM — each level bigger but slower. A **cache hit** is finding the data in cache (fast); a **cache miss** is not finding it, forcing a fetch from a slower level (slow stall). ## Cache lines: memory moves in blocks Crucially, caches don't store individual bytes — they store **cache lines**, fixed-size blocks (**typically 64 bytes**). When you touch one byte, the whole 64-byte line containing it is loaded. For an `int[]` (4 bytes each), one line holds **16 consecutive ints**: touching `a[0]` brings `a[0..15]` along for free. ## Locality: the principle that makes caches pay off - **Spatial locality:** if you access an address, you're likely to access nearby addresses soon. Sequential array iteration is the ideal case — you walk straight through contiguous memory, so each loaded line is fully used before the next is needed. - **Temporal locality:** if you access something, you're likely to access it again soon, so keeping it cached pays off. ## Prefetching CPUs also have a **hardware prefetcher** that watches access patterns; when it detects a **regular stride** (e.g. +4 bytes each step through an `int[]`), it speculatively loads upcoming lines **before** you ask, so the data is already in cache when you reach it. Sequential access is exactly what the prefetcher loves; random addresses give it nothing to predict. ## Why sequential array access wins Put together: iterating `int[]` in order = full use of every cache line + accurate prefetch = nearly every access is an L1 hit. The loop runs at cache speed. ## Why random access / pointer chasing loses - **Random index access** into a large array jumps to addresses spread across many lines; each jump can be a cache miss, and there's no stride to prefetch — you repeatedly stall on slow memory. - **Pointer chasing** is worse: a `LinkedList`, tree, or graph of separately-allocated objects scatters nodes across the heap. Each step is `node = node.next`, and you can't even compute the next address until the current node arrives — a **dependent load**. So you wait for a (likely-missing) line, then wait for the next, serially. This is why a contiguous `int[]` typically **massively outperforms** a `LinkedList<Integer>` for traversal even though both are O(n): the array has spatial locality and prefetch; the list has neither, *and* boxing (`Integer` objects) adds another layer of indirection and scattering. ## Practical guidance (and what NOT to do) - **Prefer contiguous, primitive-backed structures** (`int[]`, `ArrayList`, arrays of values) over node-based ones (`LinkedList`, scattered object graphs) for hot iteration. - **Access in memory order.** For a 2D array stored **row-major** (Java's `int[][]` is an array of row arrays, each row contiguous), iterate **row by row, then column** — the cache-friendly order; iterating column-first jumps across rows and thrashes the cache. - Be aware of **false sharing** in concurrent code (two threads writing different variables on the *same* cache line ping-pong it between cores) — but that's an advanced concern. - **Don't** start hand-counting cache lines or padding structures speculatively. This is a **measure-first** concern: pick locality-friendly data structures and access patterns by default, and only do exotic layout tuning when a profiler (e.g. cache-miss counters) shows memory stalls dominate. ## Key terms recap - **Cache hierarchy (L1/L2/L3):** small-fast to large-slow on-chip memory between the core and RAM. - **Cache hit / miss:** found in cache (fast) vs. must fetch from a slower level (stall). - **Cache line (~64 bytes):** the block size memory is loaded/stored in; touching one byte loads the whole line. - **Spatial / temporal locality:** accessing nearby data soon / reusing the same data soon. - **Prefetcher:** hardware that loads upcoming lines when it detects a regular stride. - **Pointer chasing / dependent load:** following references where each address depends on the previous fetch — serial, miss-prone, no prefetch. - **Row-major:** consecutive elements of a row are contiguous; iterate rows-outer for locality.
- Why is ArrayList usually faster to iterate than LinkedList even though both are O(n)?ArrayList stores its references in one contiguous backing array, so traversal has spatial locality and the prefetcher can run ahead — mostly cache hits. LinkedList nodes are separately allocated and scattered, so each `next` is a dependent, likely-missing load with no prefetch. Big-O is the same; cache behavior makes the array several times faster in practice.
- For a Java int[][], why iterate rows-outer, columns-inner?Java's int[][] is an array of row arrays; each row is contiguous in memory. Iterating across a row (inner loop over columns) walks contiguous addresses with good spatial locality and prefetch. Iterating columns-outer jumps to a different row array each step, missing the cache repeatedly.
saying these in an interview costs you the question
- Comparing structures only by big-O — O(n) array traversal can be many times faster than O(n) linked-list traversal due to locality
- Thinking memory is read one byte/element at a time rather than in cache lines
- Believing a LinkedList is faster to iterate than an ArrayList because 'no resizing' — pointer chasing usually loses badly
- Iterating a 2D array column-first in Java without realizing it thrashes the cache (rows are the contiguous dimension)
- Confusing cache locality with branch prediction — both are CPU effects but distinct mechanisms