Multicorefalse sharingcache linepaddingalignmentcontention

False Sharing: Independent Data, Shared Line

Two threads update two different variables. They never touch each other's data and the code is obviously correct. Throughput is worse than single-threaded, because the two variables happen to sit in one cache line and the hardware shares by line, not by variable.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Why do two threads writing to genuinely separate variables slow each other down?
What you wrote
Thread A increments `counterA`. Thread B increments `counterB`. Different variables, no shared state, no locks — this should be perfectly parallel.
What the hardware does
Both counters live inside one 64-byte cache line. Coherence tracks lines, so every write by A invalidates B's copy and every write by B invalidates A's. The line ping-pongs across the interconnect on every single increment.
It is the purest example of a hardware detail invalidating correct high-level reasoning. The code contains no bug, no shared variable and no lock, yet it performs worse than running the two threads one after the other — and nothing in the source is a clue.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

One line, two owners, no logical sharing

The layout below is the entire bug. Two eight-byte counters declared next to each other land in one line. Coherence has no concept of "these are separate variables"; it grants ownership of lines. So thread A's write forces the line exclusive to A's core, invalidating B's copy — and B's next write forces it back, invalidating A's.

The result is a transfer of the line across the interconnect on essentially every increment, in both directions. What should be two independent local operations becomes a serialised sequence of ownership transfers. Measured throughput is frequently worse than single-threaded, because the single-threaded version at least keeps the line resident.

The name is precise and worth taking literally: the sharing is false. Nothing is logically shared. The contention is entirely an artefact of where the compiler and allocator happened to place two variables.

The bug: two independent counters inside one cache line
usedanother thread's dataSIMPLIFIED
counterA (thread A writes)counterB (thread B writes)other struct fields
line 0
64 bytes total1 cache line touched

Thread A touches only the first 8 bytes and thread B only the second 8, but the coherence unit is the whole 64-byte line. Every write by either thread invalidates the other's copy of all 64 bytes.

The fix, and what it costs

The fix is to force the two counters onto different lines, by padding the struct or aligning each element to a line boundary. Once they are on separate lines, each core holds its own line Exclusive, writes locally, and never generates a coherence transaction. The scaling that the code always looked like it should have suddenly appears.

The cost is memory, and it is not trivial. Padding an 8-byte counter to a full line multiplies its footprint eightfold at a 64-byte line size. For a handful of per-thread counters that is irrelevant. For a large array of small structures it is a real trade: you are spending cache capacity — the scarce resource — to buy exclusivity. Sometimes the better answer is to restructure so the hot fields are per-thread and the cold fields stay packed.

Note also that padding hard-codes an assumption about line size. Sixty-four bytes is the common case on current mainstream CPUs, but it is not architectural, and some platforms use a different size or prefetch line pairs, which means effective sharing granularity can be larger than one line.

False sharing: counters adjacent, one line
1struct Counters {
2 uint64 a; // thread A increments
3 uint64 b; // thread B increments
4};
5
6// Both fields land in the same 64-byte line.
7// Every increment by either thread invalidates
8// the other core's copy. The line transfers
9// across the interconnect on each write.
10//
11// Result: slower than single-threaded.
Padded: one line each
1struct alignas(64) PaddedCounter {
2 uint64 value;
3 // padding to fill the rest of the line
4};
5
6struct Counters {
7 PaddedCounter a; // its own line
8 PaddedCounter b; // its own line
9};
10
11// Each core holds its line Exclusive and writes
12// locally. No coherence traffic. Scales.
13// Cost: 8x the memory for these two counters.

Nothing about the logic changed — only the addresses. The hardware shares by line, so separating the addresses by a line removes the contention entirely. This is the clearest demonstration in the domain that *where data sits* can matter more than *what the code does*.

Finding it, and where it hides

PLATFORM-SPECIFICSixty-four bytes is the common cache line size on current mainstream x86-64 and AArch64, but it is not architectural. Some platforms use 128 bytes, and prefetchers that fetch line pairs make the effective sharing granularity larger than one line, so padding to 64 does not always suffice.

False sharing has a distinctive signature: parallel throughput that is flat or *worse* than single-threaded, with low CPU utilisation of useful work, no locks in the profile, and a coherence miss rate that rises with thread count. If adding threads makes things slower and there is no lock to blame, check the layout of what those threads write.

The places it hides are worth knowing, because it is rarely as visible as two adjacent counters. Arrays indexed by thread id — results[thread_id]++ — pack several threads' slots into one line and are probably the most common instance. Adjacent fields in a shared struct where different threads own different fields. Objects from an allocator that places small allocations close together, so two threads' "private" objects end up neighbours. Reference counts or lock words embedded next to hot data in an object header.

The last one generalises usefully: check the layout of anything written concurrently before concluding the algorithm is the problem. The correctness reasoning and the layout reasoning are independent, and only one of them is visible in the source.

  • `results[thread_id]++` — the classic. Several threads' slots share a line; pad each slot to a line.
  • Adjacent fields owned by different threads — separate them, or split the struct by ownership.
  • Allocator neighbours — two threads' "private" small objects placed next to each other by the allocator.
  • Object headers — a lock word or reference count sharing a line with hot data written by another thread.

Key points

  • Coherence tracks cache lines, so variables sharing a line contend even when nothing is logically shared.
  • The symptom is parallel throughput at or below single-threaded, with no lock in the profile.
  • The fix is padding or alignment to put independently-written data on separate lines.
  • Padding costs memory, and memory is cache capacity — do not pad indiscriminately.
  • The commonest instance is an array indexed by thread id, where several threads' slots share a line.

Progressive depth

Overview

Two threads write two different variables and slow each other down, because the variables sit in the same cache line and the hardware manages cache lines rather than variables. Move them apart and the problem disappears.

Practical

Suspect it when parallel throughput is flat or worse than single-threaded and there is no lock in the profile. The classic instance is an array indexed by thread id. Fix by padding or aligning to a line, or better, by accumulating thread-locally and combining once at the end — which costs no memory and removes the sharing entirely.

Advanced

The cost is a read-for-ownership per write, transferring the line cache-to-cache and leaving the previous owner Invalid. It therefore scales with write frequency and with the number of participating cores, and it can invert the scaling curve. Padding assumes a line size, which is not portable; and padding is not free, because the memory it burns is cache capacity you wanted for something else. Where a struct has both hot per-thread fields and cold shared fields, splitting by ownership usually beats padding the whole thing.

Internals

False sharing is invisible to the coherence protocol by construction: line granularity is a deliberate trade, since byte-granular ownership tracking would require prohibitive state and traffic. Some designs blur it further — sectored caches track sub-line validity, and prefetchers that fetch adjacent line pairs can produce sharing effects at twice the nominal line size, which is why some codebases pad to 128 bytes on machines with 64-byte lines. Store buffers can also mask short bursts by coalescing writes before they reach the coherence point, which is one reason the effect can appear and vanish with small code changes.

False Sharing

Change an input and watch which number moves — and which one refuses to.

Two threads, two counters
Both counters on one cache line
usedanother thread's datafetched, never readSIMPLIFIED
counter Acounter Bunused
line 0
64 bytes total1 cache line touched48 bytes fetched and never read

The threads share no variable, but they share a line. Every increment on one core invalidates the other core's copy, so the line ping-pongs across the interconnect and the two threads run slower together than either did alone. The source code offers no clue at all.

lines touched
1
scales with threads
no

Struct Layout & Padding

Field order decides the size
Declared in a natural reading order
usedpaddingABI-SPECIFIC
padint bpaddouble d
line 0
24 bytes total1 cache line touched10 bytes of padding

Each field must sit at an address that is a multiple of its size, so the compiler inserts padding to get there. Twenty-four bytes to hold fourteen bytes of data, and in an array of a million records that is ten megabytes of nothing.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Thread A store → core A cache: the line holding both counters must become Exclusive to core A.
  2. 2
    Core A → interconnect: an invalidate is sent; core B drops its copy of the whole line.
  3. 3
    Thread B store → core B cache: B's copy is now Invalid, so it issues a read-for-ownership.
  4. 4
    Core A → core B: the line transfers cache-to-cache and core A goes Invalid.
  5. 5
    Repeat per increment: each write reverses the transfer, serialising two logically independent operations.
What people conclude from this — wrongly
  • "There is no shared variable, so there is no contention" — the hardware shares by line, not by variable.
  • "It must be lock contention" — there is no lock; the serialization is in the coherence protocol.
  • "Adding threads should help" — with false sharing, each added thread adds ownership transfers.
  • "Padding everything is the safe default" — padding costs cache capacity, which can create a different problem.

Consequences, controls and cost

What it causes
  • • Parallel throughput that is flat or worse than single-threaded, with no lock contention visible.
  • • Performance that changes drastically after unrelated edits, because the layout shifted.
  • • Benchmarks that scale on one machine and not another due to differing line sizes or allocators.
  • • Per-thread accumulator arrays that perform far worse than expected.
What you can do
  • • Pad or align independently-written data to separate cache lines — the direct and reliable fix.
  • • Prefer thread-local accumulation with a single combine at the end, which avoids the sharing entirely and costs no padding.
  • • Split structs by ownership so that fields written by different threads are not neighbours.
  • • Where padding is too expensive, restructure so hot written fields are per-thread and cold shared fields stay packed.
How to see it
  • • Plot throughput against thread count; flat or falling with no lock in the profile is the signature.
  • • Watch coherence miss counters — lines fetched modified from a peer — and check whether they rise with thread count.
  • • Print the addresses of the hot written variables and check whether they fall within one line-sized block.
  • • A/B the padded and unpadded layouts on identical work; a large gap confirms it outright.
What it costs
  • • Padding multiplies memory footprint for the padded objects, consuming cache capacity.
  • • Line-size assumptions baked into padding are not portable across platforms.
  • • Thread-local accumulation needs a combine step and delays visibility of the aggregate.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • PLATFORM-SPECIFICCache line size is commonly 64 bytes on current x86-64 and AArch64 but is not architectural; 128-byte lines exist, and pair-fetching prefetchers can make effective granularity larger.
  • ABI-SPECIFICField placement within a struct, and therefore whether two fields share a line, depends on the compiler and ABI layout rules — see Padding: Why Your Struct Is Bigger Than Its Fields.

Misconceptions

Claim
“False sharing is a race condition or a correctness bug.”
Reality
It is purely a performance effect. The program is correct, the results are right, and coherence is doing exactly its job. Only the throughput is wrong — which is precisely why it survives code review and testing, and why it has to be found by measurement.
Claim
“Adding padding everywhere is a safe default.”
Reality
Padding spends cache capacity, and cache capacity is the scarce resource this whole module is about. Padding a large array of small structures can convert a false-sharing problem into a capacity-miss problem and end up slower. Pad the few hot fields that are actually written by different threads.
Claim
“If I pad to 64 bytes I am safe on any machine.”
Reality
Sixty-four bytes is common on current mainstream CPUs but is not architectural — some platforms use 128-byte lines, and prefetchers that fetch adjacent line pairs can produce sharing effects at twice the nominal line size. Padding encodes a machine assumption that should be stated and re-checked.

Apply it

Where the rest of this lives

Concurrency & Parallelism
False sharing as a scalability bug

That domain covers recognising the symptom while reasoning about parallel speedup and what to do about it in a concurrent design. The ownership transfer and invalidation traffic that cause it are here.