σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality skill›Module 3 · The wider machine›Topic 3.3·~55 minAdvanced

False sharing and padding

Skill positionModule 3 of 3 · Topic 3 of 7
3.3 False sharing and padding
—Lab score— / 100Total XP—Streak—

5 min readCode examples: 3

  1. 01Cores have caches of their own
  2. 02One counter per core
  3. 03How much padding
  4. 04Counters driven by real dates
  5. 05What you will build

Four threads each update their own counter, nothing is shared, and yet the loop runs 40 times slower than it should. The reason is that the four counters sit in one cache line and the cores fight over it. This lecture builds a small model of cache coherence, shows what false sharing costs on a loop driven by the dates of the SPY table, and fixes it with a few bytes of padding.

01

Cores have caches of their own

So far one program ran on one core. A real machine has several cores, and each has its own small, fast cache. This creates a problem: if core 0 and core 1 both hold a copy of a line and core 0 changes it, core 1 must not go on reading the old value. The hardware rule that prevents this is cache coherence. The simplest way to enforce it: before a core may write to a line, every other core's copy of that line is thrown out. That is an invalidation.

  1. 1
    Look in your own cacheIf the line is not there, this access is a miss.
  2. 2
    On a write, evict the othersEvery other core that holds the line loses its copy: one invalidation each.
  3. 3
    Keep the lineAfterwards the line is in this core's cache, ready for its next access.

The class below keeps, for each core, the set of lines it holds. We replay three steps: core 0 writes byte 0, core 1 writes byte 8 (the same line), then core 0 writes byte 0 again.

main.pyPython
class CoherentCaches:    def __init__(self, n_cores):        self.lines = [set() for _ in range(n_cores)]        self.misses = 0        self.invalidations = 0    def access(self, core, address, write):        line = address // LINE_BYTES        hit = line in self.lines[core]        if not hit:            self.misses += 1        if write:            for other, held in enumerate(self.lines):1                if other != core and line in held:                    held.discard(line)                    self.invalidations += 1        self.lines[core].add(line)        return hitcaches = CoherentCaches(2)print(caches.access(0, 0, True), caches.access(1, 8, True), caches.access(0, 0, True))print("misses:", caches.misses, "invalidations:", caches.invalidations)
False False False
misses: 3 invalidations: 2
  1. 1A write removes the line from every other core that holds it and counts one invalidation per copy.

All three accesses miss. The write of core 1 invalidates the copy of core 0, and the next write of core 0 invalidates the copy of core 1 in turn. Core 0 and core 1 never wrote the same byte, only neighbouring ones, and still they take the line from each other on every write.

DefinitionFalse sharing

Two cores write different variables that lie in the same cache line. The program is correct, but the line bounces between the cores as if they were sharing one variable.

02

One counter per core

A common way to count events in parallel is to give each core its own counter, so that no core waits for another one. The counters are 8 bytes each. Put them one after another in an array and four of them fit into one 64-byte line. We build the writes of 50 rounds of updates, one write per core in each round, and replay them with the counters 8 bytes apart (packed) and 64 bytes apart (padded). The time is 1 ns for an access in the core's own cache and 60 ns for a miss that fetches the line from another core.

main.pyPython
def counter_address(core, stride_bytes):    return core * stride_bytesdef update_trace(n_cores, n_updates, stride_bytes):    return [(core, "w", counter_address(core, stride_bytes)) for _ in range(n_updates) for core in range(n_cores)]def run_trace(trace, n_cores):    caches = CoherentCaches(n_cores)    for core, op, address in trace:        caches.access(core, address, op == "w")    return {"accesses": len(trace), "misses": caches.misses, "invalidations": caches.invalidations}def time_ns(stats):    return (stats["accesses"] - stats["misses"]) * HIT_NS + stats["misses"] * MISS_NSfor name, stride in (("packed", 8), ("padded", 64)):    stats = run_trace(update_trace(4, 50, stride), 4)    print(name, stats, time_ns(stats), "ns")
packed {'accesses': 200, 'misses': 200, 'invalidations': 199} 12000.0 ns
padded {'accesses': 200, 'misses': 4, 'invalidations': 0} 436.0 ns

Packed, all 200 writes miss and 199 of them invalidate the previous writer: 12,000 ns. Padded, each core keeps its counter in a line of its own: four cold misses at the start, then every write hits, and no invalidation: 436 ns. That is 27 times faster for exactly the same arithmetic, because the four cores no longer take the line from each other.

03

How much padding

To give every counter a line of its own, the distance between two counters must be a multiple of the line size, so the item size is rounded up to the next multiple of 64. An 8-byte counter takes 64 bytes, which wastes 56. An item of 65 bytes takes 128. The function below does the rounding.

main.pyPython
def padding_needed(item_bytes):    return math.ceil(item_bytes / LINE_BYTES) * LINE_BYTESprint([padding_needed(size) for size in (8, 63, 64, 65, 100)])
[64, 64, 64, 128, 128]
How to ask for it in real code

In C++ write alignas(64) on the struct that holds a thread's counter (or std::hardware_destructive_interference_size), in Rust #[repr(align(64))], in Java @Contended. In Python with NumPy, make the array of counters (n_cores, 8) and use only column 0, so that each counter starts a new 64-byte row.

Pad only what cores write

Reads alone never invalidate anything, so many cores can read one line without cost. Padding wastes 56 bytes per 8-byte counter, a bad bargain for data that is only read or for thousands of items. Keep padding for the few hot, per-thread variables that threads update often.

04

Counters driven by real dates

In the lab four cores each count the trading days they handle, and day number i of the SPY table goes to core weekday % 4. The first 1,000 days drive the writes in date order. Because the days follow the calendar, the same core rarely writes twice in a row and the line is taken away again and again. With the counters packed, 818 of the 1,000 writes miss and 817 invalidate another core; with them padded, only the four first writes miss.

What the real data says1,000 trading days of SPY, 4 cores, 1 ns a hit, 60 ns a miss
Counters packed in one line49,262ns818 misses, 817 invalidations
Counters padded to 64 bytes1,236ns4 cold misses, no invalidation
Speed-up from padding39.9×the same work, a line each
Memory paid for it56byteswasted per 8-byte counter
Time of 1,000 updates, in microseconds
Packed49.3
Padded1.2
05

What you will build

The lab builds the coherence model, the traces of per-core counters with a given stride, the replay, the padding rule, the time model and the study on the SPY dates.

  • Function · questionBuilds on
  • CoherentCaches(n_cores).access(core, address, write)Hit or miss, and who loses a copy?Plain arithmetic
  • counter_address(core, stride_bytes)Where does a core's counter live?Plain arithmetic
  • update_trace(n_cores, n_updates, stride_bytes)Which writes do the cores make?counter_address
  • run_trace(trace, n_cores)How many misses and invalidations?CoherentCaches
  • padding_needed(item_bytes)How far apart must the items be?Plain arithmetic
  • time_ns(stats)How long does the run take?Hit and miss costs
  • study(bars)What does false sharing cost on the SPY dates?run_tracepadding_neededtime_ns

Key takeaways

  • A write takes the line. To write, a core invalidates every other core's copy of the line, and they miss the next time they use it.
  • Neighbours share fate. Variables in one 64-byte line are one unit for the cores: false sharing makes parallel code slower than serial code.
  • Pad the hot, per-core data. Round each item up to 64 bytes: 40 times faster here for 56 wasted bytes per counter; do not pad data that is only read.
Quick check
Two threads each increment their own 8-byte counter, and the two counters are neighbours in an array. What happens?
Pick an answer.

Next topic: false sharing is about cores that share a socket. On a machine with two sockets, memory itself is split: some of it is close to a core and some is far. In NUMA effects you place pages on nodes, count local and remote accesses and price the difference.

Lab · what's graded

7 tasks · 100 pts
  • A coherence model30 pts
  • The address of a counter5 pts
  • Rounds of updates15 pts
  • Replaying a run15 pts
  • The padding that gives a line10 pts
  • The time of a run10 pts
  • The study on the SPY table15 pts
Explorer · lab-cl-9
▾ lab-cl-9
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-cl-9$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-cl-9 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes