CPU cache coherence simulator
Every core keeps its own copy of the memory it is using, so the cores need a protocol to stop those copies disagreeing. Run reads and writes on four cores, watch each cache line move between the MESI states, and see what it costs when cores fight over a line.
Modified Exclusive Shared empty or Invalid
Each line is 64 bytes, 8 words of 8 bytes. Example costs in cycles: 4 for an L1 hit, 30 to upgrade a Shared line, 40 from the shared last-level cache, 60 when another core must supply a Modified line, 200 from memory.
- –Press Play, One round, or Go.
The number on the left is the operation's cost in cycles.
- L1 hits
- 0 of 0
- Bus messages
- 0
- Write-backs
- 0
- Core-to-core transfers
- 0
How to use the simulator
Pick a workload and press Play or One round. In each round every core performs its part of the workload. The four columns show each core's private cache, with the state of every line it holds; the log lists each operation with its cost and the bus messages it caused. You can also issue a single read or write from any core with the controls on the right of the toolbar.
- False sharing: each core writes its own counter, but all four counters are in line 0. Every write has to take the line away from the core that wrote last.
- Padded counters: the same counters, each on its own line. After the first write, every write is an L1 hit with no bus traffic.
- Read-only sharing: all four cores read the same line. It ends up Shared in every cache and stays there.
- One shared counter and Producer and consumer show real sharing, where the traffic is unavoidable.
The protocol is tested on random sequences of reads and writes: every read returns the value most recently written, and a line is never held Modified or Exclusive by one core while another core holds any copy.
What is a CPU cache?
A CPU cache is a small, fast memory on the processor that holds copies of recently used data from main memory. Reading main memory takes hundreds of cycles; the closest cache, L1, answers in a few. Modern processors have several levels: each core usually has its own L1 and L2, and the cores share a larger, slower L3.
Caches do not store single bytes. They move memory in fixed blocks called cache lines, 64 bytes on current x86 processors from Intel and AMD, so reading one byte brings its neighbours along. That makes sequential access fast, and it is also the reason two unrelated variables can interfere with each other if they share a line.
Why caches need coherence
If two cores both cache the same line and one of them writes it, the other core's copy is now stale. Without a protocol, it could go on reading the old value indefinitely, and two cores writing the same line could each lose the other's update. Cache coherence rules this out. The usual way to state it is single-writer, multiple-reader: at any moment a line may have one core that can write it, or any number of cores that can read it, but not both.
The hardware enforces this by having caches tell each other what they are doing. In a snooping design, every cache watches a shared bus or interconnect and reacts to the requests of the others; in a directory design, a central record tracks which caches hold each line and sends messages only to those.
The MESI protocol
MESI gives each line in each cache one of four states, named by their initials:
| State | Meaning | Can read | Can write |
|---|---|---|---|
| Modified | Only copy, changed since it was loaded; memory is out of date | Yes | Yes |
| Exclusive | Only copy, same as memory | Yes | Yes, becoming Modified without any message |
| Shared | One of possibly several copies, same as memory | Yes | Only after invalidating the others |
| Invalid | No usable data | Must fetch first | Must fetch first |
The transitions follow from what each request needs:
- Read miss. The cache asks for the line (BusRd). If no other cache has it, it arrives Exclusive. If others have it, everyone ends up Shared; if one of them had it Modified, that cache supplies the data and writes it back to memory.
- Write to a Shared line. The cache broadcasts an upgrade request; every other copy becomes Invalid, and the line becomes Modified.
- Write miss. The cache asks for the line with intent to modify (BusRdX), invalidating every other copy, and the line arrives Modified.
- Eviction. A Modified line must be written back to memory before it is dropped; clean lines are dropped silently.
The Exclusive state is what MESI adds over the simpler MSI protocol. A program that reads data and then writes it, with no other core involved, gets the line Exclusive on the read and can write it without sending any message at all.
False sharing
Coherence works on whole lines. When threads on different cores write different variables that share a line, every write invalidates the other cores' copies, and the line moves from cache to cache on almost every write, even though the threads never touch each other's data. This is false sharing. It shows up as multithreaded code that gets slower, not faster, as threads are added.
The simulator's workloads, 100 rounds each, with its example latencies:
| Workload | Cycles per op | L1 hits | Invalidations | Core-to-core transfers |
|---|---|---|---|---|
| False sharing | 60.4 | 0 of 400 | 399 | 399 |
| Padded counters | 6.0 | 396 of 400 | 0 | 0 |
| One shared counter | 45.1 | 1 of 800 | 399 | 399 |
| Read-only sharing | 4.8 | 396 of 400 | 0 | 0 |
| Producer and consumer | 45.9 | 0 of 200 | 99 | 100 |
The false-sharing and padded workloads do exactly the same work. Moving each counter to its own line takes the average cost per write from 60.4 to 6.0 cycles here. Real slowdowns depend on the processor and on how often the threads write, but they are commonly large.
The fixes all put data written by different threads on different lines: padding or aligning per-thread structures to the line size, giving each thread its own copy and combining the results at the end, or ordering a structure's fields by which thread writes them. C++17 provides std::hardware_destructive_interference_size for padding, Java has the @Contended annotation, and the Linux kernel uses ____cacheline_aligned_in_smp. On Linux, perf c2c finds lines that are bouncing between cores in a running program.
Snooping and directories
The simulator uses a snooping bus: every request is seen by every cache. That is simple and fast with a few cores, but the traffic grows with the number of cores, since every miss must be checked by all of them. Larger processors use directories or snoop filters, which record which caches may hold each line so that requests go only to those. Multi-socket servers extend the same idea across sockets, where a transfer between caches costs considerably more than one within a chip.
MOESI, MESIF and others
MESI is the basis for most protocols in use, with extra states for efficiency. MOESI, used in AMD processors, adds an Owned state: a cache can share a modified line with others without first writing it back to memory, and stays responsible for writing it back later. MESIF, used by Intel, adds a Forward state that picks one of the sharers to answer requests for the line, so a request does not get a reply from every cache that holds it.
Coherence is not consistency
Coherence is about one memory location: all cores agree on the order of writes to it. It says nothing about the order in which a core's writes to different locations become visible to others. That is the job of the processor's memory consistency model. To go faster, cores buffer stores and reorder memory operations, so another core can observe them in a different order from the program. Concurrent code relies on atomic operations and memory fences, or on language-level atomics and locks that use them, to get the ordering it needs.
History
Multiprocessors with private caches needed coherence from the start. James Goodman described the write-once protocol in 1983, and Mark Papamarcos and Janak Patel of the University of Illinois published the protocol now called MESI in 1984, which is why it is also known as the Illinois protocol. Its key addition was the Exclusive state, which saves a bus transaction whenever a core writes data that no other core has read.
Cache coherence questions and answers
What is cache coherence?
Each CPU core has its own small, fast caches, so the same memory location can be cached by several cores at once. Cache coherence is the guarantee that they never disagree: once a core writes a location, no other core can go on reading the old value. Hardware enforces it with a coherence protocol such as MESI.
What do the MESI states mean?
Modified: this cache has the only copy and it differs from memory. Exclusive: the only copy, unchanged from memory. Shared: one of possibly several unchanged copies. Invalid: the line holds nothing usable. A core may write a line only in the Modified or Exclusive state; to write a Shared line it must first invalidate every other copy.
What is false sharing?
False sharing happens when cores write different variables that happen to sit in the same cache line. The protocol works on whole lines, so each write invalidates the other cores’ copies and the line bounces between caches, even though no data is actually shared. In this simulator, four cores each incrementing their own counter in one line average 60.4 cycles per write; with each counter on its own line, 6.0.
How do you fix false sharing?
Put data written by different threads on different cache lines: pad or align per-thread structures to the line size (64 bytes on most x86 processors), use per-thread copies and combine them at the end, or group fields by which thread writes them. C++17 provides std::hardware_destructive_interference_size for this, and Java has the @Contended annotation.
How big is a CPU cache line?
On current x86 processors from Intel and AMD it is 64 bytes. Some designs use larger lines; Apple’s M-series chips report 128 bytes. The line is the unit caches load, store and keep coherent, which is why two variables within the same line interact.
What are MOESI and MESIF?
Extensions of MESI. MOESI, used by AMD, adds an Owned state so a modified line can be shared without first being written back to memory. MESIF, used by Intel, adds a Forward state that designates one sharer to answer requests, so only one cache replies instead of all of them.
Further reading
- Papamarcos and Patel, ISCA, 1984A low-overhead coherence solution for multiprocessors with private cache memoriesThe paper that introduced the MESI protocol.
- Ulrich Drepper, 2007What Every Programmer Should Know About MemoryCaches, coherence and their effect on program performance, with measurements.
- Linux manual pageperf c2cFinding cache lines shared between cores in a running program.