You parallelize a counter-per-thread loop. No locks, no shared variables — each thread writes its own slot. It should scale linearly. Instead, eight threads are slower than one. Welcome to false sharing: the performance bug where memory that isn’t shared behaves as if it is.

Cores Don’t Trade Bytes, They Trade Cache Lines

A CPU never moves a single byte between cores. The unit of coherence is the cache line — 64 bytes on most x86. When a core writes any byte in a line, the cache-coherence protocol (MESI) must invalidate that line in every other core’s cache:

text
   one 64-byte cache line
   ┌───────────────────────────────────────┐
   │ counter[0] counter[1] ... counter[7]  │   ← all on ONE line
   └───────────────────────────────────────┘
   thread 0 writes counter[0]  → invalidates the line everywhere
   thread 1 writes counter[1]  → invalidates it again

The variables are independent, but they ride the same line. Every write by any thread evicts the line from every other core, forcing a re-fetch across the cache hierarchy. The line ping-pongs between cores at memory speed.

Seeing It

The classic reproduction — an array of per-thread counters:

c
struct { long count; } counters[8];   // 8 longs = 64 bytes = ONE line

// thread i hammers counters[i].count
for (long j = 0; j < 100000000; j++)
    counters[i].count++;

This will scale negatively. The fix is to push each counter onto its own line:

c
struct {
    long count;
    char pad[64 - sizeof(long)];   // pad out to a full cache line
} counters[8];

Now each thread owns a private line — no invalidation traffic, and the loop scales the way you expected.

text
   before:  [c0 c1 c2 c3 c4 c5 c6 c7]          one contested line
   after:   [c0 ......][c1 ......][c2 ...] ... one line each

Doing It Cleanly

Hand-padding is error-prone. C++ gives you the line size and an alignment tool:

cpp
#include <new>
struct alignas(std::hardware_destructive_interference_size) Counter {
    long count;
};

In C, alignas(64) plus padding does the same job.

How to Catch It

You can’t see false sharing in the code — only in the hardware counters. perf exposes the coherence traffic:

bash
perf c2c record ./your_program     # cache-to-cache transfer analysis
perf c2c report                    # points at the contended cache lines

A workload that should scale but flatlines, with high cache-coherence or HITM events, is the signature.

Takeaways

  • Cores exchange whole 64-byte cache lines, not individual variables.
  • False sharing happens when independent data lands on one line, so each write invalidates the line in every other core — turning parallel code serial.
  • Pad or align hot per-thread data to its own cache line to fix it.
  • Use perf c2c to find it; it’s invisible in source but obvious in coherence counters.