5.4.7 · D2Memory Hierarchy & Caches

Visual walkthrough — Write-allocate vs no-allocate

2,697 words12 min readBack to topic

This is the visual companion to the parent topic. Read it slowly; each step has a picture that carries the argument.


Step 0 — The vocabulary, drawn before it is used

Before we can talk about "write miss" we need three plain pictures. Nothing here is assumed.

Figure — Write-allocate vs no-allocate

Look at the picture: the long grey street is memory, the small yellow shelf is the cache, and the dashed bracket shows one line = 8 neighbouring boxes copied as a unit. The single blue arrow is a write landing on a box that is not on the shelf — that is a write miss, and it is the only situation this whole page is about.

Recall Why do we only care about write

misses? Because a write hit is boring — the box is already on the shelf, you just change it. ::: The interesting decision (fetch the neighborhood, or not?) only appears when the box is absent from the cache.


Step 1 — The fork: what a write miss forces us to decide

WHAT. A write miss just happened. We must now choose one of two behaviours.

WHY. The cache is small. Every byte we put on the shelf pushes some other byte off. So "should this written byte earn a shelf spot?" is a real cost question, not a free choice.

PICTURE.

Figure — Write-allocate vs no-allocate

The blue write hits an empty line. Two chalk arrows leave the fork:

  • Left road — write-allocate (also called fetch-on-write): pull the whole 8-box block onto the shelf, then change the one byte.
  • Right road — no-allocate (also called write-around): send the byte around the shelf, straight to memory. The shelf stays empty.

Everything below is just counting the consequences of taking each road.


Step 2 — Road A traced: write-allocate, box by box

WHAT. Follow the left road for a single write to address 0x28 (value 0xFF), on a cache with 8-byte lines.

WHY. We must see why fetching the whole block — not just the one byte — is unavoidable.

PICTURE.

Figure — Write-allocate vs no-allocate

Trace the numbered chalk arrows:

  1. Miss on 0x28 — the target line is empty.
  2. Fetch the entire block 0x28–0x2F (all 8 neighbours) up from memory. This is the pink "read" arrow going up.
  3. Write 0xFF into the byte at 0x28 on the shelf (blue).
  4. Mark dirty — the dirty bit defined in Step 0 is set, flagging this shelf copy as newer than memory.

Cost so far: exactly 1 memory read (8 bytes). Zero writes to memory yet — the dirty byte waits on the shelf.


Step 3 — Road B traced: no-allocate, box by box

WHAT. Same write to 0x28, now on the right road, paired with write-through.

WHY. To see that the shelf stays empty and the byte skips it entirely.

PICTURE.

Figure — Write-allocate vs no-allocate
  1. Miss on 0x28.
  2. No-allocate — do not touch the shelf.
  3. Write-through — the byte flows down the pink arrow straight into memory box 0x28.
  4. The cache line stays empty (invalid).

Cost so far: 1 memory write. No read, no fetch, no shelf space used.

Recall Side-by-side, what did each single write cost?

Write-allocate: 1 read (dragged in 8 neighbours). ::: No-allocate: 1 write (byte only). For one isolated write no-allocate is cheaper — the payoff of allocate only appears when neighbours get reused, which is Step 5.


Step 4 — The forced marriage: why write-back demands write-allocate

WHAT. We now prove a pairing rule, not just state it. Write-back can only sit with write-allocate.

WHY. Write-back means: "I keep the only fresh copy on the shelf and write it to memory later, when the line is evicted." Let us try to combine that with no-allocate and watch it break.

PICTURE.

Figure — Write-allocate vs no-allocate

Follow the contradiction on the board:

  • Write-back's promise: don't write to memory now — write later, from the shelf.
  • No-allocate's rule: the byte is not on the shelf.
  • So "write it later, from the shelf" is impossible — there is no shelf copy to write later. The only escape is to write to memory now.
  • But "write to memory now" is write-through behaviour!

Step 5 — The counting that decides everything: four writes to one block

WHAT. Write 0x100, 0x104, 0x108, 0x10C (all inside the same 8-byte line — the line 0x100–0x107... wait, check the arithmetic below), then read 0x100. Count memory trips on each road. This is the payoff calculation.

WHY. One isolated write favoured no-allocate (Step 3). But real programs write neighbours — this is spatial locality. We must see how the tally flips.

PICTURE.

Figure — Write-allocate vs no-allocate

Read the two chalk columns (all four writes now genuinely share the line 0x100–0x107):

Step Write-allocate + write-back No-allocate + write-through
write 0x100 miss → 1 read (fetch line) miss → 1 write
write 0x102 hit (same line, on shelf) miss → 1 write
write 0x104 hit miss → 1 write
write 0x106 hit miss → 1 write
read 0x100 hit miss → 1 read
Total 1 memory trip 5 memory trips

WHY the gap. On the left, the one fetch dragged the whole line 0x100–0x107 onto the shelf, so writes 2–4 and the final read are all free hits because they land in that same line. On the right, each byte is a fresh independent trip because nothing is ever cached. Spatial locality is exactly what write-allocate monetizes.


Step 6 — The third quadrant: write-through + write-allocate

WHAT. We have traced write-allocate+write-back (Step 2/5) and no-allocate+write-through (Step 3). The remaining valid combination is write-through + write-allocate — fetch the line on a miss, but still push every write down to memory immediately.

WHY. Step 4 killed write-back+no-allocate as a contradiction. But write-through+write-allocate is perfectly consistent, and it fills the last box of the design grid, so the reader never meets an un-shown quadrant.

PICTURE.

Figure — Write-allocate vs no-allocate

On this road, the same four-write-then-read sequence behaves like this: the first write misses and fetches the line (1 read), so all later writes are read-hits — but because it is write-through, each write also spills down to memory (4 writes), and the final read is a hit.

write-through + write-allocate
write 0x100 miss → 1 read (fetch) + 1 write-through
write 0x102 hit + 1 write-through
write 0x104 hit + 1 write-through
write 0x106 hit + 1 write-through
read 0x100 hit
Total 1 read + 4 writes = 5 trips

Step 7 — The degenerate case: streaming writes, where the tally flips back

WHAT. Now write a huge array once and never read it — a video frame buffer.

WHY. This is the case that breaks the naive "allocate always wins." When reuse probability is zero, the fetch in Step 2 buys nothing and actively harms.

PICTURE.

Figure — Write-allocate vs no-allocate
for (int i = 0; i < 1920*1080; i++)
    framebuffer[i] = pixel_value;   // written once, never read

On the write-allocate road, each new line triggers a useless fetch (reading pixels we are about to overwrite entirely), and worse, it evicts genuinely useful data off the shelf — this is cache pollution. The right road (no-allocate) avoids both.

This is exactly why CPUs offer non-temporal / streaming store instructions (e.g. movnti), often draining through write-combining buffers so the many bytes still leave as few wide memory writes.


The one-picture summary

Figure — Write-allocate vs no-allocate

The final board compresses the whole derivation: the fork (Step 1), the two costs of one write (Steps 2–3), the forced marriage with write-back (Step 4), the complete 2×2 grid (Steps 5–6), and the streaming degenerate case (Step 7).

Recall Feynman retelling — say it like a story

Memory is a long street of numbered boxes; the cache is a tiny fast shelf that only ever grabs a whole line of 8 neighbouring boxes at once. When you write to a box that is not on the shelf, you hit a fork. ::: Road one, write-allocate: haul the whole 8-box line up (one read), change your byte on the shelf, flag it dirty. Costs one read now, but every neighbour in that same line you touch afterwards is a free hit — so writing four bytes inside line 0x100–0x107 plus reading one back is just one trip to memory (when paired with write-back). ::: Road two, no-allocate: shove the byte straight past the shelf into memory. The shelf stays empty. One isolated write is cheap this way, but writing four bytes and reading one back is five separate trips. ::: Beware the line boundary: with 8-byte lines, 0x108 starts a new line, so "nearby" is not enough — the addresses must share the same 8-aligned window to be hits. ::: The grid has four corners. Write-back is forced onto write-allocate (no-allocate would leak the write out immediately and secretly become write-through). Write-through can go either way: with allocate it keeps a shelf copy for future reads while still spilling every write to memory; with no-allocate it streams past the shelf entirely. ::: The rule: if you'll reuse the neighbourhood soon, reel it in (allocate). If you're just streaming data once and never looking back, skip the shelf (no-allocate) so you don't pollute it with garbage or pay for fetches you'll instantly overwrite.


Reveal-line quick checks:

A write miss means the target box is
not currently on the cache shelf (absent from cache).
Write-allocate's unavoidable upfront cost is
one memory read to fetch the whole line.
Write-back must pair with write-allocate because
it needs a shelf copy to defer, which no-allocate refuses to create.
For four writes in one 8-byte line plus one read, allocate+write-back costs
1 memory trip vs 5 for no-allocate.
With 8-byte lines, 0x108 and 0x100 are
in different lines (boundary at every multiple of 8).
Streaming stores prefer no-allocate to avoid
cache pollution and useless fetch bandwidth.