6.1.10 · D4Parallelism & Multicore

Exercises — False sharing problem

3,073 words14 min readBack to topic

This page is a self-test ladder. Each rung climbs one level of thinking, from "can you spot it?" (L1) all the way to "can you invent the fix under pressure?" (L5). Every problem has a full worked solution hidden inside a collapsible callout — try first, then reveal.

Before you start, we fix the vocabulary so every symbol is earned:

Everything below is built on the parent note False sharing problem. Keep this one rule in your pocket: the hardware invalidates a whole 64-byte line even if two cores touch different bytes. That is the entire disease.

Figure — False sharing problem

Level 1 — Recognition

Exercise L1.1 (Recognition)

Two global variables sit at byte addresses and . Cache line size . Core 0 writes only ; Core 1 writes only . Do they false-share?

Recall Solution

Compute the line index of each — that is the only test that matters. Same line index yes, they false-share. The bytes differ, but the unit of transfer (the whole line 64) is identical, so each write kicks the line out of the other core's cache.

Exercise L1.2 (Recognition)

Same setup but now and . Do they false-share?

Recall Solution

Different line indices () no false sharing. Note , so the second variable starts exactly one line later — the smallest gap that fully separates them.

Exercise L1.3 (Recognition)

A C array int counter[8] starts at address (= 4096 decimal). An int is 4 bytes. How many of these 8 counters land in the very first cache line?

Recall Solution

The first line spans addresses to (that's the 64 bytes with line index 64). Each int occupies 4 bytes, so how many fit? ints fit in a line, but we only have 8, so all 8 counter[0..7] share the same cache line. If 8 different cores each hammer their own counter, all 8 collide on one line — the worst case.


Level 2 — Application

Exercise L2.1 (Application)

int data[16] begins at address (= 8192). Cores read/write elements 0 and 15. Do those two false-share? Which pairs of indices never share a line?

Recall Solution

int = 4 bytes, so element sits at . Line index of element : For : . For : . Same line — they false-share. In fact all indices give line 128, so every element of this 16-int (64-byte) array lives in one line. No pair is ever separated — this array is one big collision zone.

Exercise L2.2 (Application)

Estimate the slowdown. Without contention each increment costs cycles. Under full false-sharing ping-pong each increment costs cycles. What is the slowdown factor, and how many cycles do increments take in each case?

Recall Solution

Slowdown factor: For increments: The false-sharing version does 75 times more work for the identical program output.

Exercise L2.3 (Application)

You have a struct ThreadData { int thread_id; int counter; int checksum; bool done; };. Fields start at offsets 0, 4, 8, 12. A single instance begins at a 64-byte-aligned address. Core A writes counter, Core B writes checksum, Core C writes done. Do all three false-share?

Recall Solution

Because the struct is only 13 bytes (padded to 16) and starts at a line boundary, every field has line index equal to the struct's base line index. counter (offset 4), checksum (offset 8), done (offset 12) all fall inside the same 64-byte line. All three cores false-share on one line — a three-way ping-pong, even nastier than two-way because each write invalidates two remote copies.


Level 3 — Analysis

Exercise L3.1 (Analysis)

Derive the minimum padding to isolate an int value (4 bytes) so that an array of these never false-shares. Then compute the memory waste ratio.

Recall Solution

To force each element into its own line, the stride between consecutive elements must be . So the padded struct size must be a multiple of 64: Padding bytes needed: . Memory actually used is 4 bytes out of 64, so the useful fraction is ; the waste ratio is . See Memory allocators for how allocators can hand back cache-aligned blocks.

Exercise L3.2 (Analysis)

An array PaddedCounter counter[N] with struct PaddedCounter { int value; char pad[60]; } starts at a 64-byte-aligned base . Prove that element and element () can never share a line.

Recall Solution

Element 's value lives at . Its line index is (the division is exact because is a multiple of 64, and ). So the line index of element is . For : so the line indices differ — no shared line, no false sharing. The padding turned "same line" into "line index increases by exactly 1 per element."

Exercise L3.3 (Analysis)

A cache line is 64 bytes. A programmer writes struct Bad { char pad[32]; int value; } and makes an array of these, base at a line boundary. Element 0's value is at offset 32. Element 1 starts at byte 36 (since sizeof(Bad) = 36). Does this fix false sharing? Compute the line indices of value for elements 0 and 1.

Recall Solution

sizeof(Bad) = 32 + 4 = 36 bytes (assume no extra alignment padding). Element begins at ; its value is at .

  • Element 0 value: offset , line index (relative to base line).
  • Element 1 value: offset , line index .

Here elements 0 and 1 happen to land in different lines (0 vs 1) — but this is luck of the offsets, not a guarantee. Element 1 value (offset 68) and element 2 value (offset , line ) both give line index 1 → they collide! Half-padding is fragile: the correct fix is a full-line stride of 64, as in L3.2.


Level 4 — Synthesis

Exercise L4.1 (Synthesis)

You are told a hot array of double (8 bytes each) causes false sharing across 4 cores, each writing its own element arr[0..3]. Design a fix using C++17 that is portable across architectures (so you don't hard-code 64). State the mechanism and why it adapts.

Recall Solution

Use std::hardware_destructive_interference_size (the standard's name for " that separates hot data") together with alignas:

#include <new>
struct alignas(std::hardware_destructive_interference_size) Cell {
    double value;
};
Cell arr[4];   // each Cell begins on its own line

Mechanism: alignas(k) forces each Cell to start at an address that is a multiple of , and sizeof(Cell) is rounded up to a multiple of too — so the stride becomes . Picking guarantees distinct line indices (the L3.2 proof). Why it adapts: the constant is provided by the compiler for the target architecture, so on a CPU with 128-byte lines it becomes 128 automatically — no hard-coded 64 to break.

Exercise L4.2 (Synthesis)

Combine two concerns. A concurrent histogram uses std::atomic<long> bin[8] (each 8 bytes), all 8 in one 64-byte line, updated by 8 threads. Atomics already prevent races. Explain why performance is still bad and give the two-part fix.

Recall Solution

Atomics from Atomic operations guarantee each increment is indivisible (no torn read/write), but an atomic RMW still needs the line in Modified state, so it still triggers coherence invalidations. With all 8 bins in one line, every core's atomic increment bounces the line — the ping-pong of the parent note, unchanged. Two-part fix:

  1. Keep std::atomic<long> for correctness (safe concurrent updates).
  2. Add padding/alignment so each bin gets its own line:
struct alignas(64) Bin { std::atomic<long> v; };
Bin bin[8];

Now correctness and performance are both handled. See Lock-free data structures for structures that assume this padding as a default.

Exercise L4.3 (Synthesis)

Memory-budget design. You must store per-core counters for a machine with 64 cores. Full 64-byte padding costs how much memory? If the counters are read frequently but written rarely, argue when you might not pad — and connect to NUMA architectures.

Recall Solution

Padded cost: bytes = 4 KiB, versus bytes unpadded — a blow-up, but tiny in absolute terms. When not to pad: False sharing only bites on writes that need exclusive ownership. If the counters are almost always read (Shared state can be held in many caches simultaneously — reads don't invalidate), the coherence traffic is negligible and padding just wastes space and cache capacity. NUMA connection: on a NUMA machine, an even bigger cost is placing a core's counter in remote memory. Padding to a line boundary is necessary but not sufficient — you also want each core's line allocated in its local NUMA node so reads/writes stay on-socket. See NUMA architectures and Multicore scaling.


Level 5 — Mastery

Exercise L5.1 (Mastery)

Full quantitative model. cores each perform increments on their own counter. Line size , int = 4 bytes, array unpadded and line-aligned. A local write is cycles; a coherence-forced write is cycles. Assume the unpadded case forces every write to be a coherence miss (worst case). CPU runs at cycles/second. Compute wall-clock time for (a) padded and (b) unpadded, and the speed-up from padding.

Recall Solution

Total increments across all cores: . Cores run in parallel, so wall-clock time is set by per-core work (each core does increments) — but under full false sharing the ping-pong serializes writes, so we count the aggregate cycles as the bottleneck resource.

(b) Unpadded (serialized ping-pong): total cycles = cycles.

(a) Padded (fully parallel, each core local): each core independently does cycles, and all 8 run at once, so wall-clock cycles = .

Speed-up: Padding turned a 2-second job into 3 milliseconds.

Exercise L5.2 (Mastery)

Scaling law. With false sharing, adding cores can make a program slower. Model total time as (aggregate serialized coherence cost, ) versus ideal parallel . Using the L5.1 numbers, tabulate for in the false-sharing model and describe the trend versus Multicore scaling.

Recall Solution

With , , : s.

  • : s
  • : s
  • : s
  • : s

Time grows linearly with core count — negative scaling. Ideal parallel time is flat at s regardless of . This is the sharpest illustration of the Multicore scaling failure: false sharing converts extra cores from a benefit into a coherence-traffic tax. The fix (padding) restores the flat ideal curve.

Exercise L5.3 (Mastery)

Break-even reasoning. Padding wastes bytes per element (where = element size). Suppose a design constraint gives you a total memory budget bytes for per-core counters, int counters (). How many unpadded counters fit, versus padded? If you have exactly 8 cores, does padding fit the budget?

Recall Solution

Unpadded: counters fit. Padded to full line: counters fit. For 8 cores you need 8 padded counters = bytes yes, padding fits comfortably (uses one quarter of the budget). The apparent waste per element is irrelevant here because 8 is far below the 32-counter ceiling.


Recall Self-quiz

The line index test for two addresses is ::: equal they false-share. Minimum stride to isolate elements is ::: exactly (64 bytes), a full line. Atomics fix ::: correctness (races), not false-sharing performance. Padding an int to a line wastes ::: of that block. False sharing makes scaling ::: negative — time grows with core count.