σμ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—連続日数—

読了 6分コード例 4件

  1. 011つの表、2つのレイアウト
  2. 021列の走査
  3. 03行全体を読むと結果が逆になる
  4. 04数に選ばせる
  5. 05これから作るもの

ティック表はメモリ上で2通りに置けて、どちらが正しいかは読み方で決まります。行ごとに置いた表で1列だけを走査すると、使う量の5倍のメモリを読み込みます。列ごとに分けた表で行全体を読むのも、逆向きに同じだけ無駄です。この講義では両方のレイアウトのミスを数え、その数に選ばせます。

01

1つの表、2つのレイアウト

メモリは長いバイト列なので、2次元の表は平らに並べなければなりません。自然な方法が2つあります。構造体の配列では、1日の5つの値(始値、高値、安値、終値、出来高)をまとめて置き、次の日の5つ、というように続けます。配列の構造体では、まず始値がすべて、次に高値がすべて、というように並べます。値もプログラムも同じですが、アドレスが変わります。

値のアドレスは、行と列から決まります。AoSでは値の番号は「行 × 列数 + 列」、SoAでは「列 × 行数 + 行」です。float64なら8バイトを掛けます。次のコードは、100日×5列の表の最初の4日の終値のアドレスを表示します。

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. 1AoSでは次の行はn_cols個先なので、連続する終値は5個(40バイト)離れます。

AoSでは連続する日の終値は40バイト離れ、SoAでは8バイト、つまり隣同士で並びます。これは、このスキルの最初のトピックで見た「順に読む」ケースそのものです。

02

1列の走査

終値の平均を取ると、表の1列を読みます。この読み出しをアドレスのリストにし、前のトピックで作ったキャッシュに通して、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回ミスし、8個に1回で無駄がありません。AoSの走査は625回、つまり5倍ミスし、読み込んだバイトのうち終値は20%だけです。残り80%は平均が見向きもしない始値・高値・安値・出来高で、有用なラインをキャッシュから押し出します。

03

行全体を読むと結果が逆になる

多くのティック処理は別の問いを立てます。このティックの価格、数量、売買方向をまとめて欲しい、というものです。これは行の全部の値を読みます。AoSでは行全体が40バイトのひとかたまりで、1〜2ラインに収まります。SoAでは5つの値が5つの別々の配列にあるので、1行ごとに5ラインを開きます。コードは7行に1行を読み、ミスを数えます。

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が3倍有利で、ミスは214回対625回です。列の走査では5倍悪かったレイアウトが、行の読み出しでは3倍よくなります。どちらかが常に最良なのではなく、それぞれが1つの読み方に最適なのです。

すでに両方のレイアウトを使っている

NumPyの2次元配列は既定の順序ではAoSのレイアウトで、各行が連続しています。pandasのDataFrameは同じ型の列をまとめて保持し、これはSoAに近く、列単位の演算が速い理由の1つです。

04

数に選ばせる

実際のプログラムは両方の読み方をするので、選択は割合で決まります。各レイアウトについて、負荷のミスを合計します。列の走査回数×列のミス+行の読み出し回数×行のミスです。合計が小さいレイアウトが勝ち、同点ならAoSです。次のコードは同じ表で4つの割合を試します。

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
習慣ではなくアクセスパターンで選ぶ

列の走査が行の読み出し2回に1回ならAoS、行の読み出し1回に1回ならもうSoAが勝ちます。損益分岐は行の読み出し1回あたり走査0.5回付近で、実際の負荷がどちら側かを決めます。表を組み替える前に、プログラムが実際に行う読み出しを数えましょう。

実データが示すものSPY日足、5,405行×5列、32 KBの8ウェイキャッシュ
終値の走査:SoA677読み込みバイトの99.8%を使用
終値の走査:AoS3,378読み込みバイトの20%を使用
772行の読み出し:AoS1,1597行に1行
772行の読み出し:SoA3,3831行につき5ラインを開く
終値の走査と行の読み出しのミス数(レイアウト別)
列・SoA677
列・AoS3378
行・AoS1159
行・SoA3383
05

これから作るもの

ラボは講義の順に進みます。2つのレイアウト、2種類の読み出し、比較、負荷に応じた選択、有効率、そして実際の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では値1個分になります。
  • 有効なバイトを数える。 5列のAoS表の列の走査は、読み込んだ量の20%しか使わず、残りは有用なラインを押し出します。
  • 負荷で選ぶ。 各読み出しの回数に各レイアウトでのミス数を掛けて合計の小さいほうを選びます。損益分岐は行の読み出し1回あたり走査約0.5回でした。
クイックチェック
8バイト値の4列の表をAoSで保存しています。1列の走査は32バイトの行を、64バイトのラインあたり2行ずつ読み込みます。読み込んだバイトのうち使われる割合は?
答えを選んでください。

次のトピック:レイアウトは1つの表のストライドを決めました。走査順序とループタイリングでは、表は行列になり、ループの順序を選び、さらにループをタイルに分けて、行列の一区画を使っている間キャッシュに残るようにします。

ラボ:採点される内容

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と連続日数
  • 講義の進捗とメモ