6.1.10Parallelism & Multicore

False sharing problem

3,205 words15 min readdifficulty · medium5 backlinks

What Is False Sharing?

False sharing occurs when multiple processor cores modify logically independent variables that happen to reside in the same cache line. This triggers cache coherence protocols (like MESI) to invalidate and transfer the line between cores, creating contention despite no actual data sharing.

Why it happens:

  • Cache coherence operates at cache line granularity (typically 64 bytes)
  • Compilers/allocators pack data structures densely
  • Adjacent array elements or struct fields land in the same line
  • Each core's write invalidates the line in other cores' caches

The critical insight: The hardware cannot tell that you're accessing different bytes. It only knows: "Someone wrote to this 64-byte block, so I must invalidate everyone else's copy."

The Mechanism: Cache Line Ping-Pong

Figure — False sharing problem

Derivation from First Principles

Let's build up why this happens:

Step 1: Cache coherence granularity

  • Memory is divided into blocks (cache lines) of size LL bytes (typically L=64L = 64)
  • Address AA maps to cache line A/L\lfloor A / L \rfloor
  • Two addresses A1,A2A_1, A_2 share a line if: A1/L=A2/L\lfloor A_1 / L \rfloor = \lfloor A_2 / L \rfloor

Step 2: MESI protocol constraint

  • A line can be Modified in at most one cache
  • Write to Modified line: local, fast (1-2 cycles)
  • Write to Shared/Invalid line: must acquire exclusive ownership
    • Send invalidation to all other caches
    • Wait for acknowledgments
    • Transition to Modified
    • Cost: 50-200 cycles (remote cache access)

Step 3: False sharing scenario

Consider two cores accessing different variables:

  • Core 0 writes to address A0A_0 (variable counter[0])
  • Core 1 writes to address A1A_1 (variable counter[1])
  • If A0/64=A1/64\lfloor A_0 / 64 \rfloor = \lfloor A_1 / 64 \rfloor, they share a cache line

Timeline:

Time    Core 0                Core 1              Line State
0       Write counter[0]        -                   Modified in C0
1       -                       Write counter[1]    → Invalidate C0
                                    → Modified in C1
2       Write counter[0]        -                   → Invalidate C1
                                    → Modified in C0
3       -                       Write counter[1]    → Invalidate C0
                                    → Modified in C1
...     (ping-pong continues)

Cost per false sharing write: Tfalse=Tcoherence+Twrite100200 cyclesT_{\text{false}} = T_{\text{coherence}} + T_{\text{write}} \approx 100-200 \text{ cycles}

Compare to isolated write: Tlocal=Twrite12 cyclesT_{\text{local}} = T_{\text{write}} \approx 1-2 \text{ cycles}

Slowdown factor: Overhead=TfalseTlocal50100×\text{Overhead} = \frac{T_{\text{false}}}{T_{\text{local}}} \approx 50-100\times

Concrete Examples

The Solution: Cache Line Padding

Goal: Ensure each core's data occupies a separate cache line.

Common Mistakes

Detection and Measurement

How to detect false sharing:

  1. Profiling tools:

    • Linux perf c2c (cache-to-cache): Shows which cache lines have high coherence traffic
    • Intel VTune: "Memory Access Analysis" highlights contended cache lines
    • AMD μProf: Cache line contention view
  2. Performance counters:

    • Monitor MEM_LOAD_RETIRED.FB_HIT (load from fill buffer, sign of recent invalidation)
    • OFFCORE_RESPONSE.DEMAND_RFO.L3_MISS.REMOTE_HITM (remote cache hit-modified, false sharing signature)
  3. Symptoms:

    • Code scales poorly (2cores slower than 1core per throughput)
    • High L3 cache miss rate despite good locality
    • CPU utilization low but no obvious blocking

Quantifying the cost:

Let WW = writes per core, TcohT_{\text{coh}} = coherence latency (fetch line from a remote cache), TlocalT_{\text{local}} = local write latency.

Without false sharing — every write hits a line already in Modified state locally: Tper-write=Tlocal1-2 cyclesT_{\text{per-write}} = T_{\text{local}} \approx 1\text{-}2 \text{ cycles}

With false sharing — in the worst case, every write finds the line stolen by another core, so it must pay the coherence penalty once to re-acquire it: Tper-write=Tcoh+Tlocal100-200 cyclesT_{\text{per-write}} = T_{\text{coh}} + T_{\text{local}} \approx 100\text{-}200 \text{ cycles}

Key correction — the per-write penalty does NOT scale with core count. Each contended write incurs exactly one coherence miss (fetch the line back from whoever stole it), regardless of how many cores participate in the ping-pong. Adding more cores makes it more likely your line gets stolen before your next write (raising the probability of paying the penalty toward 1), but it never makes a single write pay the penalty multiple times.

Per-write slowdown factor: S=Tcoh+TlocalTlocalTcohTlocal100×S = \frac{T_{\text{coh}} + T_{\text{local}}}{T_{\text{local}}} \approx \frac{T_{\text{coh}}}{T_{\text{local}}} \approx 100\times

This is independent of NN. So the realistic worst-case slowdown is bounded by the ratio Tcoh/TlocalT_{\text{coh}}/T_{\text{local}} (~50–100×), not by N×(Tcoh/Tlocal)N \times (T_{\text{coh}}/T_{\text{local}}).

Total time (worst case, every write contended): Ttotal=W×(Tcoh+Tlocal)vs.W×TlocalT_{\text{total}} = W \times (T_{\text{coh}} + T_{\text{local}}) \quad\text{vs.}\quad W \times T_{\text{local}}

The Big Picture: When to Worry

High risk scenarios:

  • Per-thread counters/statistics (arrays indexed by thread ID)
  • Shared data structures with per-core fields (like the ThreadData struct)
  • Lock-free data structures with adjacent metadata
  • Producer-consumer ques where head/tail pointers are close

Low risk scenarios:

  • Read-only data (no invalidations)
  • True data sharing (cores need to see each other's updates anyway)
  • Single-writer patterns (one core modifies, others read)
Recall Explain to a 12-year-old

Imagine you and your friend are both doing homework at a big table. You're working on math, they're working on history—totally different subjects. But there's only one big eraser on the table.

Every time YOU make a mistake and grab the eraser, your friend has to stop and wait. And every time THEY grab the eraser, you have to stop. Even though you're not using the same homework sheet, you keep blocking each other because you're fighting over the same eraser.

That's false sharing! The eraser is like a "cache line" in the computer. Even though you're working on different variables (math vs. history), if they're stored in the same cache line (the same eraser), the computer cores keep blocking each other. And here's the subtle part: grabbing the eraser back from your friend takes the same amount of time whether there are 2 friends or 10 friends at the table—you only ever grab it from whoever has it right now. The fix? Give each person their own eraser (pad the variables into separate cache lines), and now everyone can work at full speed!

Connections

  • Cache coherence protocols — MESI/MOESI are the mechanisms that cause false sharing overhead
  • Cache line size and alignment — Understanding64-byte granularity is foundational
  • Lock-free data structures — Often vulnerable to false sharing in metadata
  • NUMA architectures — False sharing is even more expensive cross-socket
  • Memory allocators — jemalloc/tcmalloc can reduce false sharing with per-thread arenas
  • Atomic operations — Provide correctness but don't prevent false sharing
  • Multicore scaling — False sharing is a major barrier to linear speedup

#flashcards/hardware

What is false sharing? :: When multiple cores write to logically independent variables that reside in the same cache line, causing unnecessary cache coherence traffic and performance degradation.

Why does false sharing cause slowdown even when there's no data race?
Cache coherence operates at cache line granularity (64 bytes). A write to any byte in a line invalidates the entire line in other caches, even if they're accessing different bytes.
What is the typical cache line size on x86 processors?
64 bytes
How much slower can false sharing make parallel code?
10-100× slower per contended write. The per-write penalty is bounded by T_coherence/T_local (~50-100×) and does NOT scale with the number of cores.
Does the per-write false-sharing penalty scale with the number of cores N?
No. Each contended write pays exactly one coherence miss to fetch the line back from its current owner. More cores raise the probability of paying the penalty (toward 1), not the size of each penalty.
What is the formula to check if two addresses share a cache line?
⌊addr₁ / L⌋ = ⌊addr₂ / L⌋, where L is cache line size (typically 64 bytes). If equal, they share a line.
How do you fix false sharing?
Pad each frequently-written variable to occupy its own cache line (64 bytes). Use alignas(64) or struct padding to guarantee separation.
What C++17 feature helps with portable cache line padding?
std::hardware_destructive_interference_size — gives the cache line size for the current architecture.
Can atomic operations prevent false sharing?
No. Atomics prevent data races (correctness issue), but don't prevent cache line ping-pong (performance issue). You need both atomics AND padding.
What Linux tool can detect false sharing?
perf c2c (cache-to-cache) — shows which cache lines have high coherence traffic between cores.
What is the "ping-pong" effect in false sharing?
Cache line repeatedly bouncing between cores: Core 0 writes → invalidates Core 1 → Core 1 writes → invalidates Core 0, in rapid succession.
If you pad to 128 bytes instead of 64, what's the downside?
Wastes cache capacity. With many threads, over-padding evicts other useful data from L1 cache, reducing overall performance.
What performance counter indicates false sharing on Intel?
OFFCORE_RESPONSE.DEMAND_RFO.L3_MISS.REMOTE_HITM — shows remote cache hit-modified, the signature of false sharing.
In a per-thread counter array, why does false sharing occur?
Adjacent array elements (e.g., counter[0], counter[1]) are only 4 bytes apart, so 16 of them fit in one64-byte cache line. All cores contend for the same line.
What's the memory cost of fixing false sharing for 16 threads?
16 threads × 64 bytes/thread = 1024 bytes (1 KB). Small price for potentially100× speedup.

Concept Map

occurs at

packs together

modified by

triggers

requires

sends

causes

costs

serializes

hardware cannot tell

degrades

False Sharing

Cache Line Granularity 64B

Independent Variables

Multiple Cores

MESI Coherence Protocol

Exclusive Ownership

Invalidation Messages

Cache Line Ping-Pong

50-200 Cycles per Write

Should-Be-Parallel Execution

Performance

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho yaar, false sharing ka core idea bahut simple hai par bahut logon ko dikhta nahi. Jab multiple CPU cores parallel kaam karte hain, toh cache coherence hardware line-by-line kaam karta hai, jahan ek cache line typically 64 bytes ki hoti hai. Ab problem yeh hai ki agar do cores alag-alag variables pe likh rahe hain, lekin woh variables physically ek hi cache line ke andar baithe hain, toh hardware ko lagta hai ki dono same data touch kar rahe hain. Woh doosre core ki copy ko invalidate kar deta hai, chahe logically dono ka data bilkul independent ho. Bilkul us roommate example ki tarah jahan alag-alag pages likhne pe bhi poora folder lock ho jata hai.

Ab yeh matter kyun karta hai? Kyunki cost ka farak zabardast hai. Jab ek core apni local Modified line pe likhta hai toh sirf 1-2 cycles lagte hain, but jab false sharing hoti hai toh usse exclusive ownership leni padti hai, doosre caches ko invalidation bhejni padti hai aur acknowledgment ka wait karna padta hai, jisme 100-200 cycles chale jaate hain. Matlab tumhara code 50 se 100 guna slow ho sakta hai, aur sabse pareshaan karne wali baat yeh ki koi actual data sharing hai hi nahi! Tumhara program logically toh perfectly parallel hai, par hardware usse serialize kar deta hai. Isko cache line ping-pong bolte hain, jahan line baar-baar ek core se doosre core ki taraf bounce karti rehti hai.

Iska practical takeaway yeh hai ki jab tum multicore programming karte ho, tumhe apni data structures ka memory layout dhyan se sochna padta hai. Jaise agar tum ek array of counters bana rahe ho jahan har thread ek counter update karega, toh unhe alag-alag cache lines pe padding daal ke separate karna zaroori ho jaata hai. Yeh chhoti si cheez tumhare parallel program ki performance ko theek karke rakhti hai, warna tumhe lagega ki extra cores add karne se speed badh rahi hai, par actually woh ek dusre ko block karke sab slow kar rahe honge. Toh yaad rakho: hardware ko byte-level ka pata nahi, use sirf 64-byte block dikhta hai.

Go deeper — visual, from zero

Test yourself — Parallelism & Multicore

Connections