2026-06-20
The DRAM data bus is bidirectional — the same wires carry bits from the chip to the controller on reads and from the controller to the chip on writes. Like a single-lane road with traffic flowing both ways, the bus has to physically reverse direction when you switch between operations, and that reversal costs cycles you never get back.
The relevant timings are tRTW (read-to-write) and tWTR (write-to-read). On a typical DDR5-6400 module you'll see numbers like tRTW = 18 cycles and tWTR_L = 24 cycles. During these gaps, the bus is idle. No data moves. The DRAM is still drawing power, the controller is still queueing requests, but the wires are sitting in high-Z while the on-die termination flips polarity and signal integrity settles.
The math. A DDR5-6400 channel delivers 51.2 GB/s peak. A burst-of-8 read transfers 64 B in 4 clocks. If you do 16 back-to-back reads, that's 64 cycles of useful work. If you alternate read/write/read/write with 16-cycle turnarounds, you spend roughly half the time turning the bus around. Effective bandwidth collapses to ~25 GB/s. Rule of thumb: every read-to-write switch costs you a full burst's worth of bandwidth.
Why memory controllers reorder. This is why every modern memory controller batches reads and writes into write drains. The controller fills a write queue while servicing reads, and only when the write queue hits a high watermark (or read pressure drops) does it flip the bus, drain 16-32 writes in a row, and flip back. You'll see this in performance counters as bursty write bandwidth — the writes don't trickle out, they go in big convoys.
Real-world example. A streaming benchmark that does a[i] = b[i] + c[i] reads two cache lines and writes one. Naïvely, you'd expect 2/3 read bandwidth and 1/3 write bandwidth. But the controller batches: you'll see long stretches of pure read traffic, then a write drain, then more reads. If you add non-temporal stores (which bypass cache and go straight to the write queue), the write drain pressure increases and you can actually hurt throughput on a read-heavy mix. This is why memcpy implementations carefully tune their non-temporal store thresholds — typically only switching to NT stores above ~L3 cache size, where the bus turnaround cost is amortized over enough data to matter.
The controller's reordering is invisible to software but visible in uncore_imc performance counters: watch CAS_COUNT.RD vs CAS_COUNT.WR and you'll see the convoy pattern directly.
