interviewpath
  • 0day streak
  • 10total XP
  • Level 1
0/6

Unit 3 · OS & concurrency · Lesson 3 of 11

CPU caches, locality, coherence and NUMA

CPU caches, locality and coherence decide whether code runs at cache speed or memory speed, and they explain surprises such as false sharing, shared counters that do not scale and NUMA penalties on large machines. They are the bridge between big-O analysis and measured performance.
About 4 hFocus mode

Step 1: Learn the ideas

  • Memory hierarchy and approximate latencies
  • Cache lines, associativity and the three Cs of misses
  • Spatial and temporal locality; hardware prefetching
  • Data layout: arrays of structs versus structs of arrays, blocking
  • Write-back caches and store buffers
  • Cache coherence with MESI intuition
  • False sharing and true sharing
  • Scalable counters and per-core data
  • NUMA nodes, first-touch placement and remote access
  • Memory bandwidth and bandwidth-bound workloads
  • Measuring with hardware counters

Why it matters

Two loops with identical big-O can differ by an order of magnitude because one walks memory sequentially and the other jumps around. The same physics explains why adding threads to a counter-heavy service can make it slower, why two "independent" per-thread counters interfere, and why a large VM spanning two sockets behaves differently from a small one. If you have studied interference between co-located workloads, shared last-level cache and memory bandwidth are the mechanisms behind much of it. Low-level, AI-infrastructure and systems-research interviews probe this directly; backend interviews touch it through data layout and contention.

Core ideas

The hierarchy. Registers, per-core L1 and L2 caches, a shared last-level cache, then DRAM. As rough orders of magnitude, an L1 hit costs a few cycles, a last-level hit tens of cycles and a DRAM access around a hundred nanoseconds. Measure your own machine rather than memorizing figures, but keep the ratio in mind: missing every cache costs the time of hundreds of instructions.

Lines and locality. Caches move fixed-size lines, 64 bytes on most x86 and many ARM cores. Spatial locality means using the rest of a line you already paid for; temporal locality means reusing data while it is still cached. Hardware prefetchers recognize sequential and strided streams, so array scans run near bandwidth limits while pointer chasing, hash probing and random access pay full latency. Misses are compulsory (first touch), capacity (working set too big) or conflict (too many hot lines mapping to one set); coherence adds a fourth kind.

Layout is a design decision. Traversing a row-major matrix by columns touches a new line on every access. A struct-of-arrays layout lets a loop that reads one field stream through memory, while an array of structs drags unused fields along. Blocking (tiling) reorganizes a computation so each tile is reused while cached, the same idea that makes GPU kernels and FlashAttention fast.

Coherence, MESI intuition. Each cached line is Modified (only copy, dirty), Exclusive (only copy, clean), Shared (clean, possibly in several caches) or Invalid. Reads can share a line cheaply. A write needs exclusive ownership, so every other copy is invalidated and the next access from another core misses. A line written by several cores therefore bounces between them, and each transfer costs about as much as a cache miss.

False and true sharing. False sharing happens when independent variables written by different threads sit on the same line, such as per-thread counters packed into one array. Fix it with padding or alignment to line boundaries, or by accumulating locally and combining at the end. True sharing, many threads updating the same variable such as a global counter or a lock word, cannot be padded away; the scalable answer is sharding (per-core counters summed on read) or batching updates.

struct padded_counter { _Alignas(64) long value; }; /* one line per thread */
struct padded_counter per_thread[NTHREADS];

Coherence is not consistency. Coherence keeps the copies of one location in agreement. It provides neither atomic read-modify-write nor any order in which different locations become visible; that is the memory model, covered with synchronization primitives.

NUMA and bandwidth. On multi-socket machines and some large single-socket parts, each memory controller is closer to some cores than to others. Linux normally places a page on the node of the thread that first touches it, so one thread initializing a big array can strand all of it on one node. Streaming workloads saturate memory bandwidth long before the cores are busy, and adding threads then adds nothing. numactl and numa(7) expose the placement policies.

Common misconceptions

  • "Asymptotic complexity decides performance." At realistic sizes, constant factors from misses often dominate; a linked list and an array have the same traversal complexity and very different speed.
  • "Cache coherence makes concurrent code correct." It keeps one location's copies consistent; it gives neither atomic increments nor ordering across locations.
  • "Atomics make a shared counter scale." An atomic increment still needs exclusive ownership of the line, so under contention every increment is a cross-core transfer.
  • "False sharing only affects locks." Any frequently written data sharing a line with another thread's data suffers, including adjacent struct fields and arrays indexed by thread ID.
  • "Prefetchers hide memory latency." Only for predictable patterns; dependent loads in linked structures and hash chains still wait for DRAM.
  • "NUMA does not matter in the cloud." Large instances can span sockets, and placement and bandwidth effects appear there too.

How to approach it

Write two small experiments and measure them: sum a matrix by rows and by columns, and run per-thread counters packed together versus padded. Use perf stat with cycle and cache-miss events to see the difference, then perf c2c to locate the contended line (event availability varies by CPU and inside VMs). In interviews, explain answers in terms of lines, ownership and bandwidth rather than "the cache is faster", and propose a fix (layout change, padding, sharding) together with how you would verify it.

Suggested order

  1. 1Read Drepper Section 3 (CPU Caches) for lines, associativity, write policies and multi-processor coherence; skim the most hardware-specific parts.
  2. 2Read perfbook Ch. 3 (Hardware and its Habits) for why coherence traffic and atomic operations are expensive.
  3. 3Read OSTEP Ch. 10 (Multiprocessor Scheduling) for cache affinity and how schedulers account for caches.
  4. 4Read Drepper Section 6.4 (Multi-Thread Optimizations) on false sharing, then perfbook Ch. 5 (Counting) for sharded counters.
  5. 5Read Drepper Section 5 (NUMA Support) and numa(7) for placement policies.
  6. 6Run the row-versus-column and packed-versus-padded counter experiments with perf stat and perf c2c, then do the OSTEP TLB measurement homework.

Step 2: Check off what you can do

Tap each one you can genuinely do or explain. Each earns 10 XP.

Step 3: Go deeper

  • What Every Programmer Should Know About MemoryStart here

    Ulrich Drepper's website (akkadia.org)

    Section: Section 3: CPU Caches

    Primary mechanism-level explanation of caches, coherence and the cost of misses; continue with Sections 5 and 6 for NUMA and false sharing.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • Is Parallel Programming Hard, And, If So, What Can You Do About It?Start here

    kernel.org (author-maintained free book)

    Section: Ch. 3: Hardware and its Habits

    Short, concrete account of why atomic operations, cache misses and coherence traffic dominate the cost of parallel code.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • Is Parallel Programming Hard, And, If So, What Can You Do About It?

    kernel.org (author-maintained free book)

    Section: Ch. 5: Counting

    Sharded and statistical counters, the standard answer to a shared counter that does not scale.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • Operating Systems: Three Easy Pieces

    University of Wisconsin–Madison (free online book)

    Section: Ch. 10: Multiprocessor Scheduling

    Cache affinity and coherence from the scheduler’s point of view.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • The Linux man-pages project (man7.org)

    man7.org (Linux man-pages project)

    Section: perf-c2c(1)

    Reference for the perf mode that attributes cache-line contention and false sharing to code and data addresses.

    Open(opens in a new tab)Link responded, 5 Oct 2026

Step 4: Practice

Write the experiments yourself in C, C++ or Rust, since higher-level runtimes add noise: (1) sum a large matrix by rows and by columns; (2) increment per-thread counters stored contiguously versus padded to 64 bytes; (3) increment one shared atomic counter from 1, 2, 4 and 8 threads. Predict each result first, then measure on Linux with perf stat (cycles, instructions, cache misses) and explain the gap; run perf c2c on experiment 2. On a multi-socket machine or large VM, compare numactl --interleave with default first-touch placement for a bandwidth-bound loop. Then do the TLB homework and, for depth, the 6.1810 locks lab.

Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).

Step 5: Test yourself

Answer each one out loud or on paper before you finish the lesson.

  1. Question 1Why does a linked-list traversal run much slower than an array traversal of the same length?
  2. Question 2What are the three classic causes of cache misses, and which kind does coherence add?
  3. Question 3Why does an atomic fetch-and-add on one variable stop scaling as threads are added?
  4. Question 4How would you detect false sharing in a running program?
  5. Question 5Which NUMA node backs a page by default on Linux?
  6. Question 6When does adding threads to a streaming loop stop helping, and why?

You’re done when you can

  • Predict and then measure the row-versus-column traversal gap on your machine, and explain it in terms of cache lines.
  • Demonstrate false sharing with a microbenchmark and remove it with padding, showing before and after measurements.
  • Explain state by state what happens when two cores alternately write the same line.
  • Design a counter that scales with core count and state its read-side cost.
  • Explain first-touch NUMA placement and describe a scenario where it hurts.

Finish the lesson

How confident do you feel?

Lesson complete!

+50 XP

Reviews will come up on the Practice tab so it sticks.

Unlocks next: Linearizability, concurrent data structures and memory reclamation, Profiling, bottlenecks and benchmarking pitfalls, Resource scheduling, fairness, interference and cold starts, GPU architecture, memory hierarchy and arithmetic intensity, Interference, noisy neighbors and performance isolation

Your notes and study time

Log study time