Skip to article
ALGORITHMICSConcurrency
Concurrency5 min read

False Sharing

Two threads with no shared variable, made slow by sharing 64 bytes.


You split a counting job across four threads, each with its own counter, no locks, no shared state.

It runs slower than the single-threaded version.

There is no bug in the code. The problem is that “its own counter” was not as separate as it looked.

Caches work in lines, not variables

A CPU never fetches one byte. It fetches a cache line — 64 bytes on every common architecture — and that line is the unit of everything: fetching, invalidating, and coherence between cores.

const counters = new Int32Array(4); // 16 bytes: all four in one line

Four separate counters, one cache line.

Layout
line 0
line 1

64-byte cache lines · core A's counter · core B's counter

Two independent counters, one cache line. Every increment by A invalidates B's copy of the whole line and vice versa, so the two cores ping the line back and forth — often 5–10× slower than the single-threaded version, with no shared variable and no bug.

What the hardware does about it

Cores must agree on memory, so they run a coherence protocol. Simplified:

  1. Core 0 writes counters[0]. To do so it must own the line exclusively.
  2. That invalidates core 1’s copy of the line.
  3. Core 1 writes counters[1]. It must fetch the line back and take exclusive ownership.
  4. Which invalidates core 0’s copy.
  5. Repeat, millions of times per second.

The line ping-pongs between cores’ caches. Each bounce costs tens to hundreds of cycles — comparable to a main-memory access — for an operation that should have been one cycle in L1.

The fix is padding

Give each thread’s data its own line.

// 16 int32s per counter = 64 bytes = one full cache line.
const STRIDE = 16;
const counters = new Int32Array(4 * STRIDE);
function bump(thread: number) {
counters[thread * STRIDE] += 1;
}

Wasteful, and the waste is 240 bytes. The speedup is routinely 5–10×.

Most languages have a way to say this properly:

@Contended long counter; // Java, with -XX:-RestrictContended
#[repr(align(64))] struct Padded(AtomicU64); // Rust
alignas(std::hardware_destructive_interference_size) std::atomic<long> counter;

That C++ constant is the standard library admitting the line size is a portable concern.

Recognising it

The signature is distinctive: scaling gets worse as you add threads, and profilers show no lock contention because there is none.

perf c2c on Linux detects it directly, reporting which cache lines are bouncing and which source lines touch them. perf stat -e cache-misses and Intel VTune’s memory analysis also show it.

Without a profiler, the cheap test is to pad and re-measure. If padding four variables makes the program five times faster, that was false sharing.