σμ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 学习路线›模块 3 · 整台机器›主题 3.1·~50分钟高级

预取

路线进度模块 3/3 · 主题 1/7
3.1 预取
—实验得分— / 100总 XP—连续天数—

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

  1. 01在需要之前就请求
  2. 02预测下一条行:步长
  3. 03预取器值多少
  4. 04读 SPY 收盘价的三种方式
  5. 05你要实现什么

只有程序必须等待时,一次缺失才会花 80 ns。预取器会赶在程序之前请求缓存行,等到读取真正到来时,等待已经结束。本讲搭两个预取器,用覆盖率和准确率给它们打分,并说明它们在哪里有效(步长规则的循环),在哪里无能为力(顺序无法预测的读取)。

01

在需要之前就请求

到目前为止,缓存只在读取未命中时才加载缓存行。预取则把加载提前。如果猜对了,之后到来的读取就会发现这条行已经在缓存里,直接命中。现代处理器都有硬件预取器,监视读取的地址并猜接下来会读什么。最简单的猜测是:读了第 L 行的人接下来会要第 L + 1 行。我们提供的缓存有个 fill 方法,加载一条行而不计命中也不计缺失,这正是预取。

main.pyPython
def baseline_misses(trace):    cache = Cache()    for address in trace:        cache.access(address)    return cache.missesdef next_line_misses(trace):    cache = Cache()    for address in trace:        cache.access(address)        cache.fill((address // LINE_BYTES + 1) * LINE_BYTES)1    return cache.missestrace = [8 * i for i in range(800)]print("no prefetch:", baseline_misses(trace), "- next line:", next_line_misses(trace))jumps = [i * 4096 for i in range(50)]print("jumps of 4096 bytes:", baseline_misses(jumps), "- next line:", next_line_misses(jumps))
no prefetch: 100 - next line: 1
jumps of 4096 bytes: 50 - next line: 50
  1. 1这里预取下一条缓存行不花任何代价:fill 既不算命中也不算缺失。

按顺序读 800 个值,普通缓存每条缓存行缺失一次,共 100 次。用了“下一行预取器”,总共只缺失一次:在第一次读之后,接下来需要的那条行总是已经在缓存里了。第二个例子显示了它的局限。如果程序每次跳 4096 字节,当前行的下一行毫无用处,预取器什么也省不下:50 次读取仍然缺失 50 次。

02

预测下一条行:步长

真实的预取器会在程序读取的缓存行里找规律。最常见的是固定步长:程序读第 0、3、6、9 行,每次跳三行。步长检测器记住上一条行和上一个差值,当新的差值等于上一个时就认定有规律,下一条行就是当前行加上这个差值。要有两个相等的差值才能确定,所以前两次读取它从不猜。

  1. 1
    忽略重复再读同一条行,说明不了任何步长。
  2. 2
    计算差值新行减去上一行。
  3. 3
    与上一个差值比较差值相等就是有规律:预测新行加上差值。
  4. 4
    记住把新差值和新行留给下一次读取。
main.pyPython
class StridePrefetcher:    def __init__(self):        self.last_line = None        self.last_delta = None    def observe(self, line):        if line == self.last_line:            return None        prediction = None        if self.last_line is not None:            delta = line - self.last_line            if delta == self.last_delta and line + delta >= 0:1                prediction = line + delta            self.last_delta = delta        self.last_line = line        return predictionp = StridePrefetcher()print([p.observe(line) for line in [0, 3, 6, 9, 12]])p = StridePrefetcher()print([p.observe(line) for line in [5, 5, 5, 6, 7]])
[None, None, 9, 12, 15]
[None, None, None, None, 8]
  1. 1连续两个相等的差值:这是一个步长,所以下一行就是当前行加上差值。

第一个序列是三行一跳的步长。检测器在前两次读取时什么都不说,之后预测 9、12、15,始终领先一步。第二个序列里对第 5 行的重复读取被忽略,步长 1 直到最后一次读取才被确认。好的检测器很谨慎:猜错是有代价的,下面就会看到。

03

预取器值多少

用两个数给预取器打分。覆盖率说明它解决了多少问题;准确率说明它的工作有多少是有用的。为了测量,我们让轨迹通过缓存:先做一次读取,问预取器接下来要装什么,记下哪些行是被预取的,并在一次读取命中其中某条时计数。

coverage = 1 − misses with ÷ misses without accuracy = useful ÷ issued(1)覆盖率针对程序的缺失,准确率针对预取器自己的工作
main.pyPython
def run_with_prefetcher(trace, prefetcher):    cache = Cache()    marked = set()    prefetches = 0    useful = 0    for address in trace:        line = address // LINE_BYTES        if cache.access(address):            if line in marked:                useful += 1                marked.discard(line)        else:            marked.discard(line)        target = prefetcher.observe(line)        if target is not None and not cache.contains(target * LINE_BYTES):            cache.fill(target * LINE_BYTES)            prefetches += 1            marked.add(target)    return {"demand_misses": cache.misses, "prefetches": prefetches, "useful": useful}def quality(trace):    baseline = baseline_misses(trace)    run = run_with_prefetcher(trace, StridePrefetcher())    coverage = 1 - run["demand_misses"] / baseline    accuracy = run["useful"] / run["prefetches"] if run["prefetches"] else 0.0    return baseline, run, round(coverage, 3), round(accuracy, 3)import randomordered = [8 * i for i in range(2000)]shuffled = [8 * p for p in random.Random(1).sample(range(2000), 2000)]print("in order:", quality(ordered))print("shuffled:", quality(shuffled))
in order: (250, {'demand_misses': 3, 'prefetches': 248, 'useful': 247}, 0.988, 0.996)
shuffled: (250, {'demand_misses': 250, 'prefetches': 1, 'useful': 0}, 0.0, 0.0)

对按顺序的读取,预取器消除了 98.8% 的缺失,99.6% 的预取都被用上。对打乱顺序的读取它什么也不做:没有重复的步长,它几乎从不发出预取(只有一次),因此也不会造成坏处。这正是期望的行为。一个老在瞎猜的预取器会把缓存塞满没人读的行。

猜错是有代价的

无用的预取会占用内存带宽,还可能把程序仍然需要的缓存行挤出缓存。只有准确率高的预取器才值得拥有。这里的缓存足够大,能掩盖这种损害;在小缓存或繁忙的内存系统里,猜错的预取器会让程序比没有预取器时更慢。

04

读 SPY 收盘价的三种方式

实验用三种方式读 2,000 个收盘价:一个接一个存放、按顺序读;存放在每行五个值的行里(步长 40 字节);一个接一个存放,但按成交量从小到大的顺序读。前两种有规则的模式;第三种访问天数的顺序看起来是随机的,因为某一天的成交量对下一天的成交量没有任何提示。

真实数据怎么说SPY 前 2,000 个收盘价,32 KB 8 路缓存,步长预取器
按顺序:缺失数250 → 3覆盖率 99%,准确率 100%
每行 5 个值:缺失数1,250 → 340 字节的步长也能被发现
按成交量:缺失数250 → 250覆盖率 0%:没有规律可找
按成交量顺序的预取次数1无法预测时它保持安静
步长预取器的覆盖率(%)
按顺序98.8
每行 5 个值99.8
按成交量0

关键在于预取器回答的问题:接下来是什么?如果答案藏在地址的规律里,硬件白送你。如果顺序取决于数据,比如按成交量排序后访问各行,谁也猜不出来。软件上的办法是改变数据布局,让访问重新变得规则,例如先把各行排好序,再按顺序读取。

05

你要实现什么

实验搭建两个预取器、给它们打分的模拟,以及对三种读 SPY 收盘价方式的研究。

  • 函数 · 问题依赖
  • baseline_misses(trace)不预取时缺失多少?Cache
  • next_line_misses(trace)用下一行预取器时缺失多少?Cache.fill
  • StridePrefetcher().observe(line)下一行是哪条?上一行与上一个差值
  • run_with_prefetcher(trace, prefetcher)发生了什么:缺失、预取、有用的预取?Cache
  • quality(trace)它的覆盖率和准确率是多少?baseline_missesrun_with_prefetcher
  • study(bars)预取器在 SPY 收盘价上哪里有帮助?qualitynext_line_misses

要点

  • 预取掩盖缺失。 它在读取之前就请求缓存行,读取时在缓存里找到而命中;缺失的代价还是要付,只是提前付了。
  • 它需要规律。 步长检测器预测各行之间的固定差值;顺序取决于数据的读取,则让它无规律可找。
  • 用两个数给它打分。 覆盖率是被消除的缺失占比,准确率是被用上的预取占比。准确率低就意味着浪费带宽和缓存空间。
快速检查
一个循环每隔两条行读一条(步长 3 行)。下一行预取器对它有什么作用?
请选择一个答案。

下一个主题:到目前为止,地址就是缓存行,只需要关心缓存命中。在 TLB 与大页里,每个地址首先要从虚拟页翻译成物理页,而存放翻译结果的小缓存 TLB 也会未命中,其方式取决于页的大小。

实验:评分内容

6 个任务 · 100 分
  • 不预取10 分
  • 下一行预取器15 分
  • 步长预取器20 分
  • 模拟预取器25 分
  • 覆盖率与准确率15 分
  • 基于 SPY 表的研究15 分
资源管理器 · lab-cl-7
▾ lab-cl-7
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-7$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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