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:
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 againThe 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:
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:
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.
before: [c0 c1 c2 c3 c4 c5 c6 c7] one contested line
after: [c0 ......][c1 ......][c2 ...] ... one line eachDoing It Cleanly
Hand-padding is error-prone. C++ gives you the line size and an alignment tool:
#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:
perf c2c record ./your_program # cache-to-cache transfer analysis
perf c2c report # points at the contended cache linesA 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 c2cto find it; it’s invisible in source but obvious in coherence counters.