σμ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.1·~40分钟入门

内存层次与延迟数字

路线进度模块 1/3 · 主题 1/4
1.1 内存层次与延迟数字
—实验得分— / 100总 XP—连续天数—

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

  1. 01为什么决定速度的是内存而不是算术
  2. 02一次访问的代价
  3. 03平均访问时间
  4. 04谁来回答,谁来花钱
  5. 05读一张真实的表
  6. 06你要实现什么

处理器把两个数相加只要不到一纳秒,但取一个不在任何缓存里的数却可能要80 ns。本讲把这些延迟数字合成一个数,也就是一次内存访问的平均代价,并指出是哪一级内存在吃掉你的时间。

01

为什么决定速度的是内存而不是算术

交易程序的大部分时间都在等数据:一个价格、一个订单、表里的一行。对这些数据做算术几乎是免费的,但数据得先从内存运到处理器,离得越远越慢。想想书桌、书架、图书馆的某一层和城另一头的仓库:桌上那几张纸瞬间就到,书架要几秒,图书馆要一分钟,仓库要一下午。计算机把最常用的数据复制到处理器旁边小而快的存储里,叫作缓存,这样大多数请求在“书桌”上就能得到回答。

定义延迟

从请求一个值到拿到它之间的时间,这里以纳秒(十亿分之一秒)为单位。

典型的服务器核心在主内存前有三级缓存。我们把它们叫作 L1、L2、L3,把主内存叫作 RAM。被某级缓存回答的请求是一次命中,必须继续去下一级的请求是一次缺失。下面的代码把四个延迟并排放在一起。

main.pyPython
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}for level, ns in LATENCY_NS.items():    print(level, ns, "ns", ns / LATENCY_NS["L1"], "x L1")
L1 1.0 ns 1.0 x L1
L2 4.0 ns 4.0 x L1
L3 15.0 ns 15.0 x L1
RAM 80.0 ns 80.0 x L1

每一级都比上一级慢好几倍,RAM 比 L1 慢80 倍。整个技能就是要保证大多数访问在靠上的层级就得到回答。

02

一次访问的代价

访问从 L1 开始。值在那里,就只花 L1 的延迟,访问结束。不在,处理器就依次问 L2、L3,再到总有这个值的 RAM。问过的每一级不管命不命中都要花时间,所以在 L3 找到值的访问已经付了 L1、L2、L3 三级的钱。

  1. 1
    先问 L1花 1 ns。命中就结束。
  2. 2
    未命中再问 L2再花 4 ns。命中就结束。
  3. 3
    未命中再问 L3再花 15 ns。命中就结束。
  4. 4
    未命中就去 RAM再花 80 ns。RAM 总是有这个值。
main.pyPython
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}cost_found_in_l3 = LATENCY_NS["L1"] + LATENCY_NS["L2"] + LATENCY_NS["L3"]print(cost_found_in_l3, "ns")
20.0 ns

由 L3 回答的访问花 20 ns,不是 15:前两次未命中花掉的 1 ns 和 4 ns 也要算进账里。

命中率是局部的

每个命中率只统计到达该级的访问。L2 命中率 0.8 指的是在 L1 未命中的访问中有 80% 命中,而不是所有访问的 80%。把两者搞混是本主题最常见的错误。

03

平均访问时间

没有哪一次访问是“典型”的,所以我们求许多次访问的平均。按每条路径出现的频率给它的代价加权:每次访问都付 L1 的延迟,只有在 L1 未命中的那部分才付 L2,两级都未命中的那部分才付 L3,依此类推。写成一个表达式,括号像层级一样层层嵌套:

AMAT = t₁ + (1 − h₁) · (t₂ + (1 − h₂) · (t₃ + (1 − h₃) · t_RAM))(1)t₁、t₂、t₃、t_RAM 是各级延迟,h₁、h₂、h₃ 是 L1、L2、L3 的局部命中率

这就是平均内存访问时间。括号要从里往外读:最里面一层是在 L3 未命中的代价,往外一层加上访问 L3 的时间,一直到每次访问都要付的 L1。

main.pyPython
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}h1, h2, h3 = 0.9, 0.8, 0.7t = LATENCY_NSamat = t["L1"] + (1 - h1) * (t["L2"] + (1 - h2) * (t["L3"] + (1 - h3) * t["RAM"]))1print(round(amat, 2), "ns")
2.18 ns
  1. 11 - h 是未命中的访问所占比例,所以每一层括号都乘上能走到这一步的比例。

即使 90% 的访问在 L1 命中,平均仍是 2.18 ns,超过 L1 延迟的两倍。少数走到深处的访问太贵,把平均值拉了上去。所以一个看起来很棒的命中率,背后仍可能是一个慢程序。

周期还是纳秒?

芯片厂商用处理器周期给出延迟:4 GHz 下 4 个周期就是 1 ns。这里用纳秒,是为了让各级一眼就能比较。这些数字是典型值而非精确值,真实芯片上下相差一倍左右。

04

谁来回答,谁来花钱

有两个看似相同、答案却不同的问题:哪一级回答了这些访问?哪一级占用了时间?前者数访问,后者数等待。每一级只会被一部分访问到达,每次到达都要付它的延迟,所以一个很少被到达但很慢的层级,仍然可能主宰等待时间。

main.pyPython
h1, h2, h3 = 0.9, 0.8, 0.7served = [h1, (1 - h1) * h2, (1 - h1) * (1 - h2) * h3, (1 - h1) * (1 - h2) * (1 - h3)]reach = [1.0, 1 - h1, (1 - h1) * (1 - h2), (1 - h1) * (1 - h2) * (1 - h3)]1cost = [r * ns for r, ns in zip(reach, [1.0, 4.0, 15.0, 80.0])]2print([round(x, 3) for x in served])print([round(c / sum(cost), 3) for c in cost])
[0.9, 0.08, 0.014, 0.006]
[0.459, 0.183, 0.138, 0.22]
  1. 1reach 是访问到每一级的比例:所有访问都会到 L1。
  2. 2sum(cost) 就是平均访问时间:和前面一样是 2.18 ns。

RAM 只回答 0.6% 的访问,却占了 22% 的等待时间,而 L1 回答了 90% 的访问,只占 46% 的时间。这是本技能里所有优化背后的关键事实:不要数访问次数,要数时间花在了哪里。

05

读一张真实的表

这对一张真实的表有多重要?实验里 SPY 日线表有 5,405 行、五列(开、高、低、收、成交量),共 27,025 个值。把每个值读一遍,分三种条件:几乎全部命中的热缓存、温缓存,以及每一级都有一半访问未命中的冷缓存。

真实数据怎么说SPY 日线,2005 至 2026 年
表中的值27,0255,405 行 × 5 列
冷缓存对热缓存15.9×同一张表慢这么多倍
平均访问,热缓存1.05ns命中率 0.99 · 0.95 · 0.90
平均访问,冷缓存16.75ns这段等待的 60% 来自 RAM
把表读一遍所需的时间(微秒)
热缓存28
温缓存59
冷缓存453
06

你要实现什么

实验把这一讲变成六个小函数,每个都建立在前面的基础上,最后估算读取真实 SPY 表的代价。下面列出它们,以及各自回答的问题。

  • 函数 · 问题依赖
  • average_access_time(hit_rates)一次访问平均花多少?延迟表
  • slowdown(hit_rates)比总在 L1 命中慢多少倍?average_access_time
  • levels_served(hit_rates)哪一级回答了访问?局部命中率
  • time_shares(hit_rates)哪一级占用了时间?average_access_time
  • scan_time_us(n_accesses, hit_rates)n 次访问要多久?average_access_time
  • study(bars)读完整张 SPY 表要花多少?scan_time_ustime_shares

要点

  • 数时间花在哪里。 一级回答的访问占比和它花掉的时间占比是两个不同的数,稀少但很慢的一级也可能主宰时间。
  • 命中率是局部的。 每一级的命中率只统计到达它的访问,所以一级回答的占比是上面各级未命中率之积再乘它自己的命中率。
  • 平均值是一层层嵌套的括号。 AMAT = t₁ + 缺失₁ × (t₂ + 缺失₂ × (…)) 就是后面每个主题都想降低的那个数。
快速检查
L1 命中率为 0.95。到达 L2 的访问有一半命中。L2 回答了全部访问的多大比例?
请选择一个答案。

下一个主题:本讲把命中率当作已知量。在缓存行与空间局部性里,你要自己搭出产生这些命中率的缓存,并弄清为什么读紧挨着刚读过的值几乎不要钱。

实验:评分内容

6 个任务 · 100 分
  • 平均访问时间20 分
  • 相对 L1 的减速10 分
  • 谁回答了访问15 分
  • 谁占用了时间20 分
  • n 次访问的时间15 分
  • 基于 SPY 表的研究20 分
资源管理器 · lab-cl-1
▾ lab-cl-1
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-1$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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