Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 5: The CPU

Cache coherency and MESI

Lesson · 11 min

The question

Eight threads each do an atomic increment on one shared counter. Throughput is lower than with one thread. Why?

The problem coherency solves

Each core has private caches. If two cores both hold a copy of a line and one writes it, the other copy is stale. Hardware cache coherency ensures every core sees a single, consistent value for each location: at any moment a line may be cached read-only by many cores, or writable by exactly one.

The protocol behind that on most processors is a variant of MESI, named after the four states a line can be in, in each core's cache.

local writelocal write: invalidate othersother core writesother core readsread missModifiedonly copy, dirtyExclusiveonly copy, cleanSharedothers may have itInvalidnot usable
The four MESI states of one cache line in one core's cache, with the main transitions. Writing a shared line forces every other copy to Invalid; that broadcast is the cost of shared writes.
StateMeaningThis core may
Modifiedonly copy, changed since it was loadedread and write freely
Exclusiveonly copy, unchangedread, and write without telling anyone (becomes Modified)
Sharedother cores may hold copiesread; must invalidate the others before writing
Invalidnot usablemust fetch it again

Intel adds a Forward state and AMD an Owned state to reduce memory traffic, but MESI is enough for reasoning about code.

What a write to a shared line costs

When a core wants to write a line that it holds in Shared state, or does not hold at all, it must gain ownership: it sends a request that invalidates every other copy and waits for the acknowledgements. Only then can the write complete. The next time another core touches the line, it misses, fetches the line from the writer's cache, and the cycle continues.

A line written by several cores in turn therefore travels between them on every write. Each transfer costs roughly what an L3 hit or more costs, tens of nanoseconds on one socket and well over a hundred across sockets, and the transfers serialise: only one core can own the line at a time.

The shared counter

That answers the question. Each fetch_add needs the counter's line in Modified state on the incrementing core. With eight cores incrementing, the line moves continuously, every increment waits for a transfer, and they happen one at a time. One thread incrementing a line that stays in its own L1 is far faster than eight threads queueing for one line.

The fixes are the general ones for contended shared data:

  • Per-thread counters, each on its own cache line, summed when read. Writes become local; only the occasional reader pulls lines across.
  • Batching: accumulate locally and publish occasionally.
  • Ownership: one thread owns the value and others send it messages.

Reading is cheap, writing is not

Many cores can hold a line in Shared state at once and read it from their own caches without any traffic. That is why read-mostly data, such as configuration or reference data, scales perfectly, and why a seqlock or a versioned snapshot works well: readers share the line; only the single writer forces invalidations, and only when something changes.

The asymmetry gives a rule for designing hot data: separate what is written from what is read, and separate what different threads write.

Spinning politely

A thread waiting for a flag set by another core should spin on a plain load, not on an atomic exchange or compare-and-swap. A load keeps the line in Shared state in the waiter's cache and costs nothing until the writer changes it. A read-modify-write in the loop requests ownership every iteration and drags the line away from the thread trying to set it. That is why the spinlock in Module 4 tests with a load before trying the exchange.

Across sockets

On a two-socket machine, coherence traffic between sockets goes over the interconnect, which is several times slower than within a socket. Two threads that share a hot line on different sockets pay that on every transfer. Pinning cooperating threads to cores on the same socket, ideally sharing the same L3, is part of the system setup covered in Module 6.

Check yourself

Question 1 of 3

Several cores hold a line in Shared state. One core writes to it. What must happen first?

Question 2 of 3

Why do eight threads incrementing one atomic counter achieve lower total throughput than one thread?

Question 3 of 3

Why should a spin-wait loop use a load rather than a compare-and-swap in each iteration?

In the interview

Say your own answer out loud first, then open the one we would give.

What is cache coherency?

The hardware guarantee that all cores see one consistent value for each memory location despite private caches. Protocols like MESI allow many read-only copies of a line or a single writable copy; a write invalidates the other copies first.

Why is writing shared data expensive on a multicore machine?

The writing core must gain exclusive ownership of the line, invalidating every other copy and waiting for confirmation. If several cores write the same line, it moves between them on every write and the writes serialise. Reads of unchanging shared data, by contrast, are local and free.

How would you implement a counter incremented by many threads?

A counter per thread on its own cache line, summed when someone reads the total. Writes stay local and nothing is contended. If an exact live total is needed often, I would reconsider the design, since a single shared atomic is a serialisation point.