Epoch Advancement¶
Epoch-based reclamation only frees memory once the global epoch has advanced
past every retire's epoch. If nothing advances the global epoch, every retire
stamps the same epoch onto the limbo bag, safeEpoch never exceeds 1, and
reclamation is silently blocked. The limbo bag grows forever; reclaimers see
ReclaimBlocked on every pass.
manager.advance() increments the global epoch with one atomic fetchAdd.
Any registered thread may call it. The cost is one cache-line write that
contends across cores, so the question is not whether to advance but how
often.
Cadence patterns¶
Three patterns cover almost every consumer:
Per-retirement. Call manager.advance() immediately after every
retire. Simplest model, highest atomic-store traffic. Pick this only when
retires are rare (one per request, one per allocation batch).
Per-N retirements (recommended for hot paths). Use
handle.advanceEvery(n) to amortize. The helper increments a non-atomic
per-handle counter and only performs the atomic store every nth call.
Typical n is 32 to 128 for queue-style hot paths. Larger n lets the
limbo bag grow more between advances; smaller n increases atomic-store
contention.
Periodic-thread. A dedicated background thread calls
manager.advance() on a timer (e.g. every 1 ms or every 1000 ops). Keeps
the hot path free of advance work at the cost of one extra thread. Each
worker still calls reclaimNow(handle) on its own cadence to drain its own
limbo bags; reclamation is per-thread (cross-thread reclamation is not
supported, see reclamation guide).
Working example¶
A queue's pop hot path that retires a segment when its last slot drains:
proc pop(...): Option[T] =
withPin(handle):
# ... read slot, advance head ...
if mySlot == LastSlot:
it.retire(cast[pointer](seg), segmentDestructor)
handle.advanceEvery(64) # cadence-controlled global advance
if eager:
discard reclaimNow(handle) # per-thread reclamation
advanceEvery(64) makes 63 of every 64 calls a single non-atomic
increment plus a branch; only call 64 pays the atomic fetchAdd.
Pitfalls¶
- Missing entirely. Limbo bags grow unboundedly.
reclaimNowalways returns 0. The bug is silent until you measure RSS. - Too frequent. Every retire calls
advance(). The atomic store onglobalEpochping-pongs the cache line between cores. - Too rare. A 1ms timer thread is fine for most workloads, but if retires fire faster than the timer can advance, the limbo bag still grows. Pair a timer with a per-N helper for safety.
See also¶
withPin- the pinned scope inside which retires happen.reclaimNow- the reclamation pass that needs an advanced epoch to make progress.advance- the underlying typestate transition.