2026-06-28
When your CPU issues a load, the request lands in a queue at the memory controller along with dozens of others from every core, the prefetchers, and the I/O subsystem. The controller does not serve them in arrival order. It runs FR-FCFS — First-Ready, First-Come-First-Served — and the "First-Ready" part is where most of the performance comes from.
The rules, in priority order:
Why this matters: a row activate costs ~14 ns (tRCD) plus another ~14 ns precharge (tRP) if a different row is open. A row-buffer hit costs only ~14 ns (CL). Serving a younger row-hit before an older row-miss can save 30 ns of DRAM time — bigger than a full L3 miss penalty on some chips.
Concrete example. Imagine four queued requests to one bank:
Row 7 is currently open. Strict FCFS serves R1, R2, then forces a precharge + activate for R3, then another precharge + activate for R4 — three row switches' worth of latency. FR-FCFS serves R1, R2, R4 (all row-hits, ~14 ns each), then R3 last (paying the row switch once). Same four requests, roughly 40 ns saved.
The dark side: starvation and fairness. A streaming thread hammering one row can lock out a latency-sensitive thread for thousands of cycles. Modern controllers add a cap — typically 4 to 16 consecutive row-hits per bank — before forcing the oldest pending request to issue regardless of readiness. Server-class controllers also track per-thread queue occupancy and apply thread-fair variants (PAR-BS, TCM) that batch requests across threads to bound worst-case latency.
Rule of thumb. If your workload's row-buffer hit rate is above ~50%, FR-FCFS is winning you roughly (hit_rate × 30 ns) per access vs. naive FCFS. Below ~20%, the scheduler is basically reordering for free — the bottleneck is somewhere else (bank conflicts, refresh, or the bus itself).
This is why two threads with identical address streams can see wildly different latencies depending on what the other cores are doing: your row keeps getting closed by someone else's miss.
