σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality skill›Module 1 · Caches from the ground up›Topic 1.3·~55 minIntermediate

Associativity and conflict misses

Skill positionModule 1 of 3 · Topic 3 of 4
1.3 Associativity and conflict misses
—Lab score— / 100Total XP—Streak—

6 min readCode examples: 4

  1. 01The weakness of one slot
  2. 02Sets and ways
  3. 03Who gets thrown out: LRU
  4. 04Rows a power of two apart
  5. 05What you will build

In a cache with one slot per line, two busy lines that share a slot throw each other out again and again. Give every slot several ways and the problem mostly disappears, unless the rows of your table are exactly 4096 bytes apart. This lecture builds a set-associative cache and shows how a power-of-two row size can make 100% of the reads miss.

01

The weakness of one slot

In the direct-mapped cache of the last topic every memory line has exactly one slot. If your program keeps using two lines that map to the same slot, say line 0 and line 8 in an 8-slot cache, then every read of one evicts the other, although the other seven slots may be empty. Nothing is wrong with the size of the cache: the lines are fighting for a place that is too small. A miss of this kind is called a conflict miss.

02

Sets and ways

The fix is to group slots. Slots are collected into sets, and each slot inside a set is a way. A memory line belongs to one set, found by the same remainder as before, but it may sit in any way of that set. Two colliding lines now share a set with room for both. The number of sets follows from the sizes:

sets = cache size ÷ (line size × ways)(1)a 32 KB cache with 64-byte lines and 8 ways has 32768 ÷ (64 × 8) = 64 sets

An address now splits into three parts. The offset is the byte inside the line. The set index says which set the line belongs to. The tag is the rest of the line number, and it is what the cache compares to see whether a way holds this line. The function below splits an address for a cache with 64-byte lines and 64 sets.

main.pyPython
def split_address(address, line_bytes, n_sets):    line = address // line_bytes    return line // n_sets, line % n_sets, address % line_bytes1print(split_address(74565, 64, 64))
(18, 13, 5)
  1. 1Tag, set index and offset: each is one integer division or remainder of the address.

Address 74565 is byte 5 of line 1165. Line 1165 goes to set 13 (1165 modulo 64), and 18 is what is left of the line number, the tag. Any line that has set 13 will compete for the eight ways of that set, whatever its tag.

03

Who gets thrown out: LRU

When a set is full and a new line arrives, one of the lines has to go. The usual rule is LRU: evict the line that was used least recently. We can keep each set as a Python list ordered from the least recently used to the most recently used.

  1. 1
    Find the setThe line number modulo the number of sets.
  2. 2
    Look in every wayOn a hit, move the line to the most recently used end.
  3. 3
    Add on a missPut the new line at the most recently used end.
  4. 4
    Drop the oldestIf the set now holds more lines than ways, remove the first one.

Here is the whole cache. The run at the bottom reads two lines, addresses 0 and 256, that collide in a 256-byte cache with 64-byte lines, first with one way and then with two.

main.pyPython
class SetAssocCache:    def __init__(self, size_bytes, line_bytes, ways):        self.line_bytes = line_bytes        self.ways = ways        self.n_sets = size_bytes // (line_bytes * ways)        self.sets = [[] for _ in range(self.n_sets)]        self.hits = 0        self.misses = 0    def access(self, address):        line = address // self.line_bytes        lines = self.sets[line % self.n_sets]        if line in lines:            lines.remove(line)            lines.append(line)1            self.hits += 1            return True        self.misses += 1        lines.append(line)        if len(lines) > self.ways:            lines.pop(0)2        return Falsefor ways in (1, 2):    cache = SetAssocCache(size_bytes=4 * 64, line_bytes=64, ways=ways)    flags = [cache.access(a) for a in [0, 256, 0, 256, 0, 256]]    print(ways, "way:", flags)
1 way: [False, False, False, False, False, False]
2 way: [False, False, True, True, True, True]
  1. 1Moving a hit line to the end keeps the list in order: the first entry is always the least recently used.
  2. 2The full set loses its first entry, the least recently used line.

With one way the two lines evict each other and all six reads miss. With two ways both lines fit in their set: after the first two cold misses, every read hits. Adding a way removed all the conflict misses of this pattern.

Why not as many ways as possible?

Every way adds a tag comparison to each read, which costs time and power. Level 1 caches often have 8 ways and larger caches 12 to 16, because past that point each extra way removes very few misses.

04

Rows a power of two apart

Now the trap. A table of 512 float64 columns has rows of exactly 4096 bytes. Reading one column means reading addresses 0, 4096, 8192 and so on. In our cache a set index is the line number modulo 64, and 4096 bytes is exactly 64 lines, so every one of those addresses has the same set index. The code prints the set of the first six rows for a row size of 4096 bytes and for 4160 bytes, which is 4096 plus one extra line.

main.pyPython
n_sets = 64for row_bytes in (4096, 4160):    sets = [(i * row_bytes // 64) % n_sets for i in range(6)]    print(row_bytes, "->", sets)
4096 -> [0, 0, 0, 0, 0, 0]
4160 -> [0, 1, 2, 3, 4, 5]

With 4096-byte rows all the closes of a column fight for set 0. With 4160-byte rows they march through sets 0, 1, 2, 3 and so on, using the whole cache. The next code reads 96 closes twice and measures the miss rate for 1, 2, 4 and 8 ways.

main.pyPython
def column_trace(n_rows, row_bytes, passes):    return [i * row_bytes for i in range(n_rows)] * passesdef miss_rates_by_ways(trace, size_bytes, line_bytes, ways_list):    rates = {}    for ways in ways_list:        cache = SetAssocCache(size_bytes, line_bytes, ways)        for address in trace:            cache.access(address)        rates[ways] = cache.misses / len(trace)    return ratesfor row_bytes in (4096, 4160):    print(row_bytes, miss_rates_by_ways(column_trace(96, row_bytes, 2), 32768, 64, [1, 2, 4, 8]))
4096 {1: 1.0, 2: 1.0, 4: 1.0, 8: 1.0}
4160 {1: 0.5, 2: 0.5, 4: 0.5, 8: 0.5}

The padded table misses only on the first pass: 0.5 of all reads, whatever the number of ways. The aliased table misses on every read, even with 8 ways, because 96 lines need 96 places and the set has 8. Worse, LRU always evicts the line you will read next in a cyclic pattern, so the second pass finds nothing. More ways would only help once they reach the number of lines.

Powers of two are the worst row sizes

A row size that is a multiple of the set span (here 64 lines, 4096 bytes) puts a whole column into one set. Matrices and tables with 256, 512 or 1024 columns of float64 are the classic victims. Padding each row by one line, as in 4160, costs 1.6% of the memory and removes the problem.

What the real data saysfirst 96 SPY closes, read twice
Miss rate, rows 4096 bytes apart100%8 ways, 64 sets: every read misses
Miss rate, rows 4160 bytes apart50%only the first pass misses
Miss rate of the 4096-byte table by number of ways, in %
1 way100
2 ways100
4 ways100
8 ways100
05

What you will build

The lab follows the lecture: the address split, the LRU cache, the two kinds of trace, the comparison by number of ways and the study on the real SPY table.

  • Function · questionBuilds on
  • split_address(address, line_bytes, n_sets)Which tag, set and offset does an address have?Integer division
  • SetAssocCache(size_bytes, line_bytes, ways).access(address)Is this read a hit in an LRU cache with several ways?Sets of lines in LRU order
  • conflict_trace(n_lines, line_bytes, n_sets, rounds)Which addresses all collide in one set?Plain arithmetic
  • column_trace(n_rows, row_bytes, passes)Which addresses does reading a column touch?Plain arithmetic
  • miss_rates_by_ways(trace, size_bytes, line_bytes, ways_list)How do more ways change the miss rate?SetAssocCache
  • study(bars)What do rows 4096 bytes apart cost?column_tracemiss_rates_by_ways

Key takeaways

  • Ways remove conflicts. A set with several ways lets colliding lines live together; LRU decides who leaves when the set is full.
  • The address picks the set. Tag, set index and offset come straight from the address, so which lines collide is decided by the layout of your data.
  • Avoid power-of-two rows. Row sizes that are multiples of the set span put a whole column in one set; one line of padding fixes it.
Quick check
A 32 KB cache with 64-byte lines and 8 ways. How many sets does it have?
Pick an answer.

Next topic: the cache now holds the right lines when they do not collide, but how long do lines stay useful? In Temporal locality and working sets you measure how soon a value is read again, and find the cache size at which a program starts to fit.

Lab · what's graded

6 tasks · 100 pts
  • Tag, set and offset10 pts
  • A set-associative LRU cache30 pts
  • Lines that collide10 pts
  • Reading a column10 pts
  • The miss rate by number of ways15 pts
  • The study on the SPY table25 pts
Explorer · lab-cl-3
▾ lab-cl-3
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-cl-3$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-cl-3 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes