False Sharing Explained: The Silent Multithreading Bug
False sharing happens when threads on different cores write to unrelated variables that share one cache line, silently wrecking performance.
False sharing is a performance bug where two threads running on different CPU cores repeatedly invalidate each other’s cache by writing to different variables that happen to sit on the same cache line. The threads aren’t actually sharing any data — the sharing is an artifact of memory layout, not program logic — but the CPU’s cache coherence protocol treats it exactly like real contention, and throughput collapses accordingly.
Why it happens
CPUs don’t move memory in single-byte or single-variable units. They move it in fixed-size chunks called cache lines, typically 64 bytes, as covered in how CPU caches work. Whenever a core reads or writes a variable, the whole 64-byte line containing it gets pulled into that core’s cache.
The trouble starts when two variables that are logically unrelated — say, counters for two different threads — happen to land within the same 64-byte line, usually because they’re adjacent fields in a struct or array. Now, even though thread A only ever touches variable A and thread B only ever touches variable B, both threads are operating on the same cache line.
That’s where cache coherence protocols like MESI come in. When core B writes to its copy of the shared line, the protocol invalidates every other core’s copy of that line — including core A’s, even though core A’s variable didn’t change. The next time core A touches its variable, it has to reload the line from a farther cache level or from another core, at far higher latency than a private L1 hit. Then core A writes back, invalidating core B’s copy, and the cycle repeats — cache line ping-pong, purely from proximity.
A concrete example
Picture two threads, each incrementing its own counter millions of times in a tight loop, with the counters declared as adjacent fields in the same struct:
struct Counters {
long thread_a_count;
long thread_b_count;
};
Logically, there’s zero shared state — each thread only ever touches its own field. But thread_a_count and thread_b_count are only 8 bytes apart, well within a single 64-byte cache line. Every increment from either thread invalidates the other’s cached copy of that line. Run this on two cores and the counters can be an order of magnitude slower to update than if each lived on its own cache line — despite doing the exact same arithmetic.
This is the trap: nothing about the code looks wrong. There’s no shared mutable state, no missing lock, no data race. The bug lives entirely in memory layout, invisible in the source unless you know to look for it.
How to detect it
False sharing doesn’t throw an error or produce incorrect results — it just quietly caps throughput far below what the hardware should deliver, which makes it easy to misdiagnose as “the algorithm is slow” or “we need more threads.” The tell is usually a profiler: hardware performance counters that track cache invalidations, L2/L3 cache misses, or coherence traffic will show abnormally high activity on lines that shouldn’t be contended at all. If scaling from one thread to several produces far less speedup than the core count would suggest, and the workload has no obvious lock contention, false sharing on shared or adjacent data structures is a strong suspect.
How to fix it
The fix is almost always padding or alignment — forcing each thread’s hot variable onto its own cache line so writes from one core never touch a line another core cares about:
struct Counters {
alignas(64) long thread_a_count;
alignas(64) long thread_b_count;
};
The alignas(64) directive pads each field out to start on a fresh 64-byte boundary, so thread_a_count and thread_b_count end up on physically separate cache lines even though they’re logically part of the same struct. This trades memory for throughput — each padded field now consumes a full cache line instead of a few bytes — which is almost always worth it for hot, frequently written per-thread state. The same principle shows up in SIMD and NUMA-aware code: memory layout, not just algorithmic correctness, determines real-world throughput once multiple cores are involved.
False sharing vs true sharing
| True sharing | False sharing | |
|---|---|---|
| Data is actually shared | Yes | No — only the cache line is |
| Fix | Reduce contention (locks, algorithm change) | Pad or align data structures |
| Correctness impact | Requires synchronization to be correct | Already correct — purely a performance issue |
| Visible in the source | Usually, via shared variables or locks | Rarely — depends on memory layout |
| Common in | Shared counters, queues, locks | Structs of arrays, per-thread stats adjacent in memory |
Where it matters most
False sharing is most costly in high-throughput, low-latency systems: lock-free queues, per-thread statistics collectors, worker pools that write to a shared results array, and any code using SharedArrayBuffer and Atomics or worker threads for real parallelism. In JavaScript specifically, false sharing is less commonly discussed because most concurrency there doesn’t touch shared memory directly — but it becomes a live concern the moment SharedArrayBuffer is in play, since that’s exactly the kind of manually laid-out shared memory where adjacent fields can collide on a cache line.
The takeaway
False sharing turns independent, uncontended memory accesses into a performance bottleneck purely because the underlying variables share a 64-byte cache line, triggering the same coherence invalidation traffic as if the threads were genuinely fighting over the same data. It produces no incorrect results and no compiler warning — only a mysterious ceiling on multithreaded scaling. The fix is to pad or align hot per-thread data so each thread’s working set lives on its own cache line, at the cost of a little extra memory for a lot of recovered throughput.
Keep reading
Chisato · · 4 min read Thermal Interface Materials Explained
Thermal interface material fills microscopic gaps between a chip and its heatsink so heat can actually transfer to the cooler.
Chisato · · 4 min read Clock Speed vs. IPC: What Actually Makes a CPU Fast
Clock speed measures cycles per second; IPC measures work done per cycle. Real CPU performance is the product of both, not either one alone.
Chisato · · 4 min read UMA vs NUMA: Memory Architecture Explained
UMA gives every CPU core equal-latency memory access; NUMA gives each core faster access to its local memory bank. How the two architectures differ.