foundations · Stage 1
CPU・メモリ経路とアクセス局所性
キャッシュ、TLB、主記憶までの経路を観測し、データ配置から性能を判断する。
到達目標
仮想アドレスからTLB、キャッシュ階層、主記憶までの経路と各段の役割を図示できる
- アクセス順序別の測定値、CPU・メモリ経路図、実行環境を含む記録
- 局所性が実効帯域へ作用する機構を示す5分説明
同一データ量で連続、stride、ランダムの三アクセスを測定し、帯域または所要時間の差を報告できる
- アクセス順序別の測定値、CPU・メモリ経路図、実行環境を含む記録
- キャッシュ、TLB、割り当て、処理系の競合仮説を反証する診断
キャッシュミスとTLBミスの仮説を追加測定で切り分け、別処理系のデータ配置へ適用できる
- キャッシュ、TLB、割り当て、処理系の競合仮説を反証する診断
- 列指向または画像処理へデータ指向設計を移した設計案
能力の進行
recognize
キャッシュライン、アクセス局所性、TLB、ページを異なる階層の概念として識別できる
証拠: アクセス順序別の測定値、CPU・メモリ経路図、実行環境を含む記録
explain
時間的局所性と空間的局所性が再利用と転送量を通じて性能へ効く経路を説明できる
証拠: 局所性が実効帯域へ作用する機構を示す5分説明
apply
データ量を固定してアクセス順序だけを変え、環境付きの再現可能な測定を作成できる
証拠: アクセス順序別の測定値、CPU・メモリ経路図、実行環境を含む記録
diagnose
作業集合、stride、ページ数を独立に変え、キャッシュとTLBの影響を切り分けられる
証拠: キャッシュ、TLB、割り当て、処理系の競合仮説を反証する診断
lead
可読性、更新頻度、ベクトル化、メモリ量を含むデータ配置レビューを別処理系で主導できる
証拠: 列指向または画像処理へデータ指向設計を移した設計案
なぜ重要か
CPUが命令を実行する速さと、必要なデータが届く速さは同じではない。計算量が同じ二つの実装でも、データの並びと走査順序によって実効帯域が大きく変わる。大きな配列をランダムにたどる処理は、演算よりもキャッシュライン転送やアドレス変換待ちに支配され得る。
ここでいうキャッシュは、近くまたは最近使ったデータの複製をCPU近傍へ保持する階層である。TLBはTranslation Lookaside Bufferの略で、仮想ページから物理ページへの変換結果を保持する。似た性能症状を起こすが、同じ機構ではない。誤診すると、配列を小さくすべき場面で巨大ページを試すなど、原因とずれた最適化になる。
メンタルモデル
ロード命令がデータへ到達するまでを「変換」と「転送」に分ける。次の図は診断用の論理段階であり、普遍的な逐次時系列ではない。たとえばVIPT方式では、TLBのアドレス変換とL1 cacheのindex lookupが並行し得る。物理tagの照合を含む具体的な重なり方は実装依存なので、実機資料とcounterで仮説を絞る。
データ構造の選択では、オブジェクト数だけでなく、hot pathで同時に読むfieldを考える。Array of Structuresは一要素の全fieldを同時に使う処理に向き、Structure of Arraysは同じfieldを多数要素から読む処理に向き得る。ただし更新の一貫性と可読性の費用も比較する。
注記
図を読む際の補足情報です。
- 命令: 仮想アドレスAの値を要求する。
- TLB: Aのページ変換を検索する。missならページテーブルwalkが必要になる。
- L1 cache: Aを含むcache lineを検索する。hitなら近い階層で返る。
- L2と最終レベルcache: L1 miss後の候補を調べる。L2やLLCがcore-privateか複数coreでsharedかというprivate/shared範囲は機種依存である。
- memory controller: 全cacheでmissなら主記憶からline単位で転送する。
- 再利用: 同じline内の次要素を使えば空間的局所性、短時間に同じ値を使えば時間的局所性を得る。
- このDAGはhit/missの診断依存を示し、VIPTではTLB変換とL1 index lookupが並行し得るため普遍的な逐次latencyではない。
一つのloadでaddress translationとdata transferの待ちをどう切り分けるか。
- 命令
- 仮想アドレスAの値を要求する。
- group
- request
- TLB
- Aのページ変換を検索し、missならpage table walkを開始する。
- group
- translation
- page table walk
- TLB miss branchだけで仮想pageから物理pageへの変換情報を取得する。
- group
- translation
- 物理address ready
- TLB hitまたはpage table walk完了が合流し、物理tag照合へ必要なaddressを渡す。
- group
- translation
- L1 cache
- VIPTでは仮想addressのindex lookupがTLBと並行し得るが、物理tag照合後にhit/missを判断する。
- group
- transfer
- L2と最終レベルcache
- private/shared範囲と段数は機種依存で、固定latencyではない。
- group
- transfer
- memory controller
- 全cache miss時に主記憶からline単位で転送する。
- group
- transfer
- 値を命令へreturn
- L1 hit、lower cache hit、またはmemory転送完了が共通returnへ合流する。
- group
- return
- 再利用
- return後、空間的局所性または時間的局所性で後続accessのhitを増やす。
- group
- reuse
- 命令 → TLB: 仮想アドレスAの変換を検索 (translation-request)
- 命令 → L1 cache: VIPT index lookupは変換と並行し得る (vipt-parallel-index)
- TLB → 物理address ready: TLB hitなら保持した変換を使う (tlb-hit)
- TLB → page table walk: TLB miss時だけpage table walk (tlb-miss)
- page table walk → 物理address ready: walk完了で物理addressを得る (translation-result)
- 物理address ready → L1 cache: 物理tag照合でdata hit/missを確定 (tag-check)
- L1 cache → 値を命令へreturn: L1 hitなら値を返す (l1-hit)
- L1 cache → L2と最終レベルcache: L1 miss時だけ下位cacheへ (l1-miss)
- L2と最終レベルcache → 値を命令へreturn: lower cache hitなら値を返す (lower-hit)
- L2と最終レベルcache → memory controller: 全cache miss時だけ主記憶へ (lower-miss)
- memory controller → 値を命令へreturn: line転送完了後に値を返す (memory-return)
- 値を命令へreturn → 再利用: 返却後のlineを後続accessで再利用 (reuse)
TLB・page tableの変換経路とcache・主記憶の転送経路を区別し、機種依存の階層を普遍的latencyとして扱わない。
| パラメータ | 選択肢 | 既定値 |
|---|---|---|
| 作業集合 | cache内候補、cache超過候補 | small |
| access順 | 連続、固定seedランダム | sequential |
- 固定sample traceを開始: 説明用の固定sample traceでtranslationとtransferを独立に追う。所要時間の観測値だけでは原因を証明できない。; 条件 常時; node
instruction、tlb、l1-cache; edgeinstruction-to-tlb、instruction-to-l1 - 小作業集合・連続access候補: L1 hitは原因候補であり断定ではない。hardware counter等でtranslation hitとdata hitを別々に確認する。; 条件
access-order=sequential、working-set=small; nodeaddress-ready、l1-cache、return、reuse; edgeaddress-to-l1、l1-hit-return、return-to-reuse - 小作業集合・ランダムaccess候補: lower cache利用は原因候補。観測値だけでは証明できないためhardware counterと独立変数で反証する。; 条件
access-order=random、working-set=small; nodeaddress-ready、l1-cache、lower-cache、return; edgeaddress-to-l1、l1-miss-lower、lower-hit-return - 大作業集合・連続access候補: page walkとmemory transferは固定sample traceの候補経路。translationとtransferを独立観測する。; 条件
access-order=sequential、working-set=large; nodepage-table、address-ready、lower-cache、memory-controller、return; edgewalk-complete、lower-miss-memory、memory-return - 大作業集合・ランダムaccess候補: TLB missと全cache missは別の原因候補。hardware counterなしに作業集合とaccess順だけで断定しない。; 条件
access-order=random、working-set=large; nodetlb、page-table、lower-cache、memory-controller、return; edgetlb-miss、walk-complete、lower-miss-memory、memory-return
| イベント | 開始 | 終了 | 条件 |
|---|---|---|---|
| parameter-change | tlb-lookup | tlb-lookup | access-order=sequential、working-set=small |
| next | tlb-lookup | l1-hit | access-order=sequential、working-set=small |
| timer | tlb-lookup | l1-hit | access-order=sequential、working-set=small |
| previous | l1-hit | tlb-lookup | access-order=sequential、working-set=small |
| reset | l1-hit | tlb-lookup | access-order=sequential、working-set=small |
| parameter-change | tlb-lookup | tlb-lookup | access-order=random、working-set=small |
| next | tlb-lookup | small-random-return | access-order=random、working-set=small |
| timer | tlb-lookup | small-random-return | access-order=random、working-set=small |
| previous | small-random-return | tlb-lookup | access-order=random、working-set=small |
| reset | small-random-return | tlb-lookup | access-order=random、working-set=small |
| parameter-change | tlb-lookup | tlb-lookup | access-order=sequential、working-set=large |
| next | tlb-lookup | large-sequential-return | access-order=sequential、working-set=large |
| timer | tlb-lookup | large-sequential-return | access-order=sequential、working-set=large |
| previous | large-sequential-return | tlb-lookup | access-order=sequential、working-set=large |
| reset | large-sequential-return | tlb-lookup | access-order=sequential、working-set=large |
| parameter-change | tlb-lookup | tlb-lookup | access-order=random、working-set=large |
| next | tlb-lookup | memory-return | access-order=random、working-set=large |
| timer | tlb-lookup | memory-return | access-order=random、working-set=large |
| previous | memory-return | tlb-lookup | access-order=random、working-set=large |
| reset | memory-return | tlb-lookup | access-order=random、working-set=large |
| 結果 | 状態 |
|---|---|
| translationとtransferを独立に測り、L1 hitを原因候補として検証する。 | l1-hit |
| 観測値だけでは証明できないためhardware counterで候補を反証する。 | small-random-return |
| 固定sample traceを機種固有の候補経路として報告する。 | large-sequential-return |
| 作業集合と順序だけでhit/missを断定せず、普遍的latencyへ一般化しない。 | memory-return |
現在の状態: 固定sample traceを開始 — 説明用の固定sample traceでtranslationとtransferを独立に追う。所要時間の観測値だけでは原因を証明できない。
このモデルは例示的かつ決定的であり、実システムの完全な再現ではありません。
動く例で考える
全要素を一回ずつ走査し、サイズとアクセス順だけを変える
python3.13 - <<'PY' > locality-results.json
import json
import os
import platform
import random
import statistics
import time
HARNESS = "memory_lab_v2"
SEED = 20260731
REPEAT = 7
portable_defaults = {
"l1_probe": 4096,
"llc_probe": 262144,
"beyond_llc_probe": 1048576,
}
execution_sizes = (
{
"l1_probe": 1024,
"llc_probe": 4096,
"beyond_llc_probe": 16384,
}
if os.environ.get("CURRICULUM_LAB_QUICK") == "1"
else portable_defaults
)
results = []
cache_medians = {}
largest_fixture = None
for size_label, size in execution_sizes.items():
values = tuple(range(size))
access_count = size
sequential = tuple(range(access_count))
stride16 = tuple(
index
for offset in range(16)
for index in range(offset, access_count, 16)
)
random_pool = list(range(access_count))
random.Random(SEED + size).shuffle(random_pool)
random_indexes = tuple(random_pool)
indexes_by_name = {
"sequential": sequential,
"stride16": stride16,
"random": random_indexes,
}
assert all(
type(indexes) is tuple
and len(indexes) == access_count
and len(set(indexes)) == access_count
and all(type(index) is int for index in indexes)
for indexes in indexes_by_name.values()
)
expected_checksums = {
name: sum(values[index] for index in indexes)
for name, indexes in indexes_by_name.items()
}
assert len(set(expected_checksums.values())) == 1
for indexes in indexes_by_name.values():
# This untimed pass is the explicit warmup; only later samples
# contribute to reported medians.
assert sum(values[index] for index in indexes) == sum(values)
samples = {name: [] for name in indexes_by_name}
base_order = tuple(indexes_by_name)
for repeat in range(7):
offset = repeat % len(base_order)
condition_order = base_order[offset:] + base_order[:offset]
if repeat % 2:
condition_order = tuple(reversed(condition_order))
for name in condition_order:
indexes = indexes_by_name[name]
start = time.perf_counter_ns()
checksum = sum(values[index] for index in indexes)
elapsed_ns = time.perf_counter_ns() - start
assert checksum == expected_checksums[name]
samples[name].append(elapsed_ns)
for name, indexes in indexes_by_name.items():
results.append({
"size_label": size_label,
"size": size,
"pattern": name,
"repeat_count": REPEAT,
"samples_ns": samples[name],
"median_ns": statistics.median(samples[name]),
"access_count": len(indexes),
"checksum": expected_checksums[name],
"checksum_matches": True,
"same_index_type": all(
type(candidate) is type(indexes)
for candidate in indexes_by_name.values()
),
})
cache_medians[size_label] = {
name: statistics.median(samples[name])
for name in samples
}
largest_fixture = (values, expected_checksums["sequential"])
assert largest_fixture is not None
largest_values, largest_checksum = largest_fixture
page_stride_indexes = tuple(
index
for offset in range(512)
for index in range(offset, len(largest_values), 512)
)
tlb_samples = []
for repeat in range(7):
start = time.perf_counter_ns()
checksum = sum(
largest_values[index] for index in page_stride_indexes
)
tlb_samples.append(time.perf_counter_ns() - start)
assert checksum == largest_checksum
print(json.dumps({
"harness": HARNESS,
"python": platform.python_version(),
"platform": platform.platform(),
"portable_defaults": portable_defaults,
"portable_default_units": (
"8-byte integer payload bytes; topology-equivalence is not claimed"
),
"execution_sizes": execution_sizes,
"topology_observation_commands": [
"lscpu --caches",
"sysctl -a | grep -E 'hw.(cache|memsize|pagesize)'",
],
"warmup_excluded": True,
"condition_order": "rotate each repeat and reverse odd repeats",
"access_patterns": ["sequential", "stride16", "random"],
"results": results,
"additional_experiments": {
"cache_probe": {
"working_set_medians_ns": cache_medians,
"interpretation": (
"size inflection supports but does not prove a cache boundary"
),
},
"tlb_probe": {
"organization": "512-element page-stride lanes",
"access_count": len(page_stride_indexes),
"samples_ns": tlb_samples,
"checksum_matches": True,
"interpretation": (
"page-stride change supports but does not prove a TLB cause"
),
},
},
"limitations": [
"Python interpreter and object/index overhead remains in every sample",
"portable defaults are probes, not the measured machine topology",
(
"512 elements is a nominal 4KiB payload stride; "
"Python layout does not establish a physical page boundary"
),
"cache and TLB counters are not observed by this timing-only script",
],
}, ensure_ascii=False, indent=2))
PY
python3.13 -m json.tool locality-results.json
lscpu
lscpu --caches
sysctl -n machdep.cpu.brand_string
sysctl -a | grep -E 'hw.(cache|memsize|pagesize)'
- 前提
- 入力生成と一回のwarmupを測定外に置き、同一プロセスで各条件を7回、条件順を回転と反転で交互化して測る。Linuxではlscpu、macOSではsysctlの成功した方を実機topology証拠として保存する。
- 入力
- 各sizeの同じ整数配列を対象に、同じtuple[int]型のsequential、stride16、固定seed random index列を作る。三列とも全indexを重複なく含み、全要素をちょうど一回読む。
- 操作
- L1 probe、LLC probe、LLC超過probeの三sizeで三走査を行う。portable defaultは32KiB、2MiB、8MiBの8-byte payloadだが実機相当とは断定せず、topology観測値を記録して調整する。
- 観測
- 全九conditionの各7値、中央値、全要素数、checksum一致を残す。追加のworking-set sweepをcache probe、512要素間隔のpage-stride lanesをTLB probeとして測る。
- 結論
- サイズの折れ曲がりとpage-stride差は各仮説を支持し得るが確定しない。Python interpreter、index object、未取得のhardware counterを限界として報告する。
観測したL1/L2/LLC容量とprivate/shared topologyを経路図へ転記し、portable defaultや教材図をそのまま実機構成とみなさない。cache probeとTLB probeはアクセス順まで変えるため完全な単独変数実験ではなく、可能ならhardware counterを追加して再検証する。
ラボ成果物: 「アクセス局所性を変えた測定とCPU・メモリ経路図」には、CPU名、OS、処理系、全反復値、走査順、総和、仮説と未観測区間を含める。
トレードオフと失敗モード
| アクセス特性 | 候補配置 | 期待する機構 | 採用しない条件 |
|---|---|---|---|
| 一要素の全fieldを同時に読む | Array of Structures | 同じ要素のデータを近接配置 | 一部fieldだけの大量走査が支配的 |
| 同じfieldを多数要素から読む | Structure of Arrays | 不要fieldのline転送を減らす | 要素単位の更新整合性が複雑化する |
| 疎なkeyで挿入削除が多い | 間接参照を許容 | 更新と識別の単純さを優先 | profileでpointer chasingがhot pathと判明 |
もっともらしい誤診と反証
- 誤診: 「配列拡大で遅くなったのでL2 cache容量を超えた」。反証: 作業集合、ページ数、アクセス順を別々に変える。ページstrideだけで段差が出るならTLB、初回だけならページフォールトも疑う。
- 誤診: 「別変数を更新しているから共有cacheの競合ではない」。反証: 変数間へpaddingを入れて配置だけを変える。同一cache line上のfalse sharingなら、意味上は別変数でもcoherence trafficが減って改善する。
局所性のための平坦化は、境界違反や重複状態を招くことがある。profileで支配的と確認した経路に限定し、同じ入力の正しさ試験と性能測定を対にする。機種が変わればcache構成も変わるため再検証する。
知識チェック
- TLB hitならcacheにもhitするか。しない。アドレス変換結果とデータ複製は別であり、一方だけhitし得る。
- 連続走査が速ければhardware prefetchが原因と確定できるか。できない。line内再利用、少ないTLB miss、処理系最適化も候補である。
- paddingで改善したらdata raceも解消したか。解消を意味しない。配置はcoherence trafficを変えるが、language memory model上の同期を置き換えない。
5分teach-back
ロード一回の経路図を使い、TLB miss、cache miss、主記憶転送を順に説明する。連続とランダムの測定差を図のどこへ結ぶか、確定できない部分を明言する。
未知へのtransfer
データ指向設計を別の処理系へ適用する。画像のRGBA処理または分析DBの列走査を選び、使うfield、更新単位、ベクトル化、メモリ増加を比較し、元のPython測定値を流用せず再測定する。
出典と次の学習
Intel Optimization Reference Manualはcache階層とデータアクセス最適化、Armv8-A memory model guideは順序と共有メモリの概念、Linux kernelのTLB文書は変換cacheを無効化するOS上の論点を確認するために使う。URLはmetadataのsourcesに集約し、本文は機構を自分の言葉で要約した。
次は並行性で、cache coherenceとプログラム上の同期契約を混同せずに扱う。1日後は経路図、7日後はstride実験、30日後は実サービスのprofile、90日後は別CPUで再測定する。
実践ラボ
走査順序だけを変えてメモリ経路を観測する
提出成果物: アクセス局所性を変えた測定とCPU・メモリ経路図
- 同じ整数配列を連続、16要素stride、固定seedランダムの順で一巡する処理を用意する
- 入力生成とウォームアップを測定から外し、各走査を7回以上実行して中央値を記録する
- 配列サイズをL1相当、最終レベルキャッシュ相当、それを超える三段階へ変える
- 仮想アドレス、TLB、キャッシュライン、主記憶を測定結果と結ぶ経路図を描く
- キャッシュミス説とTLBミス説を区別する追加実験を一つずつ行い、限界を報告する
説明して理解を確かめる
5分で、連続走査が速くなり得る機構、TLBとキャッシュの違い、測定値を普遍的な遅延定数として扱えない理由を説明する。
アセスメント
問い: 配列を2倍にした途端に所要時間が3倍になった。『L2キャッシュからあふれた』以外の仮説を二つ挙げ、反証測定を示す。
期待する証拠: TLB、ページフォールト、割り当て、CPU周波数、処理系最適化の少なくとも二つを独立変数で切り分ける計画
問い: 二つのスレッドが別変数を更新すると遅くなった。キャッシュ容量不足と断定できない理由は何か。
期待する証拠: 同一キャッシュライン上の書込み競合、coherence、配置変更による反証を含む説明
別問題へ転用する
データ指向設計を別の処理系へ適用
復習スケジュール
- 1日後
TLBミスとキャッシュミスを区別する独立変数は何か
- 7日後
作業集合が最終レベルキャッシュを超えると予測はどう変わるか
- 30日後
局所性改善を採用しない方がよい保守上の条件は何か
- 90日後
TLBミスとキャッシュミスを区別する独立変数は何か
評価ルーブリック
| 観点 | 未達 | 発展途上 | 熟達 | 卓越 |
|---|---|---|---|---|
| technical-correctness | キャッシュ、TLB、coherenceを同一機構として説明する | 階層は区別するが、仮想アドレス変換またはキャッシュライン転送が抜ける | TLB、各キャッシュ、主記憶の役割とミス時の経路を正しく図示する | 書込み共有とlanguage memory modelを区別し、機種依存の反例にも対応する |
| judgment | 局所性を常に最優先し、可読性や更新費用を無視する | 高速な配置を選ぶが、作業集合やアクセス頻度の成立条件が曖昧である | 測定したhot pathに限定し、局所性、メモリ量、変更容易性を比較する | 段階的な配置変更と撤回条件を設け、異なるCPUで再評価する |
| evidence | 一回のwall-clock値だけでキャッシュミスを断定する | 反復測定はあるが、作業集合とアクセス順序を同時に変えている | データ量を固定し、走査順序別の中央値と環境情報で仮説を比較する | 作業集合、stride、ページ数の感度を測り、計測器の限界も記録する |
| communication | 結果表と経路図が対応せず、どの操作を測ったか分からない | 測定手順は読めるが、図の各段と仮説の対応が曖昧である | 入力、操作、観測、結論を経路図の段へ一対一で結び付ける | ハードウェア依存の留保を明示し、設計レビューで再現手順を共有できる |
出典
以下の外部資料は利用者が選択したときだけ開きます。
- Intel® 64 and IA-32 Architectures Optimization (primary)
- Armv8-A memory model (primary)
- The TLB (primary)