Cache Lines and False Sharing
CPUs move memory in fixed 64-byte chunks called cache lines, and when two threads write to unrelated variables that happen to share a line, they end up fighting over it as if they shared the data itself.
Prerequisites: The CPU Cache Hierarchy
A CPU never fetches a single byte or even a single variable from memory on its own — it always pulls a fixed-size block, typically 64 bytes on modern x86 chips, called a cache line. Ask for one int, and the CPU brings in that int's entire 64-byte neighborhood, on the assumption that nearby memory will probably be needed soon too. Most of the time this assumption pays off handsomely; it's the whole reason sequential array access is so much faster than scattered pointer chasing.
It backfires in a specific, easy-to-miss way with multithreaded code. Cache coherency protocols keep each CPU core's cached copy of a line consistent with every other core's copy — if core 1 writes to a line, every other core's cached copy of that same line is invalidated, even if the actual bytes that other core cares about didn't change. Two threads writing to two completely unrelated variables that happen to sit in the same 64-byte line will invalidate each other's cache constantly, a phenomenon called false sharing — "false" because the threads share no logical data, only physical memory layout.
False sharing is a performance bug with no logic bug behind it — the program is correct, computes the right answer, and simply runs far slower than it should, because two threads keep evicting each other's cache line over variables that have nothing to do with each other except an accident of memory layout.
Why it happens
Worked example
Four threads each increment their own dedicated counter, 100 million times, in a tight loop. If the four int64_t counters are declared as adjacent elements of a plain array (int64_t counters[4]), all four sit inside one or two 64-byte cache lines. Benchmarked, this configuration takes roughly 3.5 seconds — each thread's increment invalidates the line for the other three, forcing constant cross-core cache traffic even though the threads never touch each other's data.
Pad each counter out to its own 64-byte cache line — struct alignas(64) PaddedCounter { int64_t value; char pad[56]; } — and rerun the identical benchmark: it drops to roughly 0.9 seconds, close to a 4x improvement. Nothing about the algorithm changed; only the memory layout did, removing the artificial contention.
What this means in practice
False sharing hides in exactly the places low-latency systems put hot, frequently-updated data: per-thread counters, per-core statistics, spinlock flags packed next to the data they guard. The fix is almost always padding or alignment — forcing each independently-written piece of data onto its own cache line with alignas(64), at the cost of some wasted memory, which is a trade low-latency systems make gladly.
False sharing produces no compiler warning, no incorrect output, and no obvious symptom in a profiler beyond "this code is mysteriously slower than it should be" — it only shows up as excess time in hardware counters for cache-coherency traffic (things like MESI protocol invalidations). If parallel code scales worse than expected with more threads despite no shared logical state, checking memory layout for accidental line-sharing should come before suspecting the algorithm.
Related concepts
Practice in interviews
Further reading
- Drepper, 'What Every Programmer Should Know About Memory'