5.4.4 · D2Memory Hierarchy & Caches

Visual walkthrough — Cache line size and tags

2,354 words11 min readBack to topic

Before we start, one word we will use constantly:


Step 1 — A memory address is just a house number

WHAT. Main memory is one enormous row of numbered byte-boxes: box , box , box , … up to the last box. A memory address is simply the number written on a box.

WHY. Before we can split an address we must agree what it is. It is not a mysterious code — it is a plain house number on a very long street. If the CPU can produce addresses from up to , we call it an -bit address space, because it takes exactly bits to write the biggest house number.

PICTURE. Look at the street of byte-boxes below. The amber box is the byte the CPU asked for; its number, written in binary underneath, is the address.


Step 2 — The cache brings a whole line, so the low bits name a byte inside the line

WHAT. The cache never fetches one byte; it fetches a fixed clump of neighbouring bytes called a cache line. Choose as a power of two — say bytes. Once a whole line is in the cache, we still need to say which byte of the 64 the CPU wanted. That job goes to the lowest bits of the address: the offset.

WHY these bits, and why this many? The bytes inside one line are numbered . To count possibilities we need exactly the number of bits that satisfies "". That question — what power gives me ? — is answered by . That is the whole reason appears: it is the inverse of " bits count things".

For : , because . So the bottom 6 bits of every address are the offset.

PICTURE. One cache line unrolled into its byte-slots, slot numbers written in -bit binary. The bottom bits of the address are those slot numbers.


Step 3 — Peel off the offset, and the rest is a line number

WHAT. Take any address and mentally shift the bottom bits away. What remains is a line number: it names which -byte clump this byte belongs to, out of all clumps in memory.

WHY. Two addresses in the same line share everything except the offset bits. So dropping those bits is the same as integer-dividing the address by . The result identifies the line, not the byte — and that is the number the cache will actually reason about.

PICTURE. The full address bar with a scissors cutting off the low bits; the amber remainder is labelled "line number".


Step 4 — The cache has only a few slots, so line numbers must wrap around

WHAT. The cache is small: in a direct-mapped cache it has some number of slots, call it slots (also a power of two). There are far more lines in memory than slots on the cache. So many different lines are forced to share the same slot. The rule for which slot a line lands in is: take the line number and keep only its low bits — that is the index.

WHY. "Keep the low bits of the line number" is the same as "line number mod " (remainder after dividing by ). Mod is chosen because it maps an unlimited range of line numbers onto a fixed set of slots, cheaply, using no arithmetic — you just read the bits. To count slots we again ask "what power of two is ?", so:

Here = total cache size in bytes, = line size, = associativity (how many lines share a slot; for direct-mapped, more for set-associative). Dividing by gives the number of lines the cache holds; dividing again by groups them into slots (sets).

PICTURE. A long column of memory lines on the left, an arrow "mod " folding them onto a short column of cache slots on the right — several memory lines landing on the same slot.


Step 5 — Different lines landed on the same slot, so we need a tag to tell them apart

WHAT. Because many lines fold onto one slot (Step 4), knowing the slot is not enough — the CPU must confirm the line sitting there is the one it wanted. The tag is everything left over: the high bits of the address, above index and offset. The cache stores this tag beside the data, and on every access it compares.

WHY. Index tells you where to look; but "where" is ambiguous (Step 4's folding). The tag is the leftover bits that make the line number unique. Since address = tag-bits + index-bits + offset-bits and they must sum to :

PICTURE. The full -bit address bar coloured in three zones — amber Tag (high), cyan Index (middle), white Offset (low) — with a bracket showing .


Step 6 — Watch a lookup happen: hit vs miss

WHAT. Put the three fields to work. (1) Use the index to jump to a slot. (2) Read the tag stored there and compare it to the address's tag. (3) Also check the valid bit — a single bit meaning "this slot holds real data, not garbage from power-on". If tag matches and valid hit; use the offset to return the byte. Otherwise → miss; go fetch the line from memory.

WHY. This is the whole payoff. Index makes the lookup (one jump, no searching). Tag makes it correct (rejects the wrong line that folded onto the same slot). Valid makes it safe (a cold, empty cache never falsely reports a hit). Missing any one of these breaks the machine.

PICTURE. Two panels: top = HIT (stored tag address tag, valid , green check, byte returned). Bottom = MISS (tags differ, red cross, arrow back to memory).


Step 7 — Edge cases: the degenerate corners you must survive

WHAT / WHY / PICTURE. A formula you cannot break at the corners is a formula you understand. Four corners:

  • Line size byte (). Then : no offset bits at all. Every byte is its own line. Legal, just wasteful — you lose all spatial locality.
  • Whole memory fits in cache ( memory size, ). Then grows until : no tag bits. Every line has a permanent home, no folding, so no tag is needed to disambiguate.
  • Fully associative ( number of lines, so ). Then : no index bits. One giant slot; the line may sit anywhere, so the tag must carry all the line-number bits.
  • Byte-offset zero / line-aligned address. The bottom bits are all : the CPU asked for the first byte of the line. Nothing special breaks — offset just reads .

In every corner, one of shrinks to but the identity still holds. No case escapes it.


Step 8 — A full numeric run, matching Worked Example 2

WHAT. Take B, , , . Then , lines , so , and . Now decode address 0x0012A40 .

WHY. Numbers make the pictures concrete and let us machine-check the split. Slicing the -bit pattern from the top: Tag high bits , Index next bits , Offset low bits .

PICTURE. The -bit binary of laid out with the cut lines drawn in, each slice's decimal value labelled underneath.


The one-picture summary

Everything above compressed into a single diagram: an address enters at the top; the low bits split off as Offset, the next bits fold (mod ) to pick the slot via Index, the top bits become the Tag that is compared against what the slot stores — hit or miss below.

Recall Feynman retelling — say it back in plain words

Memory is a mile-long street of numbered byte-boxes; an address is a box's number. The cache is greedy: instead of one box it grabs a whole neighbourhood of boxes — a line — because you usually want the next box soon. So the bottom bits of the address just say which box in the neighbourhood you meant: the offset. Chop those off and the number that's left names the whole neighbourhood — the line number. But the cache has only parking slots, way fewer than there are neighbourhoods, so it folds line numbers onto slots by keeping their low bits: the index — that's the parking rule. Many neighbourhoods park in the same slot, so when you arrive you must check a label to be sure the car in the slot is yours: the leftover high bits are that label, the tag, and there's a tiny valid flag saying the slot isn't empty. Lookup is three moves: index drives you to the slot, tag confirms it's the right line, offset grabs your byte. And no matter how you size the cache — tiny lines, huge associativity, memory that fits entirely — the three widths always add back up to .


Connections