Table of Contents

False sharing

False sharing is a performance bug where two threads modify logically unrelated variables that happen to sit on the same Cache line, causing the Cache coherence protocol to bounce the line back and forth between their caches as if they were actually contending for the same data. Nothing is functionally wrong: each thread only ever reads and writes its own variable. The slowdown is purely an artifact of cache-line granularity.

struct counters {
    atomic_int a;   // written by thread 1
    atomic_int b;   // written by thread 2, but shares a line with a!
};

If a and b fall within the same 64-byte line, a write to a invalidates thread 2's cached copy of the whole line (including b) under MESI-style protocols, even though thread 2 never touches a. Thread 2's next read or write to b then misses and has to fetch the line again, and the pattern repeats every time either thread writes, turning what should be two independent, uncontended operations into a stream of coherence traffic.

Diagnosing it

False sharing is invisible in the source code, since there's no data race and no incorrect output, only a mysterious slowdown that gets worse with more threads instead of better. It typically shows up as unexpectedly high cache-miss or coherence-traffic counters in a hardware profiler (perf c2c on Linux is built specifically to surface this: it identifies cache lines with high cross-core contention and maps them back to the offending struct fields).

Fixing it

The standard fix is padding: inserting unused bytes between fields so that variables written by different threads land on different cache lines.

struct counters {
    atomic_int a;
    char pad[60];   // push b onto the next 64-byte line
    atomic_int b;
};

C++17's std::hardware_destructive_interference_size and similar constants in other languages exist to make this padding portable across architectures with different line sizes, rather than hardcoding 64 everywhere. The tradeoff is memory: padding a small, frequently-allocated struct to cache-line boundaries can noticeably inflate its footprint, which matters when many instances exist (per-thread counters in a large thread pool, for example) even though it's a non-issue for a handful of long-lived globals.