← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

False Sharing

Performance Advanced
debt(d8/e3/b3/t8)
d8 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'silent in production until users hit it' (d9), scored d8 because specialist tools like perf c2c and Intel VTune can detect it (HITM events), but no linter or compiler warns and it manifests only as unexplained slowness at runtime.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3), because the quick_fix is adding alignas(64) or padding to a struct — a localised change to the affected data structure, not a one-line swap but not cross-cutting either.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3), because the cache-line-alignment concern applies to specific hot data structures (per-thread counters, ring buffer indices) rather than shaping the whole codebase; scope is library/cli/queue-worker hot paths.

t8 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7), scored t8 because the misconception is fundamental: developers reason at variable granularity while hardware operates at cache-line granularity, so 'each thread writes its own variable' feels obviously safe but is catastrophically wrong for performance.

About DEBT scoring →

Also Known As

cache line contention cache line ping-pong cache coherence contention

TL;DR

A cache coherence pathology where threads on different cores modify independent variables sharing a cache line, causing costly invalidations.

Explanation

False sharing is a subtle performance bug that occurs on multi-core systems when two or more threads modify logically independent variables that happen to reside on the same CPU cache line (typically 64 bytes on x86-64). Even though the threads never touch each other's data, the cache coherence protocol (MESI or similar) must invalidate the entire cache line on other cores whenever any core writes to it. This turns what looks like embarrassingly parallel code into a memory bus bottleneck, often making the multi-threaded version slower than the single-threaded one.

The classic example is an array of per-thread counters where each thread increments its own slot. Logically these operations are independent, but if counters[0] through counters[7] fit within one 64-byte cache line, every increment forces a cross-core cache line transfer. Symptoms include: multi-threaded code that fails to scale with core count, unexpectedly high CPU cache miss rates in profilers (perf's cache-misses or LLC-load-misses counters), and performance that improves dramatically when unrelated struct fields are reordered or padded.

Detection typically requires hardware performance counters - tools like Linux perf, Intel VTune, or AMD uProf can highlight HITM (hit-modified) events indicating cross-core cache line contention. The fix is to force each thread's data onto its own cache line via padding, alignment attributes (alignas(64) in C++, #[repr(align(64))] in Rust, @Contended in Java), or by using thread-local storage. In garbage-collected languages, be aware that the runtime may still colocate objects. False sharing is language-agnostic - it affects C, C++, Rust, Java, Go, and any runtime that runs threads on real cores.

Common Misconception

Because each thread writes to its own variable, there is no contention and no synchronisation cost. In reality, the CPU tracks ownership at cache line granularity, not variable granularity, so physically adjacent variables contend even when they are logically independent.

Why It Matters

False sharing can silently make multi-threaded code 10x slower than expected, defeating the purpose of parallelism and wasting hardware capacity that scales with core count.

Common Mistakes

  • Placing per-thread counters in a small contiguous array without padding so multiple slots share a cache line.
  • Interleaving hot mutable fields with other frequently-written fields in a struct without considering the 64-byte cache line boundary.
  • Assuming lock-free atomic counters have no contention cost - atomics still trigger coherence traffic when they share a line.
  • Reading and writing adjacent elements of an array from different threads in parallel loops (e.g. OpenMP with static scheduling of tiny chunks).
  • Ignoring cache line alignment in ring buffers where producer and consumer indices sit next to each other.

Avoid When

  • Single-threaded code - there is no coherence traffic to eliminate.
  • Read-mostly data shared across cores - shared reads do not invalidate cache lines, so padding wastes memory.
  • Cold code paths where the increased memory footprint from padding hurts more than the rare contention.
  • Small embedded targets where memory is scarce and cache lines may be smaller (e.g. 32 bytes) or coherence is not an issue.

When To Use

  • Hot per-thread counters, statistics, or accumulators written millions of times per second.
  • Producer/consumer ring buffer indices where head and tail are written by different threads.
  • Per-CPU or per-worker data structures in queue workers or thread pools that fail to scale linearly.
  • Any struct where hardware performance counters show high HITM or cross-core cache line transfer rates.

Code Examples

✗ Vulnerable
// C++: per-thread counters share a cache line - severe false sharing
#include <thread>
#include <vector>
#include <atomic>

struct Counters {
    long a; // thread 0 writes here
    long b; // thread 1 writes here
    long c; // thread 2 writes here
    long d; // thread 3 writes here
    // All four longs (32 bytes) fit in one 64-byte cache line.
    // Every increment invalidates the line on the other 3 cores.
};

void worker(long& counter) {
    for (long i = 0; i < 100'000'000; ++i) ++counter;
}

int main() {
    Counters c{};
    std::thread t0(worker, std::ref(c.a));
    std::thread t1(worker, std::ref(c.b));
    std::thread t2(worker, std::ref(c.c));
    std::thread t3(worker, std::ref(c.d));
    t0.join(); t1.join(); t2.join(); t3.join();
}
✓ Fixed
// C++: pad each counter to its own cache line
#include <thread>

struct alignas(64) PaddedCounter {
    long value;
    // alignas(64) forces sizeof(PaddedCounter) to be a multiple of 64,
    // so consecutive instances never share a cache line.
};

struct Counters {
    PaddedCounter a; // own cache line
    PaddedCounter b; // own cache line
    PaddedCounter c;
    PaddedCounter d;
};

void worker(long& counter) {
    for (long i = 0; i < 100'000'000; ++i) ++counter;
}

int main() {
    Counters c{};
    std::thread t0(worker, std::ref(c.a.value));
    std::thread t1(worker, std::ref(c.b.value));
    std::thread t2(worker, std::ref(c.c.value));
    std::thread t3(worker, std::ref(c.d.value));
    t0.join(); t1.join(); t2.join(); t3.join();
    // Typically 5-10x faster than the unpadded version on 4+ cores.
}

Added 18 Jul 2026
Views 36
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 1 ping S 4 pings M 2 pings T 4 pings W 1 ping T 0 pings F 2 pings S 1 ping S 0 pings M 0 pings T 0 pings W 1 ping T 1 ping F 0 pings S 1 ping S 1 ping M 1 ping T 0 pings W 2 pings T 0 pings F
No pings yet today
Bing 2
Bing 11 Google 3 Applebot 2 PetalBot 2 SEMrush 2 Meta AI 1 ChatGPT 1 Unknown AI 1 Ahrefs 1
crawler 24
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Align hot per-thread data to 64 bytes (alignas(64), #[repr(align(64))], @Contended) or pad structs so independently-written fields never share a cache line.
📦 Applies To
any library cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
struct\s+\w+\s*\{[^}]*\b(long|int|size_t|atomic|volatile|std::atomic)\b[^}]*\b(long|int|size_t|atomic|volatile|std::atomic)\b
Auto-detectable: ✗ No perf intel-vtune amd-uprof perf c2c
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: Medium Context: File


✓ schema.org compliant