Photo by Anne Nygård on Unsplash
Cache Coherence and the MESI Protocol: Why Multi-Core Sharing Is Hard
Cache Coherence and the MESI Protocol: Why Multi-Core Sharing Is Hard
The moment a CPU core writes to a memory address, every other core that has cached the same address has a problem. Either the copies stay in sync or programs produce wrong answers. This synchronization challenge — cache coherence — is one of the defining constraints of modern hardware, and understanding it explains a surprising range of performance puzzles in systems programming.
Why Private Caches Create Inconsistency
CPU caches exist because main memory is slow — a DRAM access takes on the order of 60–100 nanoseconds, versus under a nanosecond for L1 cache. Each core maintains its own private L1 and L2 caches holding recently accessed data. For single-threaded workloads this is fine, but multi-core programs share data. If core 0 reads a value into its L1 cache and then core 1 modifies the same address, core 0 is now holding a stale copy. Without hardware intervention, it would continue reading the old value — a data race baked into the silicon.
The hardware solves this by tracking the state of each cache line (typically 64 bytes) and running a coherence protocol that enforces consistency across all cores.
The Four MESI States
MESI is an acronym for the four states a cache line can occupy:
Modified — The line exists only in this cache, and it has been written to. The copy in main memory is stale. Before any other core can read this address, the modified data must be written back or forwarded directly.
Exclusive — The line exists only in this cache and matches main memory. No other core holds a copy. The core can promote this to Modified on a write without broadcasting to anyone, which makes write-after-read fast.
Shared — The line may exist in multiple caches, all matching main memory. If any core wants to write, it must invalidate everyone else’s copies first.
Invalid — The line is not usable. The core must fetch it from another cache or from main memory before doing anything with it.
Transitions between these states happen via messages over the interconnect — the bus, ring, or mesh fabric connecting cores. When a core wants to write to a Shared line, it broadcasts an invalidation. Every other core holding that line drops to Invalid. The writing core takes Exclusive ownership, then transitions to Modified when it writes.
False Sharing: When the Protocol Works Against You
Cache coherence operates at the granularity of a cache line, not individual variables. This leads to a well-known pathological case called false sharing: two cores repeatedly writing to different variables that happen to sit on the same 64-byte line.
From the software perspective, the cores are modifying independent data. From the hardware perspective, they’re fighting over the same line, bouncing it back and forth between Modified states. Each write forces the other core’s copy to Invalid, triggering a fresh fetch. The result is a constant stream of coherence traffic and cache misses — destroying the performance benefit of the cache even though the data easily fits in it.
The fix is padding or alignment: ensure that frequently-written hot variables on different cores occupy different cache lines. Many concurrent data structures do this explicitly. Java’s @Contended annotation and C’s alignas(64) both enforce this at the language level.
NUMA Raises the Stakes
On multi-socket servers, the problem compounds. NUMA (Non-Uniform Memory Access) systems have separate memory controllers per socket, connected by an inter-socket fabric. A cache miss that requires fetching from a remote socket’s memory can be two to four times more expensive than a local one.
Coherence still works across sockets, but the penalty for cross-socket coherence traffic is far higher. This is why NUMA-aware allocation — binding threads and the memory they access to the same socket — matters at scale, and why operating systems expose NUMA topology to runtimes and databases.
What This Means in Practice
Most application developers don’t need to think about cache coherence daily, but it surfaces in predictable places. Lock-free data structures rely on the atomicity guarantees that coherent reads and writes provide. False-sharing bugs appear in high-throughput concurrent code as mysteriously poor scaling — more cores, slower throughput. Database and storage engine designers think carefully about minimizing cross-core coordination precisely because coherence traffic has measurable cost.
When a profiler shows unexpectedly high cache-miss rates on data that should fit in cache, false sharing is often the culprit. Knowing that the hardware is doing substantial bookkeeping to maintain memory consistency — and that this bookkeeping has a price — gives you the right mental model for why some concurrent patterns scale well and others don’t.