False Sharing
Two threads incrementing two different variables should not interfere with each other. If the two variables happen to land in the same cache line, they do: every write by one core invalidates the other core's copy of the line, and the line shuttles between them. Nothing is shared in the program. Everything is shared in the hardware. Every other page about this shows you a number from somebody else's laptop. This one measures yours.
Nothing is shared, and they fight anyway
1 What this browser will allow, including how fine a clock it is willing to show
What this browser will let the measurement use. A page that cannot measure has to say so rather than print a number nobody took.
| What | Here |
|---|
Shared memory and a fine clock were both taken away after Spectre, and both come back only for a page that is cross-origin isolated. This one asks for that isolation on its own path rather than imposing it on the rest of the studio.
2 Two threads incrementing their own counters, trials alternating between the two layouts
Two threads, each incrementing its own counter four hundred thousand times. Twenty-four trials, alternating between counters that share a cache line and counters that do not, so that anything drifting on this machine lands on both.
3 What the trials took, as a spread rather than a number, and whether the difference is real here
What the trials took
| Layout | Median | Middle half | Trials |
|---|
The middle half is shown because a single number is not a measurement. The rule is deliberately blunt: if the middle halves overlap, or the gap is smaller than a few ticks of this clock, the page reports no measurable difference. That is a statement about these trials on this machine, not a confidence interval, and it is more useful than a ratio that flatters the page.
4 How far apart the counters have to be before the cost stops, which is a measurement of this machine and not quite the cache line size
How far apart they have to be
| Counters apart by | Time |
|---|
These ran in this browser when the page loaded. Each claim, whether it held, and the number behind it.
| claim | held | measured |
|---|---|---|
| one cache line is 64 bytes, so the padded layout skips 16 slots | yes | 16 slots of 4 bytes |
| the clock's granularity is measured rather than assumed | yes | smallest observable step 0.1000 ms |
| the summary reports the median, not the mean | yes | median 6 of 2,4,6,8,10; mean would be 6 too, so also: median 1 of 1,1,1,97 |
| overlapping timings are reported as no measurable difference | yes | medians 30 and 32, ranges overlap |
| and separated timings are reported as a real one | yes | medians 102 and 12, ranges apart |
| a gap smaller than the clock can resolve is credited to the clock | yes | the same two timings, with a clock whose tick is 40 ms |
| a trial where the two threads ran together for most of the time counts | yes | one ran 0 to 10, the other 1 to 11, sharing 90% of the shorter run |
| one where they barely overlapped is discarded, not averaged in | yes | shared only 10%, below the 80% floor |
| and one where they did not overlap at all is discarded | yes | one finished at 10, the other started at 20: nothing was contended |
| a trial lasts from the first thread starting to the last one stopping | yes | 0 to 30 and 0 to 11, not either thread's own stretch |
| this browser cannot run the measurement, and the page says so instead of inventing one | yes | SharedArrayBuffer is not available. It was disabled in every browser after Spectre and comes back only for pages that are cross-origin isolated. |
What is real here, and what is not
This is a measurement, and it can fail
Three ways, all of which the page says out loud. The browser may withhold shared memory, in which case nothing is measured. The two threads may not overlap, in which case that trial contended for nothing and is discarded and counted. And the difference may be smaller than this machine's noise, in which case the answer is that it was not measurable here. Only the third is a result; the other two are the page refusing to invent one.
The clock is deliberately coarse and its granularity is measured, not assumed
performance.now() was blunted in every browser after Spectre, and cross-origin isolation is what sharpens it again. The page measures the smallest step the clock will show and reports it, because a measurement whose noise floor is unstated is not a measurement. A difference smaller than a few of those steps is credited to the clock rather than to the cache.
Atomics, not a plain increment
A plain increment in a loop can be optimised out of the loop entirely, and then the page measures nothing while printing a number. An atomic read-modify-write cannot be elided, and it is also the operation that genuinely contends for the line.
The trials alternate rather than running in batches
All the shared trials followed by all the padded ones would let processor frequency scaling, or whatever else this machine is doing, land entirely on one group and read as a cache effect. Alternating spreads it across both.
What the sweep settles, and what it does not
Sixty-four bytes is the usual answer on x86-64 and Apple's cores use a hundred and twenty-eight in places. Rather than assert either, the page sweeps the distance between the two counters and reports where the cost falls away. That distance is a real measurement of this machine and it is NOT the cache line size. The counters sit at a fixed offset into a buffer whose physical alignment JavaScript will not reveal, so a fall at thirty-two bytes can mean a sixty-four byte line whose boundary happens to lie between them. An earlier version of this page printed that number and called it the cache line, which was an artifact of where the counters were put. What the sweep does settle is the practical question: how far apart two variables have to be before they stop costing each other anything, on this processor, today. If nothing falls by more than the clock can resolve, the page says that instead.
Sound: no
The measurement is a ratio of durations in microseconds, which cannot be played at true scale. That is the same reason CPU Cache did not earn sound, and the studio's rule is that a sound carries the measurement or is not built.
The checks below test the reporting, not your processor
Every machine here ends with a table of claims checked when the page loaded. On this page those checks feed the summary and verdict functions SYNTHETIC numbers with known answers, because that is the only way to test a reporter: it confirms that overlapping spreads are called unmeasurable, that a gap smaller than the clock is credited to the clock, and that a trial whose threads did not overlap is discarded. None of it is a measurement of your machine. The measurement is the part you press a button for.
What is still not controlled
Which cores the browser places the two workers on, whether it places them on the same physical core, what else the machine is doing, and how the processor is scaling its frequency. None of that is reachable from JavaScript. The trials warm up first, alternate between layouts and discard any pair that did not overlap, which handles what can be handled and leaves the rest as a reason the answer here may differ from the answer ten minutes from now.
Sources
- Bill Bolosky and Michael L. Scott, False Sharing and its Effect on Shared Memory Performance, USENIX SEDMS IV, September 1993 — where the term is defined, as references to different objects inside one coherence block.
- perf-c2c(1), the Linux tool built specifically to find this: evidence that it is a real and expensive problem rather than a curiosity.
- MDN on SharedArrayBuffer and the cross-origin isolation it now requires, which is the Spectre mitigation this page has to satisfy.
- Logical Art, the studio this belongs to.