← cosmin bararu

Stale on purpose_

Every push into an SPSC ring must answer one question: is there room? A naive producer reloads the consumer's cursor on every push, even when the answer has not changed.

Astral keeps a local copy instead. The producer owns the head cursor and caches the consumer's tail; the consumer owns the tail and caches the producer's head. Because both cursors only move forward, an old copy can hide newly available room or work, but it cannot invent either one.

This article explains why that staleness is safe, shows the remote load behind the boundary branch, and scopes the sub-nanosecond measurements to the isolated cursor probe that produced them.

The local fast path

// standalone illustration, clang++ -O2 -std=c++17 (x86-64), not the astral source
bool spsc_push(SpscRing& r, void* item) {
    uint64_t h    = r.head.load(std::memory_order_relaxed);
    uint64_t next = h + 1;
    if (next - r.cached_tail > r.capacity) {                  // check the local copy first
        r.cached_tail = r.tail.load(std::memory_order_acquire); // refresh only at the boundary
        if (next - r.cached_tail > r.capacity)
            return false;                                    // genuinely full
    }
    r.slots[h & (r.capacity - 1)] = item;
    r.head.store(next, std::memory_order_release);           // publish
    return true;
}

The standalone push loads the producer-owned head, compares next against the local cached_tail, and jumps straight to publication when the cached view proves there is room. The acquire load of the consumer-owned tail executes only when that first check fails.

; clang++ 18 -O2 (x86-64), standalone spsc_push
    mov     rcx, qword ptr [rdi]          ; h = producer-owned head
    lea     rax, [rcx + 1]                ; next = h + 1
    mov     r8, rax
    sub     r8, qword ptr [rdi + 8]       ; next - cached_tail
    mov     rdx, qword ptr [rdi + 88]     ; capacity
    cmp     r8, rdx
    jbe     .push                         ; cached view proves room
    mov     rdx, qword ptr [rdi + 64]     ; acquire-load remote tail
    mov     qword ptr [rdi + 8], rdx      ; refresh cached_tail
    ; subtract and compare again; return false if still full
.push:
    mov     r8, qword ptr [rdi + 80]      ; slots base
    dec     rdx                            ; capacity - 1
    and     rdx, rcx                      ; h & (capacity - 1)
    mov     qword ptr [r8 + 8*rdx], rsi   ; publish payload
    mov     qword ptr [rdi], rax          ; head.store(next, release)
    ret
locally reproduced: clang 18 / gcc 13 · x86-64 · remote cursor load stays behind the boundary branch · open the standalone reproduction
CACHED CURSORS · OWNERSHIP VIEW PRODUCER-OWNED STATE head (owned) release-store cached_tail local copy, read every push ... ring payload / padding ... CONSUMER-OWNED STATE tail (owned) release-store cached_head local copy, read every pop ... ring payload / padding ... common push: compare head against cached_tail acquire-load remote tail → refresh cached_tail fires only at the full boundary cursors move only forward, so a stale cached view understates space, never overstates it one refresh buys a whole batch of local-only operations before the next cross-core load
conceptual cursor ownership: remote load only at the boundary

The isolated cached-cursor probe measured 0.48 ns per single-item check and 0.32 ns per item in the batched case. These are local bookkeeping costs, not end-to-end transit latency and not the full production push.

I checked the probe's disassembly because sub-nanosecond numbers are easy to misread. Its local path is ordinary loads, arithmetic, a compare, and publication, with no lock-prefixed instruction. A lock addl elsewhere in the binary belongs to harness bookkeeping, so the review had to inspect the surrounding symbol instead of counting lock prefixes.

Astral's production ring adds transition-sensitive wake logic around this cursor protocol. It signals when a push changes empty to nonempty and when a pop changes full to not full. On ARM, the transition check refreshes the remote cursor before deciding whether to signal, so a stale cached value cannot suppress the wake.

The same physics shows up wherever two cores write near each other, and the cache-line layout rules it obeys have a post of their own. What makes this ring's version of the trick safe is the direction of the error: cursors only move forward, so a stale copy can understate available space or items, never overstate them. The failure mode is an unnecessary refresh, not a correctness bug.