skip to content

In a shared-memory multiprocessor, a value that thousands of threads only read costs almost nothing to access, while a single value that many cores write becomes a throughput ceiling even when the program uses no locks at all. Explain the asymmetry, and describe the techniques you would use to remove such a hot spot.

level: seniorimportance: should knowfreq 42%

answer

  1. Many readers OR one writer, per line
  2. Clean line replicates → reads scale; ownership migrates → writes queue
  3. Hot written line = hardware serialisation without any lock
  4. Fix order: write less → shard per core → read-mostly snapshot → single owner
  5. Padding only fixes accidental (false) sharing

basics

~20 s

Clean lines can be replicated in every cache, so reads hit locally and scale. Writing requires exclusive ownership of the line, so every writer invalidates all other copies and the line migrates core to core — a serialisation point. Remove it by sharding state per core, batching updates, or making the hot data read-mostly.

solid answer

~50 s

Coherence permits many readers or one writer per cache line. A read-only value settles into every core's cache and stays there: no traffic, near-L1 latency, perfect scaling. A written value cannot be replicated — the writer must invalidate all other copies and take ownership — so each write costs an interconnect round trip, and the line's ownership becomes a queue that all writers pass through. That queue is why a lock-free counter can still scale worse than a single thread: the hardware serialises, whether or not the software does. Remedies, cheapest first: **stop writing so often** — accumulate thread-locally and flush in batches, or sample. **Shard**: one slot per core or thread, each on its own cache line, summed on read; this trades exact instantaneous reads for scalable writes. **Make the structure read-mostly**: publish a new immutable snapshot occasionally instead of mutating in place. **Partition ownership**: give one thread exclusive responsibility for the state and send it messages. Only then consider layout tricks like padding.

code

text · 7 lines
text
contended:
  every core: atomic_add(total, 1)      // one line, ownership queue

sharded:
  slot[i] padded to a full cache line
  worker i: slot[i].value += 1          // uncontended, stays in local cache
  reader  : sum over i of slot[i].value // O(shards), approximate instant

go deeper

for a junior

Say that many cores can hold a read-only value at once, but a write forces every other copy to be discarded, so writes are much more expensive.

for a middle

Explain ownership transfer as the mechanism and note that a hot written line serialises even without locks.

for a senior

Diagnose it from the scaling curve and coherence counters, then apply the fix ladder — write less, shard, read-mostly, single owner — with the accuracy tradeoffs of each.

for a principal

Budget contended writes per operation at design time, choose partitioning boundaries so hot state is core-local, and decide explicitly where exactness must be paid for.

## Where the asymmetry comes from Cache coherence enforces, per line, *many readers or one writer*. That single rule creates the entire cost difference. **Reads of a clean line replicate.** Every core that reads it obtains its own copy in a shared state. Afterwards each core hits in its own L1 at a few cycles, with zero interconnect traffic. Add cores and nothing degrades: read-only data has essentially unlimited read parallelism. **Writes cannot replicate.** Before a store commits, the writing core must hold the line exclusively, which means invalidating every other copy and, if another core has modified it, transferring the current data. That is a round trip through the interconnect — tens to a couple of hundred cycles on one socket, considerably more across sockets. Worse, the requests are inherently ordered: the line has one owner at a time, so N writers form a queue. Throughput on that line tends toward one transfer per ownership-transfer latency regardless of how many cores you add, and per-core throughput therefore *falls* as N grows. This is why a shared counter incremented by every core is a scalability bug even with no lock, no blocking and no contention in the program's own terms. Read-modify-write operations make it sharper still, because they must hold ownership across the read and the write. The hardware provides the serialisation the code appears to avoid. ## Recognising it The signature is a throughput curve that flattens early and then declines with added threads, concentrated on one instruction or one field, with cache misses served from other cores rather than from DRAM. Latency percentiles widen, because a thread's store may wait behind several ownership transfers. Distinguish it from a bandwidth limit (traffic goes to memory, not core to core) and from a lock convoy (threads park and wake rather than spin on ownership). ## Techniques, in the order worth trying **1. Reduce the write rate.** The best write is the one that never happens. Metrics, statistics and progress counters rarely need per-event global updates: accumulate in a register or thread-local slot and merge every N events or every few milliseconds. This turns millions of contended writes into thousands. Sampling (count one in K events and multiply) is acceptable for many observability uses. **2. Shard the state.** Give each core or thread its own slot, each aligned to its own cache line, and combine on read. Writes become uncontended and scale linearly; reads become O(number of shards). This is the standard scalable-counter design, and it generalises: sharded queues, per-core free lists, per-core RNG state, striped hash tables. The cost is that a read is a summary rather than a snapshot of an instant, which is usually fine because with concurrent writers there was never a meaningful instant anyway. **3. Make the hot data read-mostly.** If a structure is read constantly and updated rarely, mutate a copy and publish it, so readers keep hitting replicated clean lines and only the rare publication costs an invalidation. This is the copy-on-write/immutable-snapshot idea, and it converts the expensive access pattern into the cheap one. **4. Partition ownership.** Assign the state to a single owning thread and have others send it work; the owner's writes are always to a line it already owns exclusively. Message passing costs a hand-off, but the hand-off can be batched, whereas ownership transfer per increment cannot. This is the structural reason single-writer designs and per-core data structures perform well. **5. Layout fixes last.** Padding and alignment remove *false* sharing — accidental collisions between unrelated variables. They do nothing for genuine contention on the same variable. Reach for them once the sharing is known to be accidental, and treat them as tuned constants that need re-measuring on different hardware, since line sizes and adjacent-line prefetching vary. ## What to avoid Do not assume replacing a lock with an atomic operation fixes the scaling problem; both funnel through the same line ownership, and under high contention an atomic retry loop can be worse because failed attempts still take ownership. Do not add threads to a write-contended hot spot expecting improvement — that is the one change guaranteed to make it worse. And do not tune layout before establishing whether the sharing is real or accidental; the two problems have entirely different fixes.

  • A sharded counter gives an approximate total when read concurrently. When is that unacceptable, and what do you do then?
    It is unacceptable when the value gates a decision that must be exact at the moment of the write — a hard quota, a limited-capacity admission check, or an identifier allocator. Then you need the serialisation, so accept a single contended point but shrink it: hand out ranges rather than single units, so each core takes a block of the resource under one contended operation and consumes it locally. That preserves exactness while cutting contended operations by the block size.
  • Why can replacing a mutex with an atomic read-modify-write leave scalability unchanged?
    Both designs funnel every update through exclusive ownership of one cache line, and that ownership transfer is the actual bottleneck. Removing the lock removes blocking and context switches, but not the hardware serialisation. Under heavy contention the atomic version can even be worse, because failed retries still acquire the line and burn interconnect bandwidth while making no progress.

A reference book can be photocopied for everyone, so any number of people read at once. A book that must be annotated has to be the single authoritative copy, so it is physically carried from desk to desk — and the more people who want to annotate it, the longer everyone waits.

saying these in an interview costs you the question

  • Believing lock-free or atomic code avoids hardware serialisation
  • Expecting more threads to increase throughput on a write-contended location
  • Applying padding to fix contention on a genuinely shared variable
  • Treating shared reads as expensive — clean lines are replicated and cheap
  • Optimising layout before determining whether the sharing is real or accidental

context