σμ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.4·~45 minIntermediate

Temporal locality and working sets

Skill positionModule 1 of 3 · Topic 4 of 4
1.4 Temporal locality and working sets
—Lab score— / 100Total XP—Streak—

5 min readCode examples: 4

  1. 01Reading the same line again
  2. 02From distances to hit rates
  3. 03Working sets and the cliff
  4. 04A rolling mean on real prices
  5. 05What you will build

A cache only helps with data you read again, and what matters is how soon. This lecture measures that with one number, the reuse distance, and uses it to predict the hit rate of every cache size at once, including the cliff where a loop is just a little too big to fit.

01

Reading the same line again

The last two topics were about where lines go. This one is about when they come back. A program that reads a price, does some work, and reads the same price again a moment later is showing temporal locality. The shorter the gap, the better the chance that the line is still in the cache. Think of the books on your desk: a book you used a minute ago is still there, a book you used last month went back to the shelf.

DefinitionReuse distance

For a read of a line, the number of different other lines read since the previous read of the same line. The first read of a line has no distance.

Reading line 5 twice in a row gives distance 0. In the sequence 1, 2, 3, 1 the last read has distance 2, because lines 2 and 3 came in between. Reading line 2 ten times between two reads of line 1 still counts as one line. The code keeps a list of the lines in the order of their last use, most recent last. When a line comes again, the lines after it in the list are exactly the ones used since.

main.pyPython
def reuse_distances(lines):    stack = []    out = []    for line in lines:        if line in stack:            position = stack.index(line)            out.append(len(stack) - 1 - position)1            stack.pop(position)        else:            out.append(None)        stack.append(line)    return outprint(reuse_distances([1, 2, 3, 1, 2, 1, 1]))
[None, None, None, 2, 2, 1, 0]
  1. 1The lines after this one in the list are exactly the lines used since its last read: their count is the distance.
02

From distances to hit rates

Here is the useful fact. Take a cache that holds C lines and evicts the least recently used one, whatever the line numbers. At any moment it holds the C different lines used most recently. A read hits exactly when its line is among them, that is, when fewer than C different lines were read since its last use. In other words, the read hits when its reuse distance is smaller than C.

hit ⇔ reuse distance < capacity(1)for a fully flexible LRU cache: any line may go anywhere, so only the number of lines matters

The code below checks the claim. It simulates an LRU cache for capacities from 1 to 4 and also computes the hit rate from the distances alone, with no cache at all. The two columns agree. The distances are computed once, and then they give the hit rate of every cache size.

main.pyPython
def lru_hit_rate(lines, capacity):    cache = []    hits = 0    for line in lines:        if line in cache:            hits += 1            cache.remove(line)        elif len(cache) >= capacity:            cache.pop(0)        cache.append(line)    return hits / len(lines)def hit_rate_from_distances(distances, capacity):    hits = sum(1 for d in distances if d is not None and d < capacity)    return hits / len(distances)lines = [1, 2, 3, 1, 2, 3, 4, 1, 2, 3]d = reuse_distances(lines)for capacity in (1, 2, 3, 4):    print(capacity, "lines:", round(lru_hit_rate(lines, capacity), 3), round(hit_rate_from_distances(d, capacity), 3))1
1 lines: 0.0 0.0
2 lines: 0.0 0.0
3 lines: 0.3 0.3
4 lines: 0.6 0.6
  1. 1Same numbers in both columns: the distances alone predict the hit rate of every cache size.
One pass gives every cache size

Without distances you would have to rerun a simulator for every size you want to test. With them, one pass over the trace gives the whole curve of hit rate against cache size. Performance engineers call this the miss-ratio curve.

03

Working sets and the cliff

The working set of a stretch of reads is the set of different lines they touch. If the working set fits in the cache, the program runs almost entirely from cache. If it is only slightly bigger, LRU can fail completely. Take a loop that reads 48 lines in order, three times. Each line is read again after 47 others, so every reuse distance is 47. A cache of 47 lines or fewer therefore never hits, and a cache of 48 hits on every read after the first pass.

main.pyPython
def sweep_lines(n_lines, passes):    return list(range(n_lines)) * passeslines = sweep_lines(48, 3)for capacity in (16, 32, 47, 48):    print(capacity, "lines:", round(lru_hit_rate(lines, capacity), 3))
16 lines: 0.0
32 lines: 0.0
47 lines: 0.0
48 lines: 0.667
One line too small can mean zero hits

The hit rate does not fall smoothly as a loop outgrows the cache. With LRU and a loop through the same lines in the same order, it drops from 0.667 to 0.0 at the exact line where the working set stops fitting. A program can be fast on one input size and many times slower on a slightly larger one.

04

A rolling mean on real prices

A rolling mean computed the slow way reads the last 20 values for every day: it re-reads 19 of the 20 values it read the day before. That is a program with a lot of temporal locality. The code builds the addresses read for the first 600 closes, turns them into lines, and measures the distances, the hit rate for four cache sizes, and the working set of 20 consecutive reads.

main.pyPython
def rolling_mean_trace(n, window, elem_bytes):    trace = []    for end in range(window - 1, n):        for position in range(end - window + 1, end + 1):            trace.append(position * elem_bytes)    return tracedef working_set(lines, window):    return max(len(set(lines[i:i + window])) for i in range(len(lines) - window + 1))lines = [address // 64 for address in rolling_mean_trace(600, 20, 8)]d = reuse_distances(lines)print(len(lines), "reads from", len(set(lines)), "lines")for capacity in (1, 2, 4, 8):    print(capacity, "lines:", round(hit_rate_from_distances(d, capacity), 3))print("working set of 20 reads:", working_set(lines, 20), "lines")
11620 reads from 75 lines
1 lines: 0.831
2 lines: 0.831
4 lines: 0.994
8 lines: 0.994
working set of 20 reads: 4 lines

One line is enough for 83% of the reads, because the next value of the array is in the line that was just read. Four lines reach 99.4%. The 75 lines of the whole data set never have to fit: the working set of 20 reads is only 4 lines, so a tiny cache is enough. That is the difference between the size of the data and the size of the working set.

What the real data says20-day rolling mean over the first 600 SPY closes
Reads11,620from 75 different lines
Working set of 20 reads4linesthe whole data set never has to fit
Cache size for 90% hits3lines192 bytes of cache
Sweep of 48 lines, 32-line cache0.0hitswith 48 lines: 0.667
Hit rate of the rolling mean by cache size, in %
1 line83.1
2 lines83.1
4 lines99.4
8 lines99.4
05

What you will build

The lab asks for the pieces above, plus the helper that finds the smallest cache for a hit rate, and ends with the study on the real SPY closes.

  • Function · questionBuilds on
  • reuse_distances(lines)How soon is each line read again?A list in last-use order
  • lru_hit_rate(lines, capacity)What does a simulated LRU cache hit?Lines in LRU order
  • hit_rate_from_distances(distances, capacity)The same hit rate, without a cache?reuse_distances
  • working_set(lines, window)How many lines does a stretch of reads touch?A sliding window
  • capacity_for(lines, target)How small can the cache be?reuse_distanceshit_rate_from_distances
  • rolling_mean_trace(n, window, elem_bytes)Which addresses does a slow rolling mean read?Plain arithmetic
  • study(bars)How local is a rolling mean on the SPY closes?rolling_mean_tracecapacity_for

Key takeaways

  • Reuse distance predicts hits. An LRU cache of C lines hits exactly the reads whose distance is below C, so one pass of distances gives the hit rate of every size.
  • The working set sets the size. What must fit is the working set of a stretch of reads, not the whole data: 4 lines were enough for a 75-line table.
  • Beware the cliff. A loop whose working set is one line too big for an LRU cache can lose every hit; measure at the sizes where you will run.
Quick check
The reads are lines A, B, C, A. What is the reuse distance of the last A?
Pick an answer.

Next topic: you now know how a layout turns into a trace and a trace into hits and misses. In Array of structs against struct of arrays you use that to choose how to lay out a table of ticks, comparing column reads and whole-row reads in both layouts.

Lab · what's graded

7 tasks · 100 pts
  • Reuse distances20 pts
  • A simulated LRU cache15 pts
  • The hit rate from distances10 pts
  • The working set of a window15 pts
  • The smallest cache10 pts
  • A slow rolling mean10 pts
  • The study on the SPY table20 pts
Explorer · lab-cl-4
▾ lab-cl-4
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-cl-4$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-cl-4 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