σμ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 学习路线›模块 2 · 布局与遍历›主题 2.1·~55分钟中级

结构体数组与数组结构体

路线进度模块 2/3 · 主题 1/2
2.1 结构体数组与数组结构体
—实验得分— / 100总 XP—连续天数—

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

  1. 01一张表,两种布局
  2. 02扫描一列
  3. 03读整行会让结果反过来
  4. 04让计数来选择
  5. 05你要实现什么

逐笔表在内存里可以有两种放法,哪种合适取决于你怎么读。对按行存放的表只扫一列,会加载你用到的内存的五倍;对分列存放的表去读整行,则在反方向上浪费同样多。本讲数出两种布局的缺失数,让数字来做选择。

01

一张表,两种布局

内存是一长串字节,所以二维的表必须被摊平。有两种自然的方式。在结构体数组里,一天的五个值(开、高、低、收、成交量)放在一起,接着是下一天的五个值,依此类推。在数组结构体里,先是所有的开盘价,再是所有的最高价,依此类推。值和程序都没变,变的是地址。

一个值的地址由它的行和列决定。在 AoS 里,值的编号是“行 × 列数 + 列”;在 SoA 里是“列 × 行数 + 行”。float64 再乘以 8 字节。下面的代码打印一张 100 天、5 列的表中前四天收盘价的地址。

main.pyPython
def address(layout, row, col, n_rows, n_cols, elem_bytes=8):    if layout == "aos":        return (row * n_cols + col) * elem_bytes1    return (col * n_rows + row) * elem_bytesclose = 3print("aos:", [address("aos", row, close, 100, 5) for row in range(4)])print("soa:", [address("soa", row, close, 100, 5) for row in range(4)])
aos: [24, 64, 104, 144]
soa: [2400, 2408, 2416, 2424]
  1. 1在 AoS 里下一行在 n_cols 个值之后,所以相邻两个收盘价相距 5 个值(40 字节)。

AoS 里相邻天的收盘价相距 40 字节;SoA 里只相距 8 字节,一个紧挨着一个,正是本技能第一个主题里的“按顺序读”。

02

扫描一列

对收盘价求平均,就是读表的一列。我们把这次读取变成地址列表,送进前面几个主题里搭好的缓存,数 1,000 天的缺失。最后打印的数是有效比例。

main.pyPython
def scan_column(layout, col, n_rows, n_cols):    return [address(layout, row, col, n_rows, n_cols) for row in range(n_rows)]for layout in ("aos", "soa"):    misses = count_misses(scan_column(layout, 3, 1000, 5))    print(layout, "misses:", misses, "- useful share:", round(1000 * 8 / (misses * 64), 3))
aos misses: 625 - useful share: 0.2
soa misses: 125 - useful share: 1.0
useful share = value bytes ÷ row bytes (while a row fits in one line)(1)AoS 中扫一列会加载整行:8 ÷ 40 = 20%;SoA 中加载的每个字节都是收盘价:约 100%

SoA 的扫描在 1,000 个值上缺失 125 次,每八个值一次,毫无浪费。AoS 的扫描缺失 625 次,多了五倍,加载的字节里只有 20% 是收盘价。其余 80% 是平均值根本不看的开、高、低和成交量,它们还会把有用的行挤出缓存。

03

读整行会让结果反过来

很多逐笔程序问的是另一个问题:对这一笔,把价格、数量和方向一起给我。这会读一行的全部值。在 AoS 里整行是一块 40 字节,落在一两条缓存行里;在 SoA 里这五个值在五个不同的数组中,所以每一行要打开五条缓存行。代码每七行读一行,并数出缺失。

main.pyPython
def scan_rows(layout, rows, n_rows, n_cols):    return [address(layout, row, col, n_rows, n_cols) for row in rows for col in range(n_cols)]rows = list(range(0, 1000, 7))for layout in ("aos", "soa"):    print(layout, "misses:", count_misses(scan_rows(layout, rows, 1000, 5)))
aos misses: 214
soa misses: 625

这回 AoS 以三倍的优势获胜:缺失 214 次对 625 次。扫描一列时差五倍的布局,在读整行时好三倍。没有哪种布局始终最好,各有各擅长的读法。

你其实已经在用这两种布局

NumPy 的二维数组按默认顺序就是 AoS 布局:每一行是连续的。pandas 的 DataFrame 把同类型的列放在一起,接近 SoA,这也是它按列运算很快的原因之一。

04

让计数来选择

真实的程序两种读法都有,所以选择取决于两者的比例。对每种布局把整个负载的缺失加起来:扫列次数 × 列缺失 + 读行次数 × 行缺失。总数小的布局获胜,平局给 AoS。下面的代码在同一张表上试了四种比例。

main.pyPython
def compare(n_rows, n_cols, col, rows):    out = {}    for layout in ("aos", "soa"):        out[layout] = {            "column": count_misses(scan_column(layout, col, n_rows, n_cols)),            "rows": count_misses(scan_rows(layout, rows, n_rows, n_cols)),        }    return outdef best_layout(n_column_scans, n_row_reads, n_rows, n_cols, col, rows):    counts = compare(n_rows, n_cols, col, rows)    totals = {layout: n_column_scans * counts[layout]["column"] + n_row_reads * counts[layout]["rows"] for layout in counts}    return "soa" if totals["soa"] < totals["aos"] else "aos"rows = list(range(0, 1000, 7))for scans, reads in ((1, 4), (1, 2), (1, 1), (4, 1)):    print(scans, "column scans per", reads, "row reads:", best_layout(scans, reads, 1000, 5, 3, rows))
1 column scans per 4 row reads: aos
1 column scans per 2 row reads: aos
1 column scans per 1 row reads: soa
4 column scans per 1 row reads: soa
按访问模式选择,不要凭习惯

每两次读行才有一次扫列时,AoS 赢;每次读行就有一次扫列时,已经是 SoA 赢。盈亏平衡点大约在每次读行配半次扫列附近,你的真实负载决定你站在哪一边。重排一张表之前,先数清你的程序实际做哪些读取。

真实数据怎么说SPY 日线,5,405 行 × 5 列,32 KB 8 路缓存
扫描收盘价:SoA677加载字节的 99.8% 被用到
扫描收盘价:AoS3,378加载字节的 20% 被用到
读取 772 行:AoS1,159每七行读一行
读取 772 行:SoA3,383每行要打开五条缓存行
按布局的收盘价扫描与读行缺失数
列,SoA677
列,AoS3378
行,AoS1159
行,SoA3383
05

你要实现什么

实验沿着本讲走:两种布局、两种读法、比较、按负载选择、有效比例,以及对真实 SPY 表的研究。

  • 函数 · 问题依赖
  • address(layout, row, col, n_rows, n_cols)这个值在哪里?简单算术
  • scan_column(layout, col, n_rows, n_cols)扫一列会读哪些地址?address
  • scan_rows(layout, rows, n_rows, n_cols)读整行会碰到哪些地址?address
  • compare(n_rows, n_cols, col, rows)每种读法在每种布局下缺失多少次?scan_columnscan_rows
  • best_layout(...)这个负载适合哪种布局?compare
  • useful_fraction(layout, col, n_rows, n_cols)加载的内容里用到了多少?scan_column
  • study(bars)SPY 表适合哪种布局?comparebest_layout

要点

  • 布局就是一组步长。 AoS 让扫列的步长等于行宽,SoA 让它等于一个值。
  • 数一数有用的字节。 对 5 列的 AoS 表扫一列,只用到所加载内容的 20%,其余部分还会把有用的行挤出去。
  • 按负载选择。 把每种读法的次数乘上它在各布局下的缺失数,取总数小的;平衡点大约是每次读行配半次扫列。
快速检查
一张表有 4 列、每个值 8 字节,按 AoS 存放。扫描一列会加载 32 字节的行,每条 64 字节缓存行装两行。加载的字节里有多少比例被用到?
请选择一个答案。

下一个主题:布局决定了一张表的步长。在遍历顺序与循环分块里,表变成矩阵,你要选循环的顺序,再把循环切成小块,让矩阵的一小块在被使用期间一直留在缓存里。

实验:评分内容

7 个任务 · 100 分
  • 一个值的地址15 分
  • 扫描一列10 分
  • 读取整行10 分
  • 两种读法的缺失20 分
  • 适合负载的布局15 分
  • 有效比例15 分
  • 基于 SPY 表的研究15 分
资源管理器 · lab-cl-5
▾ lab-cl-5
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-5$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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