σμ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 Academy路线图学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
路线图学习路线
AO
+1.2k 位学习者
登录
  • 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 Academy路线图学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
路线图学习路线
AO
+1.2k 位学习者
登录
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality 学习路线›模块 1 · 从零理解缓存›主题 1.4·~45分钟中级

时间局部性与工作集

路线进度模块 1/3 · 主题 4/4
1.4 时间局部性与工作集
—实验得分— / 100总 XP—连续天数—

阅读约 5 分钟4 个代码示例

  1. 01再次读同一行
  2. 02从距离到命中率
  3. 03工作集与悬崖
  4. 04对真实价格做滚动均值
  5. 05你要实现什么

缓存只对你会再次读取的数据有帮助,关键在于多快会再读。本讲用一个数,重用距离,来衡量它,并用它一次性预测所有缓存大小的命中率,包括循环只是稍微大了一点就装不下的那道悬崖。

01

再次读同一行

前两个主题讲的是行放到哪里,这一个讲的是它们什么时候回来。一个程序读了一个价格,做点事,不久后又读同一个价格,这就是时间局部性。间隔越短,这条行还留在缓存里的机会就越大。想想书桌上的书:一分钟前用过的还在,上个月用过的早已放回书架。

定义重用距离

对某条行的一次读取而言,从上一次读这条行到现在之间,读过的其他不同行的个数。一条行的第一次读取没有距离。

连续读两次第 5 行,距离是 0。在序列 1, 2, 3, 1 里,最后一次读的距离是 2,因为中间来过第 2 行和第 3 行。在两次读第 1 行之间读了十次第 2 行,也只算一条行。代码维护一个按最近使用时间排序的行列表,最近的在最后。一条行再次出现时,它在列表中后面的那些行,恰好就是此后用过的行。

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. 1列表里排在它后面的行,正是自它上次被读以来用过的行:它们的个数就是距离。
02

从距离到命中率

这里有个很有用的事实。取一个能装 C 条行、并替换最近最少使用行的缓存,不论行号是什么。它任何时候装的都是最近用过的 C 条不同的行。一次读取命中,恰好是它的行在其中,也就是自上次使用以来读过的不同行少于 C 条。换句话说,当重用距离小于 C 时这次读取命中。

hit ⇔ reuse distance < capacity(1)适用于完全灵活的 LRU 缓存:任何行都可以放在任何位置,所以只有行数起作用

下面的代码验证这个说法。它对容量 1 到 4 模拟 LRU 缓存,同时只用距离、完全不用缓存来算命中率。两列结果一致。距离只需算一次,就能给出每种缓存大小的命中率。

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. 1两列数字相同:只靠距离就能预测每种缓存大小的命中率。
一遍遍历给出所有缓存大小

没有距离的话,每测一种大小就得重新跑一次模拟器。有了距离,对轨迹遍历一遍就能得到命中率随缓存大小变化的整条曲线。性能工程师把它叫作缺失率曲线。

03

工作集与悬崖

一段读取的工作集是它们碰到的不同行的集合。工作集装得进缓存,程序几乎完全从缓存里运行;如果只比缓存大一点点,LRU 可能彻底失效。取一个把 48 条行按顺序读三遍的循环:每条行在读过 47 条别的行之后才被再读,所以所有重用距离都是 47。因此 47 条行及以下的缓存一次都不命中,而 48 条行的缓存在第一遍之后每次读都命中。

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
少一条行就可能完全不命中

当循环大到超出缓存时,命中率不会平滑下降。对于 LRU 和以同样顺序反复遍历同样行的循环,命中率会在工作集刚好装不下的那一条行处,从 0.667 一下掉到 0.0。一个程序可能在某个输入规模下很快,而在稍大一点的规模下慢上许多倍。

04

对真实价格做滚动均值

按笨办法算的滚动均值,每天都读最近 20 个值:也就是重读前一天读过的 20 个值中的 19 个。这是个时间局部性很强的程序。代码为前 600 个收盘价构造读取地址、转成行号,测量重用距离、四种缓存大小下的命中率,以及连续 20 次读取的工作集。

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

只要一条行,就能让 83% 的读取命中,因为数组的下一个值就在刚读的那条行里。四条行达到 99.4%。整个数据集的 75 条行根本不需要都装进去:20 次读取的工作集只有 4 条行,所以很小的缓存就够了。这就是数据大小与工作集大小的区别。

真实数据怎么说SPY 前 600 个收盘价的 20 日滚动均值
读取次数11,620来自 75 条不同的行
20 次读取的工作集4lines整个数据集根本不需要都装下
命中率 90% 所需的缓存大小3lines192 字节的缓存
遍历 48 条行,缓存 32 条行0.0hits48 条行时为 0.667
滚动均值按缓存大小的命中率(%)
1 条行83.1
2 条行83.1
4 条行99.4
8 条行99.4
05

你要实现什么

实验要求你实现上面这些部分,再加一个为给定命中率找最小缓存的辅助函数,最后对真实的 SPY 收盘价做研究。

  • 函数 · 问题依赖
  • reuse_distances(lines)每条行多久会被再读?按最近使用顺序的列表
  • lru_hit_rate(lines, capacity)模拟的 LRU 缓存命中多少?按 LRU 顺序的行
  • hit_rate_from_distances(distances, capacity)不用缓存也能得到同样的命中率?reuse_distances
  • working_set(lines, window)一段读取会碰到多少条行?滑动窗口
  • capacity_for(lines, target)缓存最小可以多小?reuse_distanceshit_rate_from_distances
  • rolling_mean_trace(n, window, elem_bytes)笨办法的滚动均值读哪些地址?简单算术
  • study(bars)SPY 收盘价上的滚动均值局部性有多强?rolling_mean_tracecapacity_for

要点

  • 重用距离预测命中。 容量为 C 条行的 LRU 缓存恰好命中距离小于 C 的读取,所以一遍距离就给出每种大小的命中率。
  • 工作集决定大小。 要装得下的是一段读取的工作集,而不是全部数据:75 条行的表,4 条行就够了。
  • 小心悬崖。 工作集比 LRU 缓存大一条行的循环可能丢光所有命中;要在你实际运行的规模上测量。
快速检查
读取的行依次是 A、B、C、A。最后一个 A 的重用距离是多少?
请选择一个答案。

下一个主题:你现在知道布局如何变成访问轨迹、轨迹如何变成命中与缺失。在结构体数组与数组结构体里,你要用它为逐笔表选布局,比较两种布局下读列和读整行的表现。

实验:评分内容

7 个任务 · 100 分
  • 重用距离20 分
  • 模拟的 LRU 缓存15 分
  • 由距离得到的命中率10 分
  • 窗口的工作集15 分
  • 最小的缓存10 分
  • 笨办法的滚动均值10 分
  • 基于 SPY 表的研究20 分
资源管理器 · lab-cl-4
▾ lab-cl-4
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-4$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

lab-cl-4 中的所有内容将恢复为初始文件,终端也会被清空。你当前的解答将丢失,且无法撤销。

将被重置
  • main.py → 初始代码
  • 终端历史与测试输出
  • 未提交的更改
将被保留
  • 实验最高得分
  • 你的 XP 与连续天数
  • 讲解进度与笔记