Cache memory

Intermediate

A cache keeps copies of recently used memory blocks. When the processor asks for an address, the cache must answer one question fast: is that block here? (A hit, or else a miss: fetch it from the level below.)

Splitting the address

With blocks of B bytes, a cache of L lines, and S sets, an address splits into three fields:

FieldBitsMeaning
offsetlog₂ Bwhich byte inside the block
indexlog₂ Swhich set to look in
tagthe restwhich of the many blocks that share this set it is

Each line stores a valid bit and the tag of the block it holds. A hit means: the line is valid and its tag matches.

address_bits, block, lines, ways = 16, 16, 8, 2
sets = lines // ways
offset_bits = block.bit_length() - 1
index_bits = sets.bit_length() - 1
tag_bits = address_bits - index_bits - offset_bits
print("offset", offset_bits, "bits, index", index_bits, "bits, tag", tag_bits, "bits")

address = 0x1234
offset = address & (block - 1)
index = (address >> offset_bits) & (sets - 1)
tag = address >> (offset_bits + index_bits)
print(hex(address), "-> tag", tag, " set", index, " offset", offset)
Open in compiler

Where may a block go?

  • Direct-mapped: each block has exactly one possible line (block number mod lines). Simple and fast, but two blocks that map to the same line throw each other out again and again (conflict misses).
  • Set-associative: lines are grouped into sets of k (k-way). A block may go in any line of its set; all k tags are compared at once. 4- to 16-way is common.
  • Fully associative: one set; a block may go anywhere. Fewest conflicts, but every tag must be compared.

When a set is full, a replacement policy chooses the block to throw out, usually the least recently used (LRU).

Try the same addresses with each kind of placement:

Addresses 0 and 16 fall in blocks 0 and 4, which share line 0 of this direct-mapped cache. Switch to 2-way and the conflict disappears.

lines, block = 4, 4
cache = [None] * lines              # the block number held by each line
result = ""
for address in [0, 4, 16, 0, 8, 20, 4, 0]:
    b = address // block
    line = b % lines
    if cache[line] == b:
        result += "H"
    else:
        result += "M"
        cache[line] = b
print("hits and misses:", result)
print("hits:", result.count("H"), "of", len(result))
Open in compiler

Writes

  • Write-through: every write goes to the cache and to memory. Simple, but slow without a write buffer.
  • Write-back: writes go only to the cache, and a dirty bit marks the block; it is written to memory when it is thrown out. Less traffic; used by most caches today.

Misses come in three kinds, the "three Cs": compulsory (the first use of a block), capacity (the cache is too small) and conflict (too many blocks compete for one set).

Common mistakes

  • Using the address instead of the block number to pick a line: first divide by the block size.
  • Forgetting the valid bit: an empty line may still hold an old tag.
  • Thinking more associativity is free: comparing more tags at once costs time and power.

Quick check

1. A cache has 64-byte blocks. How many offset bits does an address have?
2. Direct-mapped, 8 lines, 1-byte blocks: which line holds address 21?
3. What does the tag tell the cache?
4. In a write-back cache, when does a changed block reach memory?

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.