Cache Coherence and Snooping
Every core in a shared-memory
machine has its own private cache
— a small, fast, private copy of the memory it has touched recently. That is wonderful for speed and a
disaster for correctness. The moment two cores cache the same memory block, you have two copies of
one truth. When one core scribbles on its copy, the other core is holding a stale value and
does not even know it.
This is the cache-coherence problem, and it is the central hazard of multicore hardware.
The shared-memory abstraction promises that a read returns the last value written — but private caches quietly
break that promise. This lesson is about the promise the hardware must restore, and the classic mechanism
that restores it: bus snooping.
Two caches, one truth
Picture the simplest possible failure. Memory holds X = 1. Core A and
Core B both read X, so both caches now hold 1.
Now Core A executes X \leftarrow 2 and updates its own cache. If
nothing else happens, Core B's cache still holds the old 1. When B reads
X, it gets 1 — a value that no longer exists anywhere
in the program's logic. Two threads now disagree about a single variable.
This is not a rare corner case; it is what happens by default the instant two cores share data. A
coherence protocol is the hardware that makes sure this never leaks up to the software — it
keeps the private caches consistent so that, to the programmer, there appears to be just one copy of
X.
What coherence actually promises
"Coherence" has a precise definition, and it is narrower than people expect. It governs a
single memory location at a time, and it demands two invariants — together often called
SWMR, Single-Writer / Multiple-Reader:
- Single-Writer / Multiple-Reader: at any moment, for a given block, either
one core may write it (and no one else has it), or any number of cores may read it —
never both a writer and other holders at once;
- Write serialisation (data-value invariant): all writes to the same location are seen
by every core in the same order, and a read returns the value of the most recent write in that
order;
- coherence is per-location — it says nothing about the ordering of accesses to
different locations. That larger question is
memory
consistency, a separate lesson.
Enforce SWMR and the two-caches-one-truth disaster cannot happen: before Core A is allowed to write
X, every other cache's copy of X must be dealt with.
Two ways to keep copies honest
When a core wants to write a block that others may be caching, there are exactly two strategies:
| Policy | On a write, the writer… | Cost | Used by |
| Write-invalidate | tells every other cache to discard its copy, then writes freely | one invalidate message; other cores re-fetch only if they need it again | almost all real systems (MESI etc.) |
| Write-update | broadcasts the new value to every cache holding the block | a bus message on every write; wasteful if others never re-read | rare (some older machines) |
Write-invalidate wins in practice: if Core A writes a variable ten times in a loop,
invalidate pays once (the other copies are gone after the first write), whereas update would
broadcast all ten new values to cores that may not care. Invalidate is lazy in the good way — it only does
work when someone actually re-reads.
Snooping: everyone listens to the bus
How does Core A tell all the other caches to invalidate? In a small system, through a shared
bus. Every cache controller snoops — it watches every transaction that appears on the bus,
not just its own. When A wants to write X, it broadcasts an "invalidate
X" on the bus; every other controller sees it, checks its own tags, and drops its
copy of X if it has one. No directory, no bookkeeping — just a broadcast that
everyone overhears.
The bus does something subtle and essential: it serialises all coherence traffic. Only one
transaction occupies the bus at a time, so all cores observe the same single order of writes — which is
exactly the write-serialisation invariant, handed to you for free by the physics of a shared wire. That is
why snooping is so simple. It is also why it does not scale: one shared bus becomes a bottleneck once you
have many cores, which is the motivation for
directory-based
coherence.
// A tiny two-core model showing incoherence, then how invalidate fixes it.
interface Cache { value: number | null; valid: boolean; }
let memory = 1;
const A: Cache = { value: null, valid: false };
const B: Cache = { value: null, valid: false };
function read(c: Cache, label: string): number {
if (!c.valid) { c.value = memory; c.valid = true; } // miss: fetch from memory
console.log(` ${label} reads X -> ${c.value}`);
return c.value as number;
}
// --- WITHOUT coherence: B keeps a stale copy ---
console.log("No coherence:");
read(A, "A"); read(B, "B"); // both cache X = 1
A.value = 2; // A writes 2 into ITS cache only
read(B, "B"); // B still sees the stale 1 <-- BUG
// --- WITH write-invalidate snooping ---
console.log("With write-invalidate:");
A.valid = false; B.valid = false; memory = 1; // reset
read(A, "A"); read(B, "B"); // both cache X = 1
A.value = 2; memory = 2; B.valid = false; // A's write INVALIDATES B's copy
read(B, "B"); // B misses, re-fetches -> gets 2 <-- fixed
Coherence works on whole cache blocks (typically 64 bytes), not
individual variables. So if two cores update two different variables that happen to sit in the same
64-byte block, each write invalidates the other core's copy of the whole
block — even though the cores never actually share data. The block ping-pongs back and forth across the
bus, and a perfectly correct parallel program crawls. This is false sharing, and the fix is
pure layout: pad or align hot per-thread data so each lands in its own cache line. It is one of the most
common and most invisible multicore performance bugs.
Students routinely conflate coherence and consistency. Coherence is the
narrow guarantee about one location: all cores agree on the order of writes to
X. It says nothing about how accesses to X
and Y interleave. A machine can be perfectly coherent and still let one core see
X's new value before Y's while another sees the
reverse. That cross-location ordering question is consistency, and it is a whole separate lesson.
Coherence keeps each variable honest; consistency governs the story across variables.