σμ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.3·~55 分上級

フォルスシェアリングとパディング

スキル上の位置モジュール 3/3 · トピック 3/7
3.3 フォルスシェアリングとパディング
—ラボスコア— / 100合計XP—連続日数—

読了 7分コード例 3件

  1. 01コアにはそれぞれ専用のキャッシュがある
  2. 02コアごとに1つのカウンタ
  3. 03パディングの量
  4. 04実際の日付で動くカウンタ
  5. 05これから作るもの

4つのスレッドがそれぞれ自分のカウンタを更新し、共有しているものは何もないのに、ループは本来より40倍遅くなります。4つのカウンタが同じキャッシュラインにあり、コアがそれを奪い合うからです。この講義ではキャッシュコヒーレンスの小さなモデルを作り、SPYの表の日付で動くループでフォルスシェアリングがどれだけ高くつくかを示し、数バイトのパディングで直します。

01

コアにはそれぞれ専用のキャッシュがある

ここまでは1つのプログラムが1つのコアで動いていました。実際のマシンには複数のコアがあり、それぞれに小さく速い専用キャッシュがあります。すると問題が起きます。コア0とコア1がともにあるラインのコピーを持ち、コア0がそれを変更したら、コア1は古い値を読み続けてはいけません。これを防ぐハードウェアの規則がキャッシュコヒーレンスです。最も簡単な実現方法は、コアがラインに書き込む前に、他のすべてのコアのそのラインのコピーを捨てることで、これが無効化です。

  1. 1
    自分のキャッシュを見るラインがなければ、このアクセスはミスです。
  2. 2
    書き込みなら他を追い出すラインを持つ他のコアは、それぞれコピーを失います。1コアにつき1回の無効化です。
  3. 3
    ラインを保持するその後、ラインはこのコアのキャッシュにあり、次のアクセスに備えます。

次のクラスは、コアごとに保持しているラインの集合を持ちます。3つのステップを再生します。コア0がバイト0に書き込み、コア1がバイト8(同じライン)に書き込み、再びコア0がバイト0に書き込みます。

main.pyPython
class CoherentCaches:    def __init__(self, n_cores):        self.lines = [set() for _ in range(n_cores)]        self.misses = 0        self.invalidations = 0    def access(self, core, address, write):        line = address // LINE_BYTES        hit = line in self.lines[core]        if not hit:            self.misses += 1        if write:            for other, held in enumerate(self.lines):1                if other != core and line in held:                    held.discard(line)                    self.invalidations += 1        self.lines[core].add(line)        return hitcaches = CoherentCaches(2)print(caches.access(0, 0, True), caches.access(1, 8, True), caches.access(0, 0, True))print("misses:", caches.misses, "invalidations:", caches.invalidations)
False False False
misses: 3 invalidations: 2
  1. 1書き込みは、そのラインを持つ他のコアからラインを取り除き、コピー1つにつき1回の無効化を数えます。

3回のアクセスはすべてミスです。コア1の書き込みでコア0のコピーが無効化され、次のコア0の書き込みで今度はコア1のコピーが無効化されます。コア0とコア1は同じバイトには一度も書いていません。隣り合うバイトだけなのに、書き込みのたびにラインを奪い合います。

定義フォルスシェアリング

2つのコアが、同じキャッシュラインにある別々の変数に書き込みます。プログラムは正しいのに、あたかも1つの変数を共有しているかのように、ラインがコア間を行き来します。

02

コアごとに1つのカウンタ

イベントを並列に数える一般的な方法は、各コアに専用のカウンタを持たせ、どのコアも他を待たないようにすることです。カウンタは1つ8バイトで、配列に並べると4つが64バイトの1ラインに入ります。50周分の更新(毎周コアごとに1回の書き込み)を作り、カウンタの間隔を8バイト(詰めた場合)と64バイト(パディングした場合)にして再生します。時間は、コア自身のキャッシュでのアクセスが1 ns、他のコアからラインを取ってくるミスが60 nsです。

main.pyPython
def counter_address(core, stride_bytes):    return core * stride_bytesdef update_trace(n_cores, n_updates, stride_bytes):    return [(core, "w", counter_address(core, stride_bytes)) for _ in range(n_updates) for core in range(n_cores)]def run_trace(trace, n_cores):    caches = CoherentCaches(n_cores)    for core, op, address in trace:        caches.access(core, address, op == "w")    return {"accesses": len(trace), "misses": caches.misses, "invalidations": caches.invalidations}def time_ns(stats):    return (stats["accesses"] - stats["misses"]) * HIT_NS + stats["misses"] * MISS_NSfor name, stride in (("packed", 8), ("padded", 64)):    stats = run_trace(update_trace(4, 50, stride), 4)    print(name, stats, time_ns(stats), "ns")
packed {'accesses': 200, 'misses': 200, 'invalidations': 199} 12000.0 ns
padded {'accesses': 200, 'misses': 4, 'invalidations': 0} 436.0 ns

詰めた場合は、200回の書き込みがすべてミスし、そのうち199回が直前の書き手を無効化して12,000 nsです。パディングすると、各コアが自分のラインにカウンタを持ち、最初に4回のコールドミスがあるだけで、以降はすべての書き込みがヒットし、無効化はなく、436 nsです。算術はまったく同じなのに27倍速くなります。4つのコアがラインを奪い合わなくなったからです。

03

パディングの量

各カウンタに専用のラインを与えるには、カウンタ間の距離をライン長の倍数にする必要があるので、要素のサイズを次の64の倍数に切り上げます。8バイトのカウンタは64バイトを使い、56バイトが無駄になります。65バイトの要素は128バイトです。次の関数が切り上げを行います。

main.pyPython
def padding_needed(item_bytes):    return math.ceil(item_bytes / LINE_BYTES) * LINE_BYTESprint([padding_needed(size) for size in (8, 63, 64, 65, 100)])
[64, 64, 64, 128, 128]
実際のコードでの指定方法

C++ ではスレッドのカウンタを持つ構造体に alignas(64)(または std::hardware_destructive_interference_size)を付け、Rustでは #[repr(align(64))]、Javaでは @Contended を使います。PythonのNumPyなら、カウンタ配列を (n_cores, 8) にして列0だけを使い、各カウンタが新しい64バイトの行から始まるようにします。

コアが書き込むものだけをパディングする

読み出しだけでは何も無効化されないので、多くのコアが1つのラインをコストなしで読めます。パディングは8バイトのカウンタごとに56バイトを無駄にするので、読み取り専用のデータや数千個の要素には割に合いません。スレッドが頻繁に更新する、スレッドごとの少数のホットな変数だけに使いましょう。

04

実際の日付で動くカウンタ

ラボでは4つのコアがそれぞれ担当する取引日を数え、SPYの表のi番目の日は「曜日 % 4」のコアに割り当てられます。最初の1,000日が、日付順に書き込みを駆動します。日はカレンダーどおりに進むので、同じコアが続けて書くことはまれで、ラインは何度も奪われます。カウンタを詰めると1,000回の書き込みのうち818回がミスし、817回が他のコアを無効化します。パディングすると、最初の4回の書き込みだけがミスします。

実データが示すものSPYの1,000取引日、4コア、ヒット1 ns、ミス60 ns
1ラインに詰めたカウンタ49,262nsミス818回、無効化817回
64バイトにパディングしたカウンタ1,236nsコールドミス4回、無効化なし
パディングによる高速化39.9×同じ仕事で、1ラインずつ
その代償のメモリ56bytes8バイトのカウンタごとに無駄になる量
1,000回の更新の時間(マイクロ秒)
詰めた49.3
パディング1.2
05

これから作るもの

ラボでは、コヒーレンスのモデル、指定した間隔でのコアごとのカウンタのトレース、再生、パディングの規則、時間モデル、そしてSPYの日付での研究を作ります。

  • 関数 · 問い前提
  • CoherentCaches(n_cores).access(core, address, write)ヒットかミスか、誰がコピーを失うか?単純な算術
  • counter_address(core, stride_bytes)コアのカウンタはどこにあるか?単純な算術
  • update_trace(n_cores, n_updates, stride_bytes)コアはどんな書き込みをするか?counter_address
  • run_trace(trace, n_cores)ミスと無効化は何回か?CoherentCaches
  • padding_needed(item_bytes)要素はどれだけ離す必要があるか?単純な算術
  • time_ns(stats)実行にどれだけかかるか?ヒットとミスのコスト
  • study(bars)SPYの日付でフォルスシェアリングはどれだけ高くつくか?run_tracepadding_neededtime_ns

重要ポイント

  • 書き込みはラインを奪う。 書き込むために、コアは他のすべてのコアのラインのコピーを無効化し、他のコアは次に使うときにミスします。
  • 隣り合うものは運命を共にする。 同じ64バイトのラインにある変数は、コアにとって1つの単位です。フォルスシェアリングは並列コードを逐次コードより遅くします。
  • コアごとのホットなデータをパディングする。 各要素を64バイトに切り上げます。ここでは1カウンタあたり56バイトの無駄で40倍速くなります。読み取り専用のデータはパディングしません。
クイックチェック
2つのスレッドがそれぞれ自分の8バイトのカウンタを増やし、2つのカウンタは配列の隣同士にあります。何が起きますか。
答えを選んでください。

次のトピック:フォルスシェアリングは、同じソケットを共有するコアの話でした。2ソケットのマシンではメモリ自体が分かれ、あるコアに近い部分と遠い部分ができます。NUMAの影響では、ページをノードに配置し、ローカルとリモートのアクセスを数え、その差をコストで見積もります。

ラボ:採点される内容

7タスク · 100点
  • コヒーレンスのモデル30点
  • カウンタのアドレス5点
  • 更新の周回15点
  • 実行の再生15点
  • ラインを与えるパディング10点
  • 実行の時間10点
  • SPYの表での研究15点
エクスプローラー · lab-cl-9
▾ lab-cl-9
▾ data
…
bash · ブラウザ内
learner@quantcraft:~/lab-cl-9$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-cl-9 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ