Visual walkthrough — Cache lines, tags, index, offset
We start from the most basic fact imaginable: memory is a row of numbered boxes. Everything else grows out of that.
Step 1 — What an address even is
WHAT. Picture main memory as one enormously long row of little boxes, each holding exactly one byte (8 bits). Every box has a number painted on it: box , box , box , … That painted number is the address. "Byte-addressable" just means each byte has its own address.
WHY. Before we can slice an address we must agree on what it counts. It counts bytes from the start of memory. Nothing more mystical than a house number on a very long street.
PICTURE. Below, the row of boxes with their addresses written underneath. If the address has bits, the biggest number we can write is , so there are boxes total.

Step 2 — Group boxes into lines (blocks)
WHAT. We never move one byte between memory and cache — we move a whole line: a fixed run of neighbouring bytes. So mentally draw fences every boxes. Each fenced group is one line.
WHY. Because of spatial locality (Spatial and temporal locality): touch one byte and you will likely touch its neighbours soon, so hauling the whole neighbourhood at once pays off. Fences at regular intervals of mean the first line is bytes , the next is , etc.
PICTURE. Same row, now with red fences every boxes (we use in pictures so it fits; real hardware uses ). Each line gets a line number written above it.

Step 3 — The offset field falls out of that remainder
WHAT. The remainder names the byte inside the line. It ranges over — exactly values. To write a number that goes from to in binary you need bits. Those are the offset bits.
WHY THIS TOOL — why and not something else? answers one precise question: "how many binary digits do I need to count different things?" One bit counts 2 things, two bits count 4, three bits count 8… bits count . Turn that around: to count things you need bits. That is the whole reason appears everywhere on this page — it converts "how many items" into "how many bits."
PICTURE. Zoom into one line of bytes. Under each byte we write its 2-bit offset (). Notice: the offset is simply the low bits of the address, because dividing a binary number by just chops off its lowest bits — and the chopped-off part is the remainder.

Step 4 — The cache is a short shelf, so many lines share a slot
WHAT. The cache is a tiny shelf of sets (rows), numbered . Memory has far more lines than the shelf has sets, so we must decide: line number which set? The rule is take the low bits of the line number: .
WHY the low bits, not the high bits? Watch what each choice does to a contiguous array (lines ):
- Low bits → set — neighbours fan out across all sets. Good.
- High bits → the first many lines all share the same low-changing pattern and pile into one set, evicting each other endlessly ("thrashing"). Bad.
So we pick low bits precisely to scatter neighbours. (More on what happens inside a set in Cache associativity & replacement policies.)
PICTURE. Left: memory's many line numbers, colour-coded by which set they land in. Right: the short cache shelf of sets. Coloured arrows show low-bit lines fanning across sets; a greyed-out "high-bit" arrow bundle shows the bad alternative collapsing into one set.

Step 5 — The leftover bits become the tag
WHAT. Many line numbers share a set (that's the whole reason we needed a cache). So when we look in set , we must confirm the line sitting there is the one we asked for. The bits left over — everything above the index — are unique among all lines that map to the same set. Store them; on a lookup, compare them. Those are the tag bits.
WHY store only the leftover, not the whole address? The index already told us the row (no need to re-store it — it's implied by where the line sits). The offset picks a byte inside the fetched line (not part of identity). So only the top slice distinguishes the competing lines. Storing fewer bits saves silicon (Memory hierarchy and latency numbers cares about every transistor).
PICTURE. One set drawn as a slot holding: a valid bit, a stored tag, and the line's data bytes. An incoming address's tag field flows into a comparator (); green = hit, red = miss.

Step 6 — Slice a real address, watching the boundaries
WHAT. Take 0xDEADBEEF, a 32-bit address, on a cache with ( offset bits)
and sets ( index bits). We peel the low 6 (offset), then the next 8
(index), then the rest is tag.
WHY the warning. Hex digits are 4-bit chunks, but and are not multiples of 4, so the fields do not line up with hex digits. You must think in bits, not nibbles.
PICTURE. The 32-bit string with two knives cutting at bit 6 and bit 14. The low 14 bits read
11 1110 1110 1111.

- Offset = low 6 bits
1011110x2Fbyte 47 in the line. - Index = next 8 bits (bits 13..6)
111110110xFBset 251. - Tag = top 18 bits, stored & compared in set 251.
Step 7 — The degenerate cases (never let the reader hit an unshown one)
WHAT & WHY & PICTURE — three extremes, all consistent with Step 5:

- Fully associative (). Index width — no index bits at all. Any line may sit in any slot, so we compare the tag of every slot. Address .
- Direct-mapped (, one line per set). Maximum index bits, minimum tag bits. Each line has exactly one home slot; no choice, no search — fastest lookup, most collisions.
- Line of one byte (). Offset — no offset bits. Then addresses bytes directly. Real caches never do this (kills spatial locality) but the formula still holds — a good sanity check that makes the field vanish.
The one-picture summary

One picture, whole derivation: the address on top, two cuts at and , three arrows into three jobs — TAG "is it the right block?" → comparator; INDEX "which set?" → row selector; OFFSET "which byte?" → byte picker inside the fetched line.
Recall Feynman: the whole walkthrough in plain words
Memory is a mile-long row of numbered boxes; the address is the box number (Step 1). We fence the
boxes into packs of — that's a line — because grabbing one box means we'll want its
neighbours (Step 2). The bottom few bits of the address say which box inside the pack — the
offset — and there are boxes so we need bits (Step 3). Our little shelf only has
cubbies, so the next bits pick which cubby — the index — and we deliberately use the
low bits so neighbouring packs scatter instead of fighting over one cubby (Step 4). Whatever bits
are left on top get written on a sticky note in the cubby — the tag — so later we can check the
pack sitting there is really ours (Step 5). Then we practised on 0xDEADBEEF, being careful that
-bit and -bit fields don't line up with hex digits (Step 6). Finally the extremes: no cubby
choices at all (fully associative, zero index), one cubby per pack (direct-mapped, max index), and
one-box packs (zero offset) — all still obeying (Step 7).
Recall Self-check clozes
The field that depends only on line size is the offset, width ==.
The field that picks the row is the index, width .
The leftover top bits stored & compared are the tag, width ==.
For 0xDEADBEEF with 6 offset + 8 index bits: offset ::: 0x2F (byte 47), set ::: 0xFB = 251.
Related building blocks: Spatial and temporal locality · Cache associativity & replacement policies · Memory hierarchy and latency numbers · Virtual vs physical addressing (VIPT/PIPT) · TLB and page tables · Cache coherence (MESI).