σμ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.2·~45分钟中级

缓存行与空间局部性

路线进度模块 1/3 · 主题 2/4
1.2 缓存行与空间局部性
—实验得分— / 100总 XP—连续天数—

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

  1. 01内存以行为单位
  2. 02缓存是一张槽位表
  3. 03顺序读与跳着读
  4. 04表的布局决定步长
  5. 05你要实现什么

缓存从不只取一个字节,它一次取一整条 64 字节的缓存行,所以紧挨着你刚读的那个值已经在里面了。本讲搭建一个缓存,说明按顺序读比每次跳过一整行便宜八倍,并把它用到一张价格表上。

01

内存以行为单位

不管要多少字节,从内存取一次的代价都一样,所以硬件一次就多要一些。内存被切成 64 字节的块,缓存总是整块地装入、整块地替换。想象一部把 64 字节运上缓存的电梯:反正电梯要上去,顺便带上邻居不花任何成本,下次你需要其中一个时它已经在楼上了。

定义缓存行

一块连续的字节,多数处理器上是 64 字节,缓存把它当作一个单位装入和替换。第 n 行包含从 64n 到 64n + 63 的字节。

每个字节都有一个地址,一个说明它在哪里的数。要找地址所在的行,用地址除以行大小并丢掉余数。下面的代码打印四个地址所在的行。

main.pyPython
line_bytes = 64for address in (0, 63, 64, 130):    print(address, "is in line", address // line_bytes)
0 is in line 0
63 is in line 0
64 is in line 1
130 is in line 2

地址 0 和 63 同在第 0 行,只要装入其中一个,另一个就免费。地址 64 是第 1 行的开头,130 在第 2 行。本主题的一切都来自这个整数除法。

02

缓存是一张槽位表

最简单的缓存是一张表,能容纳多少行就有多少个槽位。32 KB、64 字节行的缓存有 512 个槽位。内存里的每一行只允许放进一个槽位,用取余求出:行号对槽位数取模。这叫直接映射缓存。因为许多行共用一个槽位,槽位还要记一个标签,说明它此刻装的是哪一行。

  1. 1
    求出行号用地址除以行大小。
  2. 2
    求出槽位把行号对槽位数取模。
  3. 3
    比较标签槽位里正是这一行,就是命中。
  4. 4
    未命中就替换把这一行装进槽位,丢掉原来的那一行。

缓存就是这些。下面的类用十五行写成了一个完整可用的版本。我们读四个相邻的值,再读下一行,然后回到地址 0。

main.pyPython
def line_of(address, line_bytes):    return address // line_bytesclass Cache:    def __init__(self, size_bytes, line_bytes):        self.line_bytes = line_bytes        self.n_lines = size_bytes // line_bytes        self.tags = [None] * self.n_lines        self.hits = 0        self.misses = 0    def access(self, address):        line = line_of(address, self.line_bytes)1        slot = line % self.n_lines        if self.tags[slot] == line:2            self.hits += 1            return True        self.misses += 1        self.tags[slot] = line        return Falsecache = Cache(size_bytes=512, line_bytes=64)reads = [0, 8, 16, 24, 64, 72, 0, 512, 0]print([cache.access(a) for a in reads])print(cache.hits, "hits and", cache.misses, "misses")
[False, True, True, True, False, True, True, False, False]
5 hits and 4 misses
  1. 1槽位就是行号对槽位数取模。
  2. 2命中意味着槽位里的标签正是这一行。

看这串标志:第一次读未命中并装入第 0 行,接下来三次读命中,因为都在第 0 行;64 未命中并装入第 1 行;72 命中;回到 0 又命中,因为第 0 行没被替换。接着地址 512 是第 8 行,它也映射到槽位 0(8 对 8 取模),于是把第 0 行替换掉,最后一次读地址 0 又未命中:一次冲突多花了一次缺失。

缓存大小以字节计,槽位以行计

64 字节行的 512 字节缓存只有 8 个槽位,不是 512 个。忘了用大小除以行大小,是造出“看着很大、行为却不对”的缓存的常见原因。

03

顺序读与跳着读

现在用这个缓存。一个 float64 占 8 字节,所以一行装得下 8 个。按顺序读一个数组,每一行的第一次读未命中,后面七次命中:八次里一次缺失。如果用步长来读,也就是每次读之间固定跳过若干个值,读到的就是不同的行。下面的代码测量步长为 1、2、4、8、16 个值时的缺失率。

main.pyPython
def sequential_trace(n, elem_bytes):    return [i * elem_bytes for i in range(n)]def strided_trace(n, elem_bytes, stride):    return [i * stride * elem_bytes for i in range(n)]def miss_rate(trace, size_bytes=32768, line_bytes=64):    cache = Cache(size_bytes, line_bytes)    for address in trace:        cache.access(address)    return cache.misses / len(trace)n = 4096for stride in (1, 2, 4, 8, 16):    print("stride", stride, "values:", miss_rate(strided_trace(n, 8, stride)))
stride 1 values: 0.125
stride 2 values: 0.25
stride 4 values: 0.5
stride 8 values: 1.0
stride 16 values: 1.0

步长每翻一倍,缺失率也翻一倍,直到 1.0:步长 8 个值时跳 64 字节,正好一整行,每次读都打开一条新行,缓存什么也省不下。更大的步长不会更糟,只会一样。规律就是下面这个公式。

miss rate = min(1, stride × value bytes ÷ line bytes)(1)适用于对新数据的一次长遍历,其中没有哪一行会被读两次
缓存行总是 64 字节吗?

几乎所有 x86 和服务器级 ARM 芯片都是。有些芯片的外层缓存用 128 字节,所以要测你目标机器的行大小。这里的缓存把它当参数,你可以两种都试。

04

表的布局决定步长

日线表每天有五个数:开、高、低、收、成交量。像大多数程序那样一行一行存,一天的五个数就并排放着。只读收盘价时,每次读之间要跳过四个值:步长 5,也就是 40 字节。把每一列单独存,收盘价就一个接一个:步长 1。数字一样,读取一样,只是布局不同。

main.pyPython
closes = 1000as_column = miss_rate(sequential_trace(closes, 8))as_rows = miss_rate(strided_trace(closes, 8, 5))print("column layout:", as_column)print("row layout:   ", as_rows)
column layout: 0.125
row layout:    0.625

收盘价隔着读时,缺失的读取多了五倍。这不会改变程序的结果,只改变速度,而且分析器里任何一行算术都看不出来:多出来的时间都花在等内存上了。

真实数据怎么说SPY 日线收盘价,5,405 次读取
缺失数,列布局676占读取的 12.5%
缺失数,行布局3,378占读取的 62.5%
未命中读取的占比(%)
列布局12.5
行布局62.5
05

你要实现什么

实验按同样的顺序要求你实现刚读过的几个部分,最后对真实的 SPY 表做研究。

  • 函数 · 问题依赖
  • line_of(address, line_bytes)这个地址在哪一行?整数除法
  • Cache(size_bytes, line_bytes).access(address)这次读取是否命中?line_of
  • sequential_trace(n, elem_bytes)按顺序读会碰到哪些地址?简单算术
  • strided_trace(n, elem_bytes, stride)跳着读会碰到哪些地址?简单算术
  • miss_rate(trace, size_bytes, line_bytes)一条访问轨迹里有多少比例未命中?Cache
  • study(bars)SPY 收盘价用行布局要多花多少?miss_ratestrided_trace

要点

  • 内存以行为单位流动。 一行的第一次读未命中,这一行的其余部分随后免费,所以邻居很便宜。
  • 步长决定缺失率。 对新数据,缺失率是 min(1, 步长 × 每个值的字节数 ÷ 行大小),一直涨到每次读都打开新行为止。
  • 布局就是步长。 按行存表,读一列就是步长 5;按列存,就是步长 1。
快速检查
一列 4 字节的浮点数按顺序读取,行大小 64 字节。多大比例的读取会未命中?
请选择一个答案。

下一个主题:直接映射缓存有一个本讲提过的弱点,即共用槽位的行之间的冲突。在相联度与冲突缺失里,你给每个槽位多个位置,并看清为什么行间距为 4096 字节的表会毁掉一个缓存。

实验:评分内容

6 个任务 · 100 分
  • 地址所在的行10 分
  • 直接映射缓存25 分
  • 按顺序读10 分
  • 带步长地读10 分
  • 访问轨迹的缺失率20 分
  • 基于 SPY 表的研究25 分
资源管理器 · lab-cl-2
▾ lab-cl-2
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-2$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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