Free — no signup required

False Sharing (The Noisy Neighbor Problem)

3 min read

When Sharing Goes Wrong

We know caches transfer data in 64-byte chunks (cache lines). Now consider what happens in a multi-core system when two different threads on two different CPU cores try to modify independent variables that just happen to sit next to each other within the same 64-byte cache line.

The Scenario:

  1. Thread A on Core 0 wants to increment variable counterA. The CPU loads the 64-byte cache line containing counterA into Core 0's L1 cache and marks it as Modified (using the MESI cache coherence protocol—Modified, Exclusive, Shared, Invalid).
  2. Thread B on Core 1 wants to increment variable counterB. Unfortunately, counterB is right next to counterA in memory—within the same 64-byte cache line.
  3. For Core 1 to write to that line, the MESI protocol requires Core 0 to flush its modified copy back to a shared cache level (L3 or RAM) and transition the line to Invalid. Core 1 then loads the line and marks it as Modified.
  4. Now Core 0 wants to increment counterA again. The same invalidation dance happens in reverse.
  5. They get stuck in a loop of constantly invalidating each other's caches—a "ping-pong" effect.

The Result: Even though the threads aren't logically sharing any data—counterA and counterB are completely independent—the hardware sees them fighting over the same physical cache line. Each write forces a round-trip to L3 cache or beyond (~40–100 cycles), turning what should be a 1-cycle L1 write into a 50–100 cycle ordeal. This is False Sharing, and it can make a multi-threaded program run slower than a single-threaded one.

How to Detect False Sharing

On Linux, the perf c2c (cache-to-cache) subcommand is purpose-built for this: it records cache-line-level access patterns across cores, then reports which lines are being bounced between them and which variables live on those lines. Look for lines with high HITM (Hit In a Modified state) counts—these are the false sharing victims.

The Fix: Padding to Separate Cache Lines

The standard fix is to insert padding bytes between the conflicting variables to ensure they land on different 64-byte cache lines:

// Before: false sharing — both counters share one 64-byte cache line
struct Counters {
    volatile long counter_a;   // bytes 0–7
    volatile long counter_b;   // bytes 8–15  ← same cache line as counter_a
};

// After: padded to separate cache lines
struct Counters {
    volatile long counter_a;   // bytes 0–7
    char padding[56];          // bytes 8–63  ← fills the rest of the cache line
    volatile long counter_b;   // bytes 64–71 ← now on a different cache line
};

In Java, the @jdk.internal.vm.annotation.Contended annotation (with the -XX:-RestrictContended JVM flag) achieves the same effect automatically. In Go, insert a _ [64]byte field between the conflicting fields. In C++17 and later, use alignas(64) on the struct or individual fields.

Key Point: False sharing is insidious because it doesn't show up in code reviews or algorithmic analysis. The code looks perfectly correct and well-parallelized. The only symptom is that adding more threads makes things slower instead of faster. If you ever see a multi-threaded workload where doubling the thread count yields zero speedup (or negative speedup), false sharing should be one of the first suspects—right alongside lock contention and Amdahl's Law limitations.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free