σμ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.4·~1小时高级

NUMA 效应

路线进度模块 3/3 · 主题 4/7
3.4 NUMA 效应
—实验得分— / 100总 XP—连续天数—

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

  1. 01有距离的内存
  2. 02谁决定页放在哪里
  3. 03统计本地与远端访问
  4. 04你要实现什么

在双路服务器上,同一次读取可能要 80 ns 也可能要 130 ns,取决于页放在哪个插槽上。页放在哪里,是在它第一次被触碰时决定的。本讲为两个节点建模,说明一个线程独自初始化数据会让一半的工作变成远端访问,并让每个线程先触碰自己的那部分来解决它。

01

有距离的内存

大型服务器有两个处理器插槽,每个插槽有自己的内存条。核心直接读自己插槽上的内存;要读另一个插槽的内存,请求得穿过插槽之间的链路,花的时间更长。这样的机器叫NUMA。每个插槽连同它的内存构成一个节点,线程所在的节点就是它的核心所在的位置。在我们的模型里有两个节点,每个节点两个线程:线程 0 和 1 在节点 0,线程 2 和 3 在节点 1。本地访问 80 ns,远端访问 130 ns。

main.pyPython
print([node_of_thread(thread, 2) for thread in range(4)])for thread_node in (0, 1):    print("thread on node", thread_node, [LOCAL_NS if page_node == thread_node else REMOTE_NS for page_node in (0, 1)])
[0, 0, 1, 1]
thread on node 0 [80.0, 130.0]
thread on node 1 [130.0, 80.0]

每一行是一个线程的视角:自己节点上的页要付 80 ns,另一个节点上的页要付 130 ns。差的 50 ns 看似不多,但每一次到达内存的访问都要付这个差价。

02

谁决定页放在哪里

操作系统以页为单位给程序分配内存,并在页第一次被使用的时刻决定它放在哪个节点。默认规则是首次触碰:页被放到第一个触碰它的线程所在的节点上,并一直留在那里。另一种办法是把页在各节点间交错放置。下面的代码把这两种规则用在一小串写成(线程,地址)的访问上。

main.pyPython
def first_touch_placement(trace, threads_per_node):    placement = {}    for thread, address in trace:        page = address // PAGE_BYTES        if page not in placement:            placement[page] = node_of_thread(thread, threads_per_node)    return placementdef interleaved_placement(trace, n_nodes):    return {page: page % n_nodes for page in {address // PAGE_BYTES for _, address in trace}}trace = [(0, 0), (0, 4096), (3, 8192), (1, 4100)]print(first_touch_placement(trace, 2))print(interleaved_placement(trace, 2))
{0: 0, 1: 0, 2: 1}
{0: 0, 1: 1, 2: 0}

第 0 页和第 1 页最先被位于节点 0 的线程 0 触碰,所以放在节点 0;第 2 页最先被节点 1 上的线程 3 触碰,所以放在节点 1。线程 1 之后对第 1 页的访问不会移动它。交错放置则不管谁碰了什么:第 1 页放在节点 1,仅仅因为 1 对 2 取模等于 1。

03

统计本地与远端访问

典型的并行任务把一张表切成块,每个线程一块。之后每个线程只读自己的那一块,理想情况下它读的每一页都在自己的节点上。下面的函数为 400 条记录、四个线程构造这样的访问模式,并统计三种放置方式下本地与远端访问的数量和耗时。第一种“single”:由一个线程初始化了全部数据,所以首次触碰把每一页都放到了节点 0。第二种:由各自的负责线程初始化它们自己的块。第三种:页交错放置。

main.pyPython
def access_costs(trace, placement, threads_per_node):    local = 0    remote = 0    for thread, address in trace:        if placement[address // PAGE_BYTES] == node_of_thread(thread, threads_per_node):            local += 1        else:            remote += 1    return {"local": local, "remote": remote, "ns": local * LOCAL_NS + remote * REMOTE_NS}def partition_trace(n_records, n_threads, record_bytes):    chunk = n_records // n_threads    return [(thread, record * record_bytes) for thread in range(n_threads) for record in range(thread * chunk, (thread + 1) * chunk)]work = partition_trace(400, 4, 512)single = first_touch_placement([(0, address) for _, address in work], 2)owners = first_touch_placement(work, 2)spread = interleaved_placement(work, 2)for name, placement in (("single", single), ("owners", owners), ("interleaved", spread)):    print(name, access_costs(work, placement, 2))
single {'local': 200, 'remote': 200, 'ns': 42000.0}
owners {'local': 400, 'remote': 0, 'ns': 32000.0}
interleaved {'local': 208, 'remote': 192, 'ns': 41600.0}
average access time = local share × 80 ns + remote share × 130 ns(1)一半访问在远端时:0.5 × 80 + 0.5 × 130 = 105 ns,而不是 80 ns

只由一个线程初始化全部数据时,一半访问在远端,400 次读取要 42,000 ns。由各自负责的线程初始化自己的块时,没有任何远端访问,只要 32,000 ns,同样的程序快了大约 1.3 倍。交错放置在这里帮不上忙:每个线程读的是一整块连续数据,交错的页会把它一半的数据放到错误的节点上。交错放置是给那种每个线程到处都读的数据用的,这时没有哪种放置能对所有线程都好。

只在一个线程里初始化是常见错误

如果主线程在启动工作线程之前就分配并填好一个大数组,每一页的位置就都由它自己所在的节点决定了。程序是对的,分析器也看不出毛病,只是慢,而且插槽越多越慢。应该让每个工作线程自己初始化它要使用的那一部分。

控制放置方式的工具

在 Linux 上,numactl --interleave=all 把程序的页分散到各节点,numactl --cpunodebind 和 --membind 把线程和内存绑定到节点,numastat 显示有多少访问是本地的。交易系统通常把每个线程绑定到一个核心,并给它自己节点上的内存。

真实数据怎么说4,000 条 512 字节的 SPY 记录(500 页),2 个节点上 4 个线程,本地 80 ns,远端 130 ns
由一个线程初始化420,000ns4,000 次访问中 2,000 次在远端
由各自的负责线程初始化320,000ns所有访问都在本地
交错放置的页420,000ns一半数据在错误的节点上
由负责线程首次触碰带来的收益1.31×同样的程序,同样的数据
4,000 次读取的时间(微秒)
单线程初始化420
负责线程320
交错420
04

你要实现什么

实验沿着本讲走:线程所在的节点、两种放置规则、分块作业的访问模式、本地与远端访问的统计,以及对 SPY 记录的研究。

  • 函数 · 问题依赖
  • node_of_thread(thread, threads_per_node)线程运行在哪个节点?整数除法
  • first_touch_placement(trace, threads_per_node)首次触碰把每一页放在哪里?node_of_thread
  • interleaved_placement(trace, n_nodes)交错放置呢?简单算术
  • partition_trace(n_records, n_threads, record_bytes)每个线程做哪些访问?简单算术
  • access_costs(trace, placement, threads_per_node)本地和远端访问各多少次,耗时多久?node_of_thread
  • study(bars)放置方式不当,在 SPY 记录上要付出多少代价?partition_tracefirst_touch_placementaccess_costs

要点

  • 内存有距离。 在多插槽服务器上,另一个节点的页要 130 ns 而不是 80 ns,所以放置方式也是性能的一部分。
  • 首次触碰说了算。 页放在第一个触碰它的线程所在的地方;主线程把一切填好,就让一半的工作落在远端。
  • 让数据与读它的线程匹配。 让每个工作线程初始化它要读的那一块:这里快了 1.3 倍;只对所有线程都到处读的数据使用交错放置。
快速检查
四个线程各自只读一张表的四分之一。哪种放置方式最好?
请选择一个答案。

下一个主题:字节放在哪里只是故事的一半,它们怎么被分发出去同样重要。在分配器与内存池里,程序会请求很多小对象,分配器发放它们的顺序和位置,决定了之后的遍历要碰到多少条缓存行。

实验:评分内容

6 个任务 · 100 分
  • 线程所在的节点5 分
  • 首次触碰放置20 分
  • 交错放置10 分
  • 分块作业15 分
  • 本地与远端访问25 分
  • 基于 SPY 表的研究25 分
资源管理器 · lab-cl-10
▾ lab-cl-10
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-cl-10$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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