Free preview

Why this matters: the core design is correct; this lesson is about where it's slow, and about the neighboring designs an interviewer may steer toward. The performance story of a ring buffer lives almost entirely in cache behavior — which means some of the biggest wins are invisible in the code's logic and show up only in its memory layout.

False sharing: the invisible tax

Our struct puts head and tail next to each other, which very likely lands them on the same 64-byte cache line. Logically they have different writers and never conflict. Physically, the cache coherence protocol works in whole lines: every time the producer stores tail, the consumer's cached copy of that line — which it needs for head, its own counter — is invalidated, and vice versa. Two threads that share no data end up ping-ponging a cache line between cores on every operation. That's false sharing: contention created by layout, not by logic.

The fix is padding — spend memory to buy isolation:

c
struct ring { struct record slots[CAPACITY]; alignas(64) atomic_u64 tail; // own cache line char pad1[56]; alignas(64) atomic_u64 head; // own cache line char pad2[56]; };

A hundred-odd bytes of padding routinely buys integer-factor throughput on a hot SPSC queue. In an interview, raising false sharing unprompted — with the mechanism, not just the term — is one of the strongest systems signals available, precisely because the code was already "correct" without it.

A refinement worth naming once you're here: each side also repeatedly acquire-loads the other side's counter, re-pulling that line even when nothing changed. Caching the last-seen value of the other index locally, and only re-loading when the cached value says full (or empty), cuts that traffic further. Mention it as the next turn of the same crank.

Batching: amortizing the fences

Sometimes the producer has a burst of records — the NIC delivered twelve packets. Pushing them one at a time pays the acquire/release round-trip twelve times. A batched push writes all twelve records into consecutive slots, then publishes once:

try_push_n(records, n):
    check space for n            (one acquire of head)
    copy n records into slots
    store-release tail + n       (one publish)

The consumer sees all twelve appear atomically-in-order, and per-record overhead collapses toward a pure memcpy. The symmetric try_pop_n exists for the same reason. The trade-off to state: batching adds latency for the first record in the batch — you're holding it back until its siblings are written — so a latency-first system batches only what arrives together, never waiting to fill a batch.

The waiting strategy: deliberately not our problem

try_push returning false leaves the caller holding a record and a decision. It's worth having the menu in your pocket, because "what should the caller do?" is a natural interviewer follow-up:

  • Spin and retry — lowest latency, burns a core; right when rejection is rare and microseconds matter.
  • Backoff — retry with escalating pauses; the middle ground.
  • Sleep/park and be woken — cheapest CPU, highest wake-up latency, and it drags an OS primitive back into the picture.
  • Drop the record — legitimate when freshness beats completeness (telemetry, market data snapshots).

The design point to make explicit: by keeping the buffer non-blocking, we made the waiting policy a caller-side seam. Different callers — a latency-critical ingest thread versus a background drainer — can choose different policies against the same buffer. Building any one policy into the buffer would have chosen for everyone; the API boundary is doing the same job the strategy interface did in the machine-coding problems.

Overwrite-oldest: a different contract, not a feature flag

Telemetry pipelines often want the opposite full-buffer behavior: never stall the producer; if the consumer is slow, lose the oldest data. It's tempting to see that as a mode flag on our design, and it isn't — it's a different contract with a different structure. The moment the producer can advance over unconsumed slots, it must move head too — and head acquires a second writer, which demolishes the one-writer-per-index property that our whole no-lock argument stood on.

Real overwrite-oldest designs restructure around that fact (per-slot sequence stamps that let a reader detect it lost a lap and resynchronize, for instance). The interview-ready summary: "rejection and overwrite-oldest look like siblings at the API, but they differ at the invariant level — I'd design overwrite-oldest as its own structure rather than bolt it onto this one." Knowing where a variation stops being a variation is the trade-off skill this lesson is named for.

Key takeaway

Past correctness, the ring buffer's story is cache lines and contracts: pad head and tail onto separate lines to kill false sharing, batch pushes to amortize the publish, keep waiting policies outside the buffer where each caller can choose — and recognize that overwrite-oldest isn't a flag but a different invariant structure entirely. Performance reasoning that names mechanisms, and variation reasoning that names contract boundaries, are what distinguish an L5+ answer here.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue