When each core has its own cache, the same memory location can exist in several places at once, and a write by one core must not leave the others with a stale copy. Cache coherence is the hardware's way of making all those copies behave like a single memory. Every core gets a correct view, without software having to do anything. This article explains the problem, the rule hardware must follow, and how the MSI, MESI and MOESI protocols work, with step-by-step examples, and what all this costs.
12 min read | Intermediate
What You'll Learn
- Explain the coherence problem and what coherence guarantees
- Describe the MSI states and transitions
- Trace a sequence of reads and writes on two cores
- Explain what MESI and MOESI add and why
The Problem
Take two cores with write-back caches and a variable x, holding 0 in memory:
core 0 reads x core 0's cache: x = 0 core 1 reads x core 1's cache: x = 0 core 0 writes x = 5 core 0's cache: x = 5 memory still 0 core 1 reads x core 1's cache hit: x = 0 WRONG
Core 1 reads a stale value from its own cache. Nothing is wrong with either cache as such, but the system as a whole has broken the idea that memory has one value. Single-core caches never had this problem. It comes from having several copies that can change.
What Coherence Means
A coherent system guarantees two things for each memory location:
- Single writer, multiple readers: at any moment, a location may be written by one core, or read by many, but not both.
- Latest value: a read returns the value of the most recent write to that location.
Together, these mean that all cores agree on the order of writes to any one location. Coherence says nothing about the order of writes to different locations. That is the memory consistency model, covered in Memory Consistency, Atomics and Fences.
Hardware keeps this property one cache line at a time, and it uses a coherence protocol: a set of states for each line, and the rules for moving between them. The caches work together through a shared interconnect, and Snooping vs Directory Coherence explains how they find each other. Most protocols are write-invalidate: before a core writes, it makes sure that all other copies are removed.
The MSI Protocol
The simplest useful protocol gives each line in each cache one of three states:
- Modified (M): this cache holds the only valid copy, and it has been changed. Memory is out of date. This core may read and write freely.
- Shared (S): this cache holds a clean copy. Others may hold it too. This core may read, but not write.
- Invalid (I): this cache has no usable copy.
The picture shows the main transitions for one line in one cache. A local read of an invalid line fetches a copy and enters S. A local write needs exclusive ownership, so the cache sends an invalidate request, and all other copies go to I. It then enters M. If another core then wants to read the line, the core in M must supply the data, and drop to S.
Here is the earlier sequence under MSI:
| Action | Core 0 | Core 1 | Memory | What happens |
|---|---|---|---|---|
| start | I | I | x = 0 | |
| core 0 reads | S, 0 | I | x = 0 | read request, memory replies |
| core 1 reads | S, 0 | S, 0 | x = 0 | read request, memory replies |
| core 0 writes 5 | M, 5 | I | x = 0 | invalidate, core 1 drops its copy |
| core 1 reads | S, 5 | S, 5 | x = 5 | core 0 supplies data and updates memory |
The last line is the key step. Core 1 misses, because its copy was invalidated, and it gets the new value from core 0. This kind of miss, caused only by another core's write, is the coherence miss mentioned in Miss Types and Average Memory Access Time.
Quick Check
Q: Under MSI, core 0 holds a line in M. Core 1 writes to the same line. What happens to the line in each cache?
Show Answer
Core 1 sends a request for exclusive ownership. Core 0 supplies the data and moves to I. Core 1 receives it and moves to M. The data doesn't need to go to memory at that moment, because core 1 now owns the newest copy.
MESI: The Exclusive State
MSI has an annoying inefficiency. A program reads a private variable, and then writes it. The read puts it in S, and the write must then send an invalidate on the interconnect, though no one else has a copy. In single-threaded code this happens all the time, and every such message is wasted.
MESI adds a fourth state:
- Exclusive (E): this cache holds the only copy, and it is clean, equal to memory.
When a read finds that no one else has the line, the line enters E rather than S. A later write moves it from E to M silently, with no message at all, because the cache already knows it has the only copy. If another core reads it first, it drops to S. MESI, in some form, is the base of most protocols used today. The name is sometimes given as the Illinois protocol, after the university where it was first described.
MOESI: The Owned State
MESI has a remaining cost: when a core in M must share a line, the data is also written back to memory. If the line is shared around a lot, that is much unneeded memory traffic. MOESI adds:
- Owned (O): this cache has a modified copy, and other caches may hold shared copies. Memory is out of date, and this cache is responsible for supplying the data, and for writing it back in the end.
A line in M that receives a read request can move to O, and the reader gets S. No memory write takes place. Several sharers can go on reading a value newer than memory, with the owner serving requests. Some processor families use it, while others use a variation called MESIF, in which a Forward state marks one clean sharer as the one that answers. In each case, the point is to reduce slow memory traffic by making caches answer each other.
| State | Valid copy? | Same as memory? | Other copies? | Can write without asking? |
|---|---|---|---|---|
| M | yes | no | no | yes |
| O | yes | no | maybe | no |
| E | yes | yes | no | yes (moves to M) |
| S | yes | maybe | maybe | no |
| I | no | - | - | no |
Write-Invalidate or Write-Update?
An alternative is write-update: instead of invalidating other copies, send them the new value. If they will read it soon, this avoids misses. But most writes are followed by more writes from the same core, so sending every one uses far more interconnect bandwidth than one invalidate. Nearly all designs use invalidation.
Where Is This Used?
Every multicore phone, laptop and server chip is coherent. Bus standards include it: Arm's ACE and CHI protocols, and RISC-V systems often use similar ones or open versions such as TileLink. Those define the messages between caches, using MOESI-style states. Devices can take part too. A coherent DMA engine or GPU keeps its accesses in step with the caches, saving the driver from having to flush them by hand, as Polling, Interrupts and DMA discusses.
What Happens Under the Hood
The table above treated each step as instant, but a real transaction takes several messages and many cycles, and things can happen in between. Two cores may try to write the same line at the same moment. So an interconnect must put requests in a single order per line, and each cache has transient states, such as "waiting for data, want M". A real protocol has many of these in addition to its four or five named states. They are the reason protocols are so hard to verify, and they are often formally checked, because the bugs can be very rare and timing-dependent. Engineers use model checkers, which explore every possible order of messages in a small system.
Coherence must also work with evictions. A line in M that is evicted has to be written back to memory, and a modern protocol must handle a request for a line that is on its way out.
Coherence Beyond the Data Caches
Instruction caches are usually left out of the protocol, or handled more simply, because code rarely changes. This saves work, but a program that writes new code, such as a just-in-time compiler, must run fence.i on RISC-V before it runs that code, so that the instruction cache sees it. Devices are the other case. An engine that reads memory by DMA might find a stale value if the newest data is in a core's cache. A coherent interconnect lets the device's access snoop the caches, so that it sees the same values as the cores. Without that, the driver has to clean and invalidate the caches by hand. TLBs are not kept coherent by this hardware either, which is why TLBs and Page Table Walks needs shootdowns.
An Example: A Spin Lock
A lock shows the protocol under stress. Suppose four cores wait for a lock by repeatedly doing an atomic swap on one word. Each swap is a write, so each needs the line in M. The line moves from core to core all the time, even though the lock is held and nobody can get it. The interconnect fills up with useless traffic, and the core that holds the lock is slowed down too.
A small change in software fixes this. A test-and-test-and-set lock spins on an ordinary load, which keeps a Shared copy in each cache and makes no traffic. Only when the value changes, because the holder wrote it and invalidated those copies, does a waiting core try the atomic swap. The lock now uses the protocol in a way that suits it.
The Cost of Coherence
Coherence is automatic, but it is not free. Each cache needs extra state, and some logic to handle incoming requests. Traffic grows with the number of cores. Misses that go to another core's cache cost tens to a hundred or more cycles. When programs share a line heavily, performance can suffer badly. A spin lock, in which several cores repeatedly write to one word, is a classic example: the line bounces around, and all the cores slow down. Multicore Processors and Shared Memory describes the related trap of false sharing. Coherence makes sharing correct, but not cheap, and well-written software shares as little as it can.
Quick Check
Q: A single-threaded program reads a line, and then writes to it. How many interconnect messages does the write need under MSI and under MESI?
Show Answer
Under MSI, one: the line is in S, so an invalidate request must go out, though nobody has a copy. Under MESI, none: the line was loaded in E, so the write moves it to M silently.
Common Mistakes
Mistake #1: Confusing coherence and consistency
Coherence is about one location. Consistency is about the order of accesses to different locations. A coherent system can still let a core see two writes in a surprising order.
Mistake #2: Thinking caches are invisible to parallel performance
Correctness is automatic, while speed is not. Sharing writable data costs a line transfer each time. Measure and reduce how often lines bounce between cores.
Mistake #3: Testing a protocol with simple traffic
Bugs come from rare races between messages, such as a read arriving just as a line is evicted. Random stress tests and formal checks of the state machines are both needed.
Practice Exercise
Challenge: Trace MESI
Two cores share a line x. It starts in I in both, with memory holding 0. Trace each step under MESI, giving the state in each cache, and say whether there is interconnect traffic. (1) Core 0 reads. (2) Core 0 writes 1. (3) Core 1 reads. (4) Core 1 writes 2. (5) Core 0 reads. (6) How many of these steps would still need traffic if core 0 were the only one using the line?
Hint
When a read finds no other copy, the line enters E.
Solution
(1) E, I: read request, memory replies. (2) M, I: silent, no traffic. (3) S, S: read request, core 0 supplies 1, and memory is updated. (4) I, M: invalidate request, core 0 drops its copy. (5) S, S: read request, core 1 supplies 2. (6) With only core 0, just step 1 needs traffic: the read miss. Every other message exists because of sharing.
Summary
- Private write-back caches can hold stale copies, and coherence makes them behave as one memory.
- The key rule is a single writer or multiple readers, per line.
- MSI has Modified, Shared and Invalid states; a write invalidates all the other copies.
- MESI's Exclusive state avoids messages for private data, and MOESI's Owned state avoids memory writes on sharing.
- Coherence is correct by itself but costly under heavy sharing, and it is hard to verify.