Write Policies

Reading from a cache is easy: if the data is there, hand it over. Writing is where the interesting decisions live. The moment the CPU stores a value, the cache's copy and main memory's copy disagree — and the architect must decide when, and whether, to reconcile them. Those decisions are the write policies, and they trade memory-bandwidth against simplicity and, eventually, against the correctness of a whole multicore machine.

There are really two independent questions. On a write that hits: do we push the new value all the way to memory now, or later? On a write that misses: do we bother pulling the block into the cache at all? Two questions, two-by-two — four combinations, of which two pairings dominate real hardware.

Write-through vs write-back

On a write hit, the two schools are:

The dirty bit is the whole trick of write-back: one bit per line that remembers "this block has been modified since it was loaded". On eviction, a clean block (dirty bit 0) can be silently dropped — memory already has it — while a dirty block must be written back first. If a block is written a hundred times then evicted once, write-through does a hundred memory writes; write-back does one. Temporal locality of writes is exactly what makes write-back a huge win.

Write-allocate vs no-write-allocate

On a write miss — you are storing to a block that is not in the cache — there is a second choice:

On a write miss…What happensUsually paired with
Write-allocatefetch the block into the cache, then write it (bet: more writes/reads to it are coming)write-back
No-write-allocatewrite straight to memory, leave the cache untouchedwrite-through

The natural pairings follow the philosophy. Write-back + write-allocate keeps everything hot data local and minimises memory traffic — the default in nearly every modern L1/L2. Write-through + no-write-allocate keeps things simple and is handy where writes are rarely re-read soon (some streaming or I/O paths). The reasoning: if you are going to keep the block's only fresh copy locally (write-back), you had better allocate it on a write miss too; if you are pushing everything to memory anyway (write-through), why disturb the cache for a write you may never revisit?

The write buffer: hiding the store latency

Write-through's flaw is obvious — a store to slow memory on every write would stall the pipeline. The fix is a write buffer: a small FIFO queue between cache and memory. The store lands in the buffer in one cycle and the CPU races on; the buffer drains to memory in the background. Write-back machines use one too, so an eviction's write-back does not block the incoming block. A write buffer can even satisfy a later read to an address still queued in it (write-to-read forwarding), and lets writes be coalesced. It turns the memory-traffic cost of writing from a stall into (usually) free background work — until the buffer fills, at which point the CPU finally must wait.

// Count memory writes: write-through vs write-back on a bursty write stream. // Stream: (blockAddr, isWrite). One block, written many times, then a different block evicts it. const stream: [number, boolean][] = [ [0, true], [0, true], [0, true], [0, true], [0, true], // 5 writes to block 0 [4, true], // block 4 evicts block 0 (same set) ]; // Write-through: every write goes to memory immediately. const wtWrites = stream.filter(([, w]) => w).length; // Write-back: writes stay local (dirty), memory is touched only when a dirty block is evicted. // Block 0 is dirtied then evicted once -> exactly 1 write-back; block 4's write dirties it (still resident). const wbWrites = 1; console.log(`write-through memory writes: ${wtWrites}`); console.log(`write-back memory writes: ${wbWrites}`); console.log(`traffic reduction: ${(wtWrites / wbWrites).toFixed(1)}x`);

Writes and the coherence storm ahead

On a single core, write-back is a clear win and the story ends. Add a second core with its own cache and write-back becomes a minefield: if core A holds the only fresh (dirty) copy of a block and core B reads that address, B must somehow get A's value, not memory's stale one. This is the cache-coherence problem, and the dirty bit you met here grows into the "Modified" state of the MESI protocol. Write-through does not escape it either — it just changes which races you must handle. The humble write policy is the seed of the entire multiprocessor-memory chapter.

Simplicity and safety, in the right place. Write-through means memory (or the next level) is always current, so a crash, a DMA device, or another core reading memory directly never sees stale data — no write-back bookkeeping, no "is this line dirty?" question on eviction. Some GPUs, small embedded caches, and level-1 data caches that back onto an inclusive L2 use write-through precisely because a fast write buffer hides its one real cost (traffic) while keeping the design dead simple and the next level's copy trustworthy. As so often, the "worse" policy wins wherever its weakness is cheap and its simplicity is valuable.

Two independent bits ride on every line and beginners conflate them. Valid answers "does this line hold real data at all?" (cleared at reset). Dirty answers "has this valid line been written since it was loaded, so memory is now stale?" A line can be valid-and-clean (safe to drop), valid-and-dirty (must be written back before eviction), or invalid (ignore it entirely). A dirty bit on an invalid line is meaningless. And note: only write-back caches even have dirty bits — write-through keeps memory current, so there is never anything to write back.