How Does False Sharing Degrade Multicore Performance?
False sharing occurs in multicore processors when threads running on different cores modify logically independent variables that happen to reside on the same CPU cache line. Even though the threads do not share data at the program level, the hardware cache coherence protocol treats the entire cache line as a single unit, causing repeated invalidations and stalls that severely degrade parallel performance.
Cache Lines and Coherence Protocols
Modern CPUs do not transfer individual bytes between main memory and processor caches. Instead, they move fixed-size blocks called cache lines, which are typically \(64\) bytes wide on x86-64 and ARM architectures. When a core reads or writes a variable, it loads the entire \(64\)-byte cache line containing that variable into its private L1 or L2 cache.
To keep private caches consistent across multiple cores, hardware uses cache coherence protocols such as MESI (Modified, Exclusive, Shared, Invalid). Under MESI, when one core writes to a memory address, it must obtain exclusive ownership of the corresponding cache line. This requires sending an invalidation message over the interconnect to all other cores that hold a copy of that line, marking their copies as Invalid.
The Mechanism of False Sharing
False sharing arises when two or more threads on different cores
access different variables located within the same \(64\)-byte block. For example, consider an
array of counters counters[N] where thread \(0\) increments counters[0] and
thread \(1\) increments
counters[1]. Because each integer is typically \(4\) or \(8\) bytes, both elements sit on the same
cache line.
When Core \(0\) writes to
counters[0], the coherence protocol invalidates the cache
line in Core \(1\). Moments later, when
Core \(1\) attempts to write to
counters[1], it suffers a cache miss because its copy of
the line is now Invalid. Core \(1\)
must stall, request the updated cache line from Core \(0\) (or shared L3 cache), and invalidate
Core \(0\)'s copy. This back-and-forth
ownership transfer, often called cache line ping-ponging, repeats on
every write.
Why Performance Degrades
The performance penalty of false sharing comes from several compounding factors:
- Pipeline Stalls: Cores must wait dozens to hundreds of clock cycles for cache line transfers across the interconnect instead of executing instructions from their local L1 cache.
- Interconnect Saturation: Frequent invalidation messages and data transfers consume memory bus and ring/mesh interconnect bandwidth, slowing down unrelated memory operations.
- Negative Scaling: Adding more cores to a workload afflicted by false sharing often makes execution slower rather than faster, because contention on the shared cache line grows with the number of writing threads.
How to Prevent False Sharing
Eliminating false sharing requires separating independent, frequently written variables onto different cache lines or avoiding shared writes altogether:
- Padding and Alignment: Align per-thread data
structures to the cache line size (e.g.,
alignas(64)in C++ or#[repr(align(64))]in Rust) so each thread's variables occupy distinct cache lines. - Thread-Local Accumulation: Have each thread accumulate results in a local register or stack variable, then write the final value to shared memory once after the loop finishes.
- Data Layout Restructuring: Group read-only fields separately from frequently modified fields, and avoid packing independent thread state into contiguous arrays without spacing.