False sharing is when two threads hammer different words that happen to sit on the same cache line, and the line ping-pongs between them. The usual fix is to pad hot per-thread data so each lands on its own line. On Apple silicon the received advice is "pad to 128 bytes." This repository measures the actual granule on M4 and finds it is not one number:
- when the two threads share a core cluster, the false-sharing granule is 64 bytes (padding past a 64 B boundary removes it),
- when they are on different clusters (a P core and an E core), the granule is 128 bytes (64 B of padding is not enough; the sharing survives to 128 B).
The effective granule doubles across clusters. So the padding you need depends on where your threads run: 64 B if you can keep the sharers in one cluster, 128 B otherwise. This is a measured microarchitectural law with an up-front mechanistic prediction and a named falsifier, not a proof.
M4 has a 64 B CPU (L1) cache line but a 128 B SoC line that the shared L2 / system-level cache tracks and moves. Same-cluster coherence resolves at the shared L2 in 64 B units; cross-cluster coherence is mediated by the SLC in 128 B units, which drags along the neighboring 64 B half. Prediction: the false-sharing recovery edge is at 64 B same-cluster and 128 B cross-cluster.
Two threads atomic-fetch-add words in one page-aligned buffer; the measured thread sits at offset 0, the other at a swept offset. macOS gives no hard core pinning, so placement is QoS-biased and confirmed per run by a calibrated compute probe whose ops/ns cleanly separates P (~0.18) from E (~0.05); runs that do not land on the intended clusters are discarded (here 13-14 of 14 confirmed per cell). ns per op, median with BCa bootstrap 95% CIs:
| offset | same-cluster ns/op (CI) | cross-cluster ns/op (CI) |
|---|---|---|
| 4 B | 7.50 [7.41, 8.79] | 10.04 [9.13, 10.49] |
| 32 B | 8.76 [8.58, 9.00] | 10.83 [9.30, 11.46] |
| 48 B | 8.02 [7.67, 8.62] | 13.27 [11.33, 14.13] |
| 64 B | 1.84 [1.79, 2.07] <- recovered | 9.15 [8.61, 9.71] still sharing |
| 96 B | 1.79 [1.79, 1.83] | 8.61 [7.94, 9.57] still sharing |
| 112 B | 1.79 [1.79, 1.98] | 10.60 [10.10, 11.02] still sharing |
| 128 B | 1.79 [1.77, 1.79] | 1.77 [1.74, 1.79] <- recovered |
| 256 B | 1.79 [1.77, 1.79] | 1.79 [1.79, 1.79] |
Same-cluster drops to the recovered floor at 64 B and stays there. Cross-cluster stays contended through 64, 96, and 112 B and only drops at 128 B. At 64 B the two regimes are entirely CI-separated (1.84 vs 9.15 ns): the exact same layout that is already fixed within a cluster still falsely shares across clusters. The recovery edges land on the 64 B and 128 B line boundaries, never at 48 or 96.
Read-only control (the measured thread does relaxed loads instead of atomic read-modify-writes, so it needs no exclusive ownership):
| offset 4 B | offset 256 B | |
|---|---|---|
| load, same-cluster | 0.82 [0.75, 0.87] | 0.99 [0.97, 1.01] |
| load, cross-cluster | 1.49 [1.48, 1.51] | 1.02 [1.01, 1.02] |
With loads, sharing a line (4 B) costs about the same as being fully separated (256 B): 0.8-1.5 ns, not the 8-10 ns of the RMW case. The false-sharing penalty is driven by writes acquiring exclusive ownership, not by the layout or the TLB. The small residual cross-cluster load cost (1.49 vs 1.02) is the writer invalidating the reader's shared copy, an order of magnitude below the RMW ping-pong.
Robust: the recovery-edge locations (64 B same-cluster, 128 B cross-cluster) and the same-vs-cross ordering, across repeated runs. The absolute ns magnitudes drift with thermal state and DVFS, so only the edge locations and ordering are claimed, not the exact costs. Placement is a measured, gated variable, not an assumption.
./run.sh
Needs clang for arm64 and Python 3 (standard library only; the bootstrap is pure Python). CPU only. The sweep is placement-gated, so it runs the benchmark many times and keeps the runs that landed on the intended clusters; exact numbers vary but the two edge locations do not.
bench/twogranule.c: the two-thread RMW/load benchmark with QoS placement, the calibrated placement probe, and the offset as an argument.bench/sweep.py: the placement-gated offset sweep with BCa bootstrap CIs and the read-only control (pure Python).PREREG.md: the pre-registration (prediction and falsifier committed first).run.sh: reproduces everything.
Apple M4, CPU only. The granule is read off the recovery-edge location per placement regime. No proof is involved; this is a measured law with an a-priori mechanistic prediction (64 B L1 line, 128 B SoC line) and a named falsifier (a single 128 B edge in all regimes would collapse it to the already-known "128 B lines" fact).
MIT.