THIS EXPLANATION
THE ROOM
CDA·92 Computing, Data & AI 6 MIN · 8 STATIONS

False sharing

A Socratic walk-through of false sharing — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why do two processor cores slow each other down when neither one ever touches the other's variable?

Two threads on two cores, each incrementing its own counter in a loop. No lock, no atomic operation, no variable in common: by every reading of the source they are independent.

Yet the pair runs slower than a single thread doing both loops. Nothing in the program says these threads interact, so the interaction is happening somewhere the program cannot see. Where?

b

Reasoning it through

REASONING #

Start with a bookkeeping question that has nothing to do with concurrency. A cache holding a copy of memory must record which memory — an address tag plus state bits. Track single bytes and the tag is larger than the data it describes. So caches track blocks: a cache line, commonly 64 bytes on today's mainstream designs and 128 on some, though the figure is a property of the machine and not a law.

Now add the second core. Both may hold copies of one line, so the machine needs a rule stopping a core from reading a value another has already changed. The rule, in the MESI family of coherence protocols, is ownership: a cached line is Modified, Exclusive, Shared or Invalid, and a core may only write to a line it holds exclusively. Acquiring that means telling every other cache to invalidate its copy.

Put the two facts side by side and the answer falls out. The unit the program cares about is a variable, maybe 8 bytes. The unit the protocol works in is a line, 64. When two variables land in one line the protocol cannot tell them apart — no machinery could, because the state bits describe the line, not its contents.

Trace one round. Core 1 writes counter A, taking the line exclusively, so core 2's copy is invalidated. Core 2 then reads counter B — its own variable, untouched by anyone — and misses, because its copy of the line is gone. It requests the line, taking ownership, which invalidates core 1's copy. Core 1 writes A again and the exchange repeats, every iteration.

The arithmetic that decides whether this happens to you. For a 64-byte line, two addresses share a line exactly when they agree in every bit above the low six — when a >> 6 == b >> 6. Consider the most natural way anyone writes this: an array of per-thread counters, 8 bytes each, thread i at offset 8*i*. Threads 0 through 7 sit at offsets 0 to 56, every one below 64. Eight threads, one line — an array written precisely to keep threads apart has put all eight in the worst possible place. The fix is to pad every entry to the line size and align the array to a line boundary, so thread i sits at offset 64*i*. The cost is exact: 512 bytes instead of 64, an eightfold expansion to hold the same eight numbers.

Worth it? A hit in a core's own first-level cache is a handful of cycles; a line taken from another core's cache costs tens within a die and more across sockets. I will not attach vendor numbers, which vary by generation and topology, but the ladder is set out in this collection's piece on caching and the gap is at least an order of magnitude.

Underneath the latency there is a worse effect, and it is why the pair can lose to a single thread. Only one core may hold the line for writing at any instant, so N writers on one line do not run in parallel and slowly — they run in a queue. False sharing converts parallel work into serial work and charges coherence traffic on top. Adding cores makes it worse, which is the signature separating it from ordinary slowness.

The falsification test. Run the identical program twice, altering only the offset of the counters: 8 bytes apart, then padded and aligned to the line size. Algorithm, instruction count and working set are essentially unchanged; only the addresses moved. The prediction is a large runtime difference in the padded direction, with coherence-miss counters falling in step. There is a positive control too: the account requires at least one writer, since several caches may hold a line in the Shared state at once, so making every access a read must abolish the effect. The refuting observation is padding that changes nothing, or a slowdown surviving separation onto different lines — either would put the cause elsewhere, in a real lock, allocator contention, remote-node placement, or bandwidth saturation.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a library that lends by the shelf rather than the book, because a card for every volume would cost more than the shelves do. Two readers want different books that sit side by side. Each request recalls the whole shelf from the other reader, who must send it back and ask for it again to reach a book the first never opened.

WHERE IT BREAKS DOWN

the library's rule is a policy someone chose and could revoke, whereas line granularity is a cost constraint — per-byte coherence state would consume more silicon than the data it tracked.

d

Clarifying the model

THE MODEL #

Three things to hold apart.

First, false sharing is not sharing. True sharing is several threads hitting the same variable: inherent contention, which no padding helps and which wants an algorithmic answer such as per-thread accumulation with one combining step at the end. False sharing is contention manufactured by layout alone, and layout is what you can change.

Second, this is the bill for a bet made elsewhere. The piece on caching argues that moving data in blocks is a sound wager on spatial locality — fetch the neighbours, they will probably be wanted. That is excellent for one core walking an array, and false sharing is exactly its cost when two cores write into one block. Same mechanism, opposite sign; the fixed point of difference is the number of writers.

Third, name the constraint rather than the technology. Two durable facts do the work: coherence must be maintained at a granularity coarser than a byte, and moving ownership between cores costs far more than a local hit. Sixty-four bytes is this decade's number — which is why hardcoding 64 into a padding macro is a portability bug, when the runtime can report the interference size and 128-byte lines exist today.

Two caveats. Padding is not free: it inflates the working set, so a structure padded everywhere can lose more to capacity misses than it gains. And adjacent-line prefetchers mean one line's width is occasionally not enough. Measure the fix.

e

A picture of it

THE PICTURE #
False sharing
False sharing Each row is one 64-byte cache line, drawn to scale left to right. In the top row the counters share a line, so the coherence protocol -- which owns and invalidates whole rows, never cells -- cannot let core 1 write the first cell without taking the second from core 2. The rows beneath are the same counters after padding and alignment: each holds a line of its own, and the empty space is the price. The whole fix is a change of address, with no change to the code reading or writing either counter. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/false-sharing.md","sourceIndex":1,"sourceLine":4,"sourceHash":"eafe16cf8f4c84aae81e08d2309008337e164f1b18ecd1ef6609630200672d67","diagramType":"block","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":273},"qa":{"passed":true,"findings":[]}} One line counter A counter B 48 bytes unused Line n counter A 56 bytes of padding Line n plus 1 counter B 56 bytes of padding

How to readEach row is one 64-byte cache line, drawn to scale left to right. In the top row the counters share a line, so the coherence protocol — which owns and invalidates whole rows, never cells — cannot let core 1 write the first cell without taking the second from core 2. The rows beneath are the same counters after padding and alignment: each holds a line of its own, and the empty space is the price. The whole fix is a change of address, with no change to the code reading or writing either counter.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Nothing was shared; a container was. Caches track ownership by line because per-byte bookkeeping would cost more than the data, so coherence grants and revokes whole lines. Two independent variables falling inside one line inherit each other's invalidations, and the pair serialises on a line neither meant to share. The cure is arithmetic on addresses rather than logic in the program — pad and align so a >> 6 and b >> 6 differ.

g

Where to go next

ONWARD #
  • How per-core accumulation with a final reduction removes true sharing, and when the combining step becomes the bottleneck.
h

Key terms

TERMS #
TermWhat it means
Cache linethe fixed-size block, commonly 64 bytes, that a cache tracks, transfers and owns as a unit.
True sharinggenuine contention for the same variable, which layout changes cannot remove.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4