CPUアーキテクチャ
CPUは「フェッチ→デコード→実行→ライトバック」を1秒間に数十億回繰り返す、計算の心臓部である。命令セットアーキテクチャ(ISA)の設計思想からキャッシュ階層、分岐予測、アウトオブオーダー実行まで、現代プロセッサの動作原理を体系的に理解することは、高性能ソフトウェアを書くための土台となる。
CPUアーキテクチャ
CPUは「フェッチ→デコード→実行→ライトバック」を1秒間に数十億回繰り返す、計算の心臓部である。命令セットアーキテクチャ(ISA)の設計思想からキャッシュ階層、分岐予測、アウトオブオーダー実行まで、現代プロセッサの動作原理を体系的に理解することは、高性能ソフトウェアを書くための土台となる。
この章で学ぶこと
- CPUの命令サイクルを4段階で説明できる
- パイプライン処理とその限界(ハザード)を理解する
- CISC vs RISCの設計哲学の違いを説明できる
- キャッシュ階層の仕組みとキャッシュフレンドリーなプログラミング手法を理解する
- 分岐予測の原理とプログラマーが取るべき対策を説明できる
- スーパースカラ実行、アウトオブオーダー実行、レジスタリネーミングの概要を理解する
- マルチコア・SMTの並列処理モデルを説明できる
- RISC-Vの特徴とオープンISAの意義を理解する
前提知識
- 論理ゲートの基本(AND, OR, NOT)の概念的理解があると望ましい
1. CPUの基本構造
1.1 CPUの主要コンポーネント
CPUは大きく分けて「制御ユニット」「演算ユニット(ALU)」「レジスタファイル」「キャッシュ」の4つの主要コンポーネントで構成される。これらが密接に連携して、プログラムの命令を高速に処理する。
| CPU | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ┌────────────────────────────────────────────────────────────┐ | ||||||||||||||
| 制御ユニット (CU) | ||||||||||||||
| ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ | ||||||||||||||
| 命令 | プログラム | 命令デコーダ | ||||||||||||
| レジスタ | カウンタ | |||||||||||||
| (IR) | (PC) | オペコード → 制御信号 | ||||||||||||
| └──────────┘ └──────────┘ └────────────────────────┘ | ||||||||||||||
| ┌────────────────────────────────────────────────────┐ | ||||||||||||||
| マイクロシーケンサ: 制御信号のタイミング調整 | ||||||||||||||
| └────────────────────────────────────────────────────┘ | ||||||||||||||
| └────────────────────────────────────────────────────────────┘ | ||||||||||||||
| ┌────────────────────┐ ┌──────────────────────────────────┐ | ||||||||||||||
| ALU | レジスタファイル | |||||||||||||
| (算術論理演算 | ┌────┐ ┌────┐ ┌────┐ ┌────┐ | |||||||||||||
| ユニット) | R0 | R1 | R2 | R3 | ||||||||||
| └────┘ └────┘ └────┘ └────┘ | ||||||||||||||
| ┌───────────────┐ | ┌────┐ ┌────┐ ┌────┐ ┌────┐ | |||||||||||||
| 整数加算器 | R4 | R5 | R6 | R7 | ||||||||||
| 整数乗算器 | └────┘ └────┘ └────┘ └────┘ | |||||||||||||
| 除算器 | ... | |||||||||||||
| ビットシフタ | x86-64: 16汎用 + 多数の専用 | |||||||||||||
| 論理演算器 | AArch64: 31汎用 + SP + ZR | |||||||||||||
| 比較器 | ||||||||||||||
| └───────────────┘ | ┌──────────────────────────┐ | |||||||||||||
| フラグレジスタ (RFLAGS) | ||||||||||||||
| ┌───────────────┐ | ZF: Zero Flag | |||||||||||||
| FPU/SIMD | CF: Carry Flag | |||||||||||||
| (浮動小数点 | OF: Overflow Flag | |||||||||||||
| / ベクトル) | SF: Sign Flag | |||||||||||||
| └───────────────┘ | └──────────────────────────┘ | |||||||||||||
| └────────────────────┘ └──────────────────────────────────┘ | ||||||||||||||
| ┌────────────────────────────────────────────────────────────┐ | ||||||||||||||
| キャッシュ階層 | ||||||||||||||
| L1i: 32-64KB (命令キャッシュ) レイテンシ: ~4サイクル | ||||||||||||||
| L1d: 32-64KB (データキャッシュ) レイテンシ: ~4サイクル | ||||||||||||||
| L2: 256KB-1MB (統合) レイテンシ: ~12サイクル | ||||||||||||||
| └────────────────────────────────────────────────────────────┘ | ||||||||||||||
│ バス(メモリバス / リングバス)
▼| ~40サイクル |
|---|
│| (DRAM) |
|---|
1.2 各コンポーネントの役割
| コンポーネント | 役割 | アクセス速度 | 容量の目安 |
|---|---|---|---|
| ALU | 算術演算(加減乗除)と論理演算(AND/OR/NOT/XOR)を実行 | 1サイクル(単純演算) | -- |
| FPU | 浮動小数点演算を専門に処理する演算器 | 1-5サイクル | -- |
| SIMD演算器 | 1命令で複数データを並列処理(SSE, AVX, NEON等) | 1-3サイクル | -- |
| 制御ユニット | 命令のデコードと実行制御、他コンポーネントへの制御信号送出 | -- | -- |
| レジスタ | CPU内部の超高速記憶装置(1クロックでアクセス可能) | ~0.3ns | x86: 16個, ARM: 31個 |
| PC(プログラムカウンタ) | 次に実行する命令のアドレスを保持 | -- | -- |
| IR(命令レジスタ) | 現在実行中の命令を保持 | -- | -- |
| L1キャッシュ | 最も高速なキャッシュ。命令用(L1i)とデータ用(L1d)に分離 | ~1ns | 32-64KB |
| L2キャッシュ | コアごとの中間キャッシュ | ~3-4ns | 256KB-1MB |
| L3キャッシュ | 全コア共有のキャッシュ | ~10ns | 8-96MB |
1.3 レジスタの詳細分類
レジスタはCPU内で最も高速な記憶領域であり、用途に応じて複数の種類がある。
x86-64 アーキテクチャのレジスタ構成:
汎用レジスタ(64ビット × 16個):| RAX (アキュムレータ) | RBX (ベース) |
|---|---|
| RCX (カウンタ) | RDX (データ) |
| RSI (ソースインデックス) | RDI (デスティネーション) |
| RSP (スタックポインタ) | RBP (ベースポインタ) |
| R8 〜 R15 (追加汎用) |
SIMD / 浮動小数点レジスタ:| XMM0-XMM15 (128ビット SSE) |
|---|
| YMM0-YMM15 (256ビット AVX) |
| ZMM0-ZMM31 (512ビット AVX-512) |
制御・ステータスレジスタ:| RIP (命令ポインタ = プログラムカウンタ) |
|---|
| RFLAGS (フラグレジスタ: ZF, CF, OF, SF, PF, AF) |
| CR0-CR4 (制御レジスタ: ページング、保護モード等) |
| CS, DS, SS, ES, FS, GS (セグメントレジスタ) |
AArch64 (ARM 64ビット) アーキテクチャのレジスタ構成:| X0-X30 (64ビット汎用レジスタ × 31個) |
|---|
| XZR (ゼロレジスタ: 常に0を返す) |
| SP (スタックポインタ) |
| PC (プログラムカウンタ: 直接書き込み不可) |
| V0-V31 (128ビット SIMD/FPレジスタ × 32個) |
| NZCV (条件フラグ: Negative, Zero, Carry, oVerflow) |
1.4 レジスタの部分アクセス(x86-64)
x86-64では歴史的な経緯から、64ビットレジスタの下位部分に別名でアクセスできる。
RAX (64ビット):| 63 32 31 16 15 8 7 0 | |||
|---|---|---|---|
| EAX | AH | AL | |
| AX | |||
| RAX |
例:
RAX = 0x00000000_AABBCCDD のとき
EAX = 0xAABBCCDD (下位32ビット)
AX = 0xCCDD (下位16ビット)
AH = 0xCC (AXの上位8ビット)
AL = 0xDD (AXの下位8ビット)
注意: EAXへの書き込みはRAXの上位32ビットをゼロクリアする(x86-64の仕様)
AXへの書き込みはRAXの上位ビットを変更しない
2. 命令セットアーキテクチャ(ISA)
2.1 ISAとは何か
命令セットアーキテクチャ(ISA: Instruction Set Architecture)は、ソフトウェアとハードウェアの間のインターフェースを定義する仕様である。ISAは以下の要素を規定する。
- 命令の種類と形式: どのような演算ができ、命令がどのようにエンコードされるか
- データ型: サポートする整数・浮動小数点数の幅
- レジスタの構成: 汎用レジスタの数、幅、用途
- メモリモデル: アドレッシングモード、エンディアン、アライメント
- 割り込み・例外: ハードウェア割り込みやトラップの処理方法
- 特権レベル: ユーザーモードとカーネルモードの区別
ISAが同一であれば、異なるマイクロアーキテクチャ(具体的なハードウェア実装)上でも同じバイナリが動作する。例えば、x86-64のISAに準拠していれば、Intel Core でも AMD Ryzen でも同一のプログラムを実行できる。
2.2 命令の構成要素
一般的な機械語命令は以下の要素で構成される。
命令のフォーマット(概念図):| オペコード | オペランド1 | オペランド2 | オペランド3 |
|---|---|---|---|
| (操作種別) | (宛先) | (ソース1) | (ソース2) |
例: ADD R1, R2, R3
│ │ │ │
│ │ │ └── オペランド3: ソースレジスタ2
│ │ └─────── オペランド2: ソースレジスタ1
│ └──────────── オペランド1: 宛先レジスタ
└───────────────── オペコード: 加算を指示
2.3 アドレッシングモード
CPUがオペランドの値をどこから取得するかを指定する方式を「アドレッシングモード」と呼ぶ。
| モード | 記法例(x86) | 説明 | 用途 |
|---|---|---|---|
| 即値 | mov rax, 42 |
命令自体に値が埋め込まれている | 定数の設定 |
| レジスタ | mov rax, rbx |
レジスタの値を使用 | 変数間の代入 |
| 直接 | mov rax, [0x1000] |
メモリの固定アドレスの値を使用 | グローバル変数 |
| レジスタ間接 | mov rax, [rbx] |
レジスタが指すメモリの値を使用 | ポインタ参照 |
| ベース+オフセット | mov rax, [rbp-8] |
ベースレジスタ+定数オフセット | ローカル変数 |
| インデックス付き | mov rax, [rbx+rcx*8] |
ベース+インデックス×スケール | 配列アクセス |
; アドレッシングモードの具体例(x86-64)
; 即値アドレッシング: 定数42をRAXに設定
mov rax, 42 ; RAX = 42
; レジスタアドレッシング: RBXの値をRAXにコピー
mov rax, rbx ; RAX = RBX
; レジスタ間接: RBXが指すメモリアドレスの値をRAXにロード
mov rax, [rbx] ; RAX = Memory[RBX]
; ベース+オフセット: スタック上のローカル変数アクセス
mov rax, [rbp-8] ; RAX = Memory[RBP - 8]
; インデックス付き: 配列の要素アクセス(8バイト要素)
; array[i] にアクセス: RBX=配列先頭, RCX=インデックス
mov rax, [rbx+rcx*8] ; RAX = Memory[RBX + RCX * 8]
; RIP相対: 現在の命令位置からの相対アドレス(位置独立コード)
mov rax, [rip+0x1234] ; RAX = Memory[RIP + 0x1234]2.4 命令の分類
ISAが提供する命令は、大きく以下のカテゴリに分類される。
命令の分類体系:
1. データ転送命令
├── レジスタ間: MOV, MOVZX, MOVSX
├── ロード: LDR (ARM), MOV reg,[mem] (x86)
└── ストア: STR (ARM), MOV [mem],reg (x86)
2. 算術演算命令
├── 整数: ADD, SUB, MUL, DIV, INC, DEC, NEG
└── 浮動小数: FADD, FSUB, FMUL, FDIV (x87)
ADDSS, MULSD (SSE/AVX)
3. 論理演算命令
├── ビット演算: AND, OR, XOR, NOT
└── シフト: SHL, SHR, SAR, ROL, ROR
4. 比較・分岐命令
├── 比較: CMP, TEST
├── 条件分岐: JE, JNE, JG, JL, JGE, JLE, ...
├── 無条件: JMP, CALL, RET
└── 条件実行: CMOV (x86), 条件接尾辞 (ARM)
5. SIMD / ベクトル命令
├── SSE: ADDPS, MULPS, SHUFPS, ...
├── AVX: VADDPS, VMULPS, VFMADD, ...
└── NEON: FADD, FMUL (ARM)
6. システム命令
├── 特権: INT, SYSCALL, HLT, WBINVD
├── 同期: LOCK, MFENCE, LFENCE, SFENCE
└── 仮想化: VMXON, VMLAUNCH (Intel VT-x)
3. 命令サイクル
3.1 基本4段階
CPUは全ての命令を以下の4段階(または5段階)で処理する。これは最も基本的なモデルであり、現代のCPUではさらに細分化されている(後述)が、概念的な理解としてはこの4段階モデルが重要である。
命令サイクル(4段階モデル):| Fetch | ───→ | Decode | ───→ | Execute | ───→ | Write Back |
|---|---|---|---|---|---|---|
| 取得 | 解読 | 実行 | 書き戻し |
│ │ │ │
PCのアドレス 命令を解析 ALUで演算 結果をレジスタ
からメモリの してオペコード またはメモリ またはメモリに
命令を読む とオペランド アクセスを実行 格納する
を特定
各段階の詳細:
- Fetch(命令取得): プログラムカウンタ(PC)が指すメモリアドレスから命令を読み出し、命令レジスタ(IR)に格納する。同時にPCを次の命令アドレスへ更新する。
- Decode(命令解読): IRに格納された命令のビットパターンを解析し、オペコード(操作種別)とオペランド(対象データ)を特定する。必要なレジスタの値もこの段階で読み出す。
- Execute(実行): ALUが実際の演算を行う。メモリアクセスが必要な場合はアドレス計算もここで行う。5段パイプラインではこの後にMemoryアクセス段階が独立して存在する。
- Write Back(書き戻し): 演算結果をデスティネーションレジスタまたはメモリに書き込む。フラグレジスタの更新もこの段階で行われる。
3.2 5段パイプラインモデル
教科書的な5段パイプライン(MIPSアーキテクチャ等)では、Execute段階がさらに「演算」と「メモリアクセス」に分かれる。
5段パイプライン:| IF | ──→ | ID | ──→ | EX | ──→ | MEM | ──→ | WB |
|---|---|---|---|---|---|---|---|---|
| 命令 | 命令 | 演算 | メモリ | 書き | ||||
| 取得 | 解読/ | 実行 | アクセス | 戻し | ||||
| レジスタ | ||||||||
| 読出 |
IF: Instruction Fetch - 命令メモリから命令を取得
ID: Instruction Decode - 命令を解読し、レジスタの値を読み出す
EX: Execute - ALUで演算を行う
MEM: Memory Access - データメモリの読み書き(Load/Storeのみ)
WB: Write Back - 結果をレジスタに書き込む
3.3 具体例: アセンブリ命令の実行トレース
; x86-64 アセンブリ: 2つの数値を加算
; C言語の a = b + c に相当
; 命令1: bの値をRAXにロード
mov rax, [rbp-8]
; IF: PC=0x401000 からこの命令のバイト列を取得
; ID: MOV命令と解読。ソース=[RBP-8]、宛先=RAX
; EX: アドレス計算 RBP - 8 = 0x7FFFE000
; MEM: メモリ[0x7FFFE000]から値を読み出し
; WB: 読み出した値をRAXに格納
; 命令2: cの値をRAXに加算
add rax, [rbp-16]
; IF: 次のPCからこの命令を取得
; ID: ADD命令と解読。ソース=RAXと[RBP-16]、宛先=RAX
; EX: アドレス計算 RBP - 16
; MEM: メモリからcの値を読み出し
; WB: RAX + c の結果をRAXに格納、フラグ更新
; 命令3: 結果をaに格納
mov [rbp-24], rax
; IF: この命令を取得
; ID: MOV命令(ストア)と解読。ソース=RAX、宛先=[RBP-24]
; EX: アドレス計算 RBP - 24
; MEM: RAXの値をメモリ[RBP-24]に書き込み
; WB: レジスタへの書き戻しなし3.4 現代のCPUにおけるパイプラインの深さ
実際の商用CPUでは、パイプラインは5段よりもはるかに深い。段数が多いほどクロック周波数を上げやすくなるが、分岐予測ミスのペナルティも大きくなる。
| CPU | 年代 | パイプライン段数 | 備考 |
|---|---|---|---|
| MIPS R2000 | 1985 | 5段 | 教科書的な基本パイプライン |
| Intel Pentium III | 1999 | 10段 | |
| Intel Pentium 4 (Prescott) | 2004 | 31段 | 深すぎて分岐ミスペナルティ大 |
| Intel Core (Skylake) | 2015 | 14-19段 | 適度な深さに回帰 |
| Apple M1 (Firestorm) | 2020 | 13段程度 | 高IPC設計 |
| AMD Zen 4 | 2022 | 19段程度 |
教訓: Pentium 4 の NetBurst アーキテクチャは31段もの深いパイプラインを採用し、クロック周波数を3.8GHzまで引き上げたが、分岐予測ミス時のペナルティが大きく、IPCが低下した。後継の Core アーキテクチャでは14段程度に戻し、IPC を大幅に向上させた。これは「クロック周波数だけが性能ではない」ことを示す歴史的な事例である。
4. パイプライン処理
4.1 基本概念
パイプライン処理は、複数の命令の各段階を時間的に重ね合わせ(オーバーラップ)させることで、スループット(単位時間あたりの命令完了数)を向上させる技術である。1命令あたりのレイテンシは変わらないが、連続して処理する場合の効率が劇的に改善する。
パイプラインなし(逐次実行):
時間 → 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
命令1: IF ID EX ME WB
命令2: IF ID EX ME WB
命令3: IF ID EX ME WB
→ 3命令完了に15クロック必要
5段パイプライン:
時間 → 1 2 3 4 5 6 7
命令1: IF ID EX ME WB
命令2: IF ID EX ME WB
命令3: IF ID EX ME WB
→ 3命令完了に7クロック(= 5 + (3-1))
IF=Fetch ID=Decode EX=Execute ME=Memory WB=WriteBack
一般化: N命令をK段パイプラインで処理する場合
- パイプラインなし: N × K サイクル
- パイプラインあり: K + (N - 1) サイクル
- 高速化比: N × K / (K + N - 1)
- N が十分大きいとき: 約 K 倍の高速化
4.2 パイプラインの理想と現実
理想的なパイプラインでは、毎サイクル1命令が完了する(CPI = 1: Cycles Per Instruction)。しかし現実には以下の要因でパイプラインが「ストール」(停止)し、性能が低下する。
パイプラインストール(バブルの挿入):
正常時:
時間 → 1 2 3 4 5 6 7 8
命令1: IF ID EX ME WB
命令2: IF ID EX ME WB
命令3: IF ID EX ME WB
命令4: IF ID EX ME WB
ストール発生時(命令2が命令1の結果を待つ場合):
時間 → 1 2 3 4 5 6 7 8 9
命令1: IF ID EX ME WB
命令2: IF ID -- EX ME WB ← IDの後にバブル(空白サイクル)
命令3: IF -- ID EX ME WB ← 1サイクル遅延
命令4: -- IF ID EX ME WB ← 1サイクル遅延
"--" はバブル(NOP)= パイプラインが無駄になるサイクル
4.3 パイプラインハザード(3種類)
パイプラインの効率を阻害する要因を「ハザード」と呼ぶ。ハザードは大きく3種類に分類される。
| パイプラインハザード 3種 |
|---|
| 1. データハザード (Data Hazard) |
| ───────────────────────────── |
| 前の命令の結果を後の命令が使用する場合に発生。 |
| 3つのサブタイプが存在する: |
| RAW (Read After Write) - 最も一般的: |
| ADD R1, R2, R3 ; R1に書き込み |
| SUB R4, R1, R5 ; R1を読み出し ← まだ書き込み完了前 |
| WAR (Write After Read): |
| ADD R1, R2, R3 ; R2を読み出し |
| SUB R2, R4, R5 ; R2に書き込み ← 先に書き込むと危険 |
| WAW (Write After Write): |
| ADD R1, R2, R3 ; R1に書き込み |
| SUB R1, R4, R5 ; R1に書き込み ← 順序が逆転すると危険 |
| 対策: |
| - フォワーディング(バイパス): 結果をWB前にEX段から転送 |
| - パイプラインストール: 依存解消まで待機 |
| - レジスタリネーミング: WAR/WAWを除去 |
| 2. 制御ハザード (Control Hazard) |
| ───────────────────────────── |
| 分岐命令で次に実行すべき命令のアドレスが不確定な場合。 |
| BEQ R1, R2, label ; R1==R2 なら label へ分岐 |
| ADD R3, R4, R5 ; ← 実行すべきか、分岐先か不明 |
| 対策: |
| - 分岐予測 (Branch Prediction): 分岐先を予測して投機実行 |
| - 遅延分岐 (Delayed Branch): 分岐直後の命令を常に実行 |
| - 投機的実行: 予測に基づいて実行し、外れたら巻き戻し |
| 3. 構造ハザード (Structural Hazard) |
| ───────────────────────────── |
| 同一のハードウェア資源を同一サイクルで複数の命令が使用。 |
| 例: Fetch段階とMemory段階が同じメモリポートを使用 |
| 対策: |
| - 資源の複製: L1キャッシュをL1i(命令)とL1d(データ)に分離 |
| - パイプラインストール: 片方を待たせる |
4.4 フォワーディング(データバイパス)
データハザードの最も一般的な解決策がフォワーディングである。演算結果をWriteBack段階まで待たずに、Execute段階の出力を直接次の命令のExecute段階に転送する。
フォワーディングなし(2サイクルのストール):
時間 → 1 2 3 4 5 6 7 8 9
ADD R1,R2,R3: IF ID EX ME WB
SUB R4,R1,R5: IF ID -- -- EX ME WB ← R1が確定するまで待機
フォワーディングあり(ストールなし):
時間 → 1 2 3 4 5 6 7
ADD R1,R2,R3: IF ID EX ME WB
SUB R4,R1,R5: IF ID EX ME WB
↑
EX段の出力を直接転送(バイパスパス)
ただし Load-Use ハザードではフォワーディングでも1サイクルのストールが必要:
時間 → 1 2 3 4 5 6 7 8
LDR R1,[R2]: IF ID EX ME WB
ADD R3,R1,R4: IF ID -- EX ME WB ← MEの結果がEXに必要
↑
MEの出力をEXに転送(1サイクルのストール)
4.5 パイプラインの性能指標
# パイプラインの性能計算(Python)
def pipeline_performance(
num_instructions: int,
pipeline_stages: int,
stall_cycles: int = 0,
branch_miss_rate: float = 0.0,
branch_penalty: int = 0,
branch_frequency: float = 0.0
) -> dict:
"""パイプラインの性能指標を計算する。
Args:
num_instructions: 命令数
pipeline_stages: パイプライン段数
stall_cycles: データハザードによる総ストールサイクル数
branch_miss_rate: 分岐予測ミス率 (0.0-1.0)
branch_penalty: 分岐ミス1回あたりのペナルティ(サイクル数)
branch_frequency: 全命令中の分岐命令の割合 (0.0-1.0)
Returns:
性能指標の辞書
"""
# 理想的な実行サイクル数(ハザードなし)
ideal_cycles = pipeline_stages + (num_instructions - 1)
# 分岐ミスによるペナルティサイクル数
num_branches = int(num_instructions * branch_frequency)
branch_stalls = int(num_branches * branch_miss_rate * branch_penalty)
# 実際の実行サイクル数
actual_cycles = ideal_cycles + stall_cycles + branch_stalls
# CPI (Cycles Per Instruction)
cpi = actual_cycles / num_instructions
# IPC (Instructions Per Cycle)
ipc = num_instructions / actual_cycles
# パイプラインなしの場合
no_pipeline_cycles = num_instructions * pipeline_stages
# 高速化比
speedup = no_pipeline_cycles / actual_cycles
return {
"ideal_cycles": ideal_cycles,
"actual_cycles": actual_cycles,
"data_stalls": stall_cycles,
"branch_stalls": branch_stalls,
"cpi": round(cpi, 3),
"ipc": round(ipc, 3),
"speedup": round(speedup, 2),
"efficiency": round(ipc / 1.0 * 100, 1), # 理想IPC=1に対する効率
}
# 使用例
result = pipeline_performance(
num_instructions=1000,
pipeline_stages=5,
stall_cycles=50, # データハザードで50サイクルストール
branch_miss_rate=0.05, # 分岐予測ミス率5%
branch_penalty=15, # ミス時15サイクルのペナルティ
branch_frequency=0.20 # 命令の20%が分岐命令
)
# 結果例:
# ideal_cycles: 1004
# actual_cycles: 1054 + 150 = 1204
# CPI: 1.204
# IPC: 0.831
# speedup: 4.15x(パイプラインなしの5000サイクルに対して)5. 分岐予測(Branch Prediction)
5.1 なぜ分岐予測が重要か
現代のCPUはパイプラインが深く(14〜20段)、分岐命令の結果が確定するまでに多数のサイクルを要する。分岐先が確定するまでパイプラインを止めると、大きなスループット低下を招く。そこで、分岐の結果を「予測」して投機的に命令を実行し、予測が正しければペナルティなし、外れた場合のみパイプラインをフラッシュしてやり直す。
分岐予測の概要:
分岐命令 BEQ R1, R2, target:
予測正解の場合:
IF ID EX ME WB ← 分岐命令
IF ID EX ME WB ← 予測した方の命令(正解)
IF ID EX ME WB
→ ペナルティなし、スムーズに実行継続
予測失敗の場合:
IF ID EX ME WB ← 分岐命令
IF ID EX ×× ×× ← 予測した命令(間違い)→ 破棄
IF ID ×× ×× ×× ← 予測した命令(間違い)→ 破棄
IF ×× ×× ×× ×× ← 予測した命令(間違い)→ 破棄
IF ID EX ME WB ← 正しい分岐先から再開
→ 数サイクルの無駄(フラッシュペナルティ)
5.2 静的予測と動的予測
分岐予測は大きく「静的予測」と「動的予測」に分類される。
静的予測(コンパイル時に決定、ハードウェア的に固定):
- 常に分岐しない予測: 条件分岐は常に「分岐しない」と予測。単純だが精度が低い。
- 後方分岐は分岐する予測: ループの戻り先への分岐(後方分岐)は「分岐する」と予測。ループ内のほとんどのイテレーションで正解する。
- コンパイラヒント: GCCの
__builtin_expectのように、コンパイラが分岐の方向をヒントとしてCPUに伝える。
動的予測(実行履歴に基づいてハードウェアが予測):
- 1ビットカウンタ: 前回の結果をそのまま予測。精度約85%。
- 2ビット飽和カウンタ: 2回連続で外れるまで予測を変えない。精度約90%。
- 相関予測器: 他の分岐の結果も考慮して予測。精度約95%。
- TAGE予測器: 複数の履歴長を持つテーブルを使う高精度予測器。精度97%以上。
5.3 2ビット飽和カウンタの仕組み
最も基本的な動的予測器の一つが2ビット飽和カウンタである。
2ビット飽和カウンタの状態遷移:| 00: 強く | ──────────→ | 01: 弱く |
|---|---|---|
| 分岐しない | ←────────── | 分岐しない |
↑ 分岐せず │ 分岐した
│ ▼| 10: 弱く | ←────────── | 11: 強く |
|---|---|---|
| 分岐する | ──────────→ | 分岐する |
予測方法:
状態 00, 01 → 「分岐しない」と予測
状態 10, 11 → 「分岐する」と予測
例: ループ10回の分岐 (9回分岐する、最後の1回で分岐しない)
反復: 1 2 3 4 5 6 7 8 9 10
実際: T T T T T T T T T NT
状態: 01 10 11 11 11 11 11 11 11 10
予測: NT NT T T T T T T T T
正否: × × O O O O O O O ×
→ 10回中8回正解(80%)、次のループ突入で1回ミス
→ 全体では 8/10 = 80% → ループが長いほど精度向上
5.4 分岐予測の精度比較
| 予測方式 | 精度の目安 | ハードウェアコスト | 採用例 |
|---|---|---|---|
| 常に分岐しない | ~60% | ほぼゼロ | 最も単純な実装 |
| 1ビットカウンタ | ~85% | 低 | 初期のパイプラインCPU |
| 2ビット飽和カウンタ | ~90% | 低〜中 | 幅広く使用 |
| 相関予測器 (gshare) | ~95% | 中 | Pentium Pro以降 |
| トーナメント予測器 | ~96% | 中〜高 | Alpha 21264 |
| TAGE予測器 | ~97% | 高 | 現代の高性能CPU |
| パーセプトロン予測器 | ~97% | 高 | AMD Zen系列 |
5.5 分岐予測がプログラムに与える影響
// 分岐予測の影響を確認する典型的な例(C言語)
//
// ソート済み配列と未ソート配列で、条件分岐の予測しやすさが変わる
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define SIZE 32768
int compare_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int main(void) {
int data[SIZE];
srand(42);
for (int i = 0; i < SIZE; i++)
data[i] = rand() % 256;
// --- テスト1: 未ソート配列 ---
clock_t start = clock();
long long sum = 0;
for (int iter = 0; iter < 100000; iter++) {
for (int j = 0; j < SIZE; j++) {
if (data[j] >= 128) // 分岐: ランダムなので予測困難
sum += data[j];
}
}
clock_t end = clock();
printf("Unsorted: sum=%lld, time=%.3f sec\n",
sum, (double)(end - start) / CLOCKS_PER_SEC);
// --- テスト2: ソート済み配列 ---
int sorted[SIZE];
memcpy(sorted, data, sizeof(data));
qsort(sorted, SIZE, sizeof(int), compare_int);
start = clock();
sum = 0;
for (int iter = 0; iter < 100000; iter++) {
for (int j = 0; j < SIZE; j++) {
if (sorted[j] >= 128) // 分岐: 前半は全てfalse、後半は全てtrue
sum += sorted[j];
}
}
end = clock();
printf("Sorted: sum=%lld, time=%.3f sec\n",
sum, (double)(end - start) / CLOCKS_PER_SEC);
// 典型的な結果:
// Unsorted: time=11.5 sec (分岐予測ミス率 ~50%)
// Sorted: time= 3.8 sec (分岐予測ミス率 ~0%)
// → 同じ計算量で約3倍の差
return 0;
}5.6 ブランチレスプログラミング
分岐予測ミスを回避する手法として、条件分岐を使わずに同等の処理を行う「ブランチレスプログラミング」がある。
// ブランチレスプログラミングの例
// 分岐あり(予測ミスの可能性あり)
int max_branch(int a, int b) {
if (a > b)
return a;
else
return b;
}
// ブランチレス版(ビット演算を利用)
int max_branchless(int a, int b) {
// (a > b) のとき diff < 0 → mask = 0xFFFFFFFF
// (a <= b) のとき diff >= 0 → mask = 0x00000000
int diff = b - a;
int mask = diff >> 31; // 算術右シフト: 符号ビットで埋める
return b - (diff & mask);
}
// コンパイラの最適化を活用(推奨)
int max_cmov(int a, int b) {
// 多くのコンパイラは三項演算子をCMOV命令に変換する
return (a > b) ? a : b;
}
// x86-64で生成されるアセンブリ:
// max_branch: max_cmov:
// cmp edi, esi cmp edi, esi
// jle .L2 cmovge eax, edi ← 条件付きムーブ(分岐なし)
// mov eax, edi ret
// ret
// .L2:
// mov eax, esi
// ret
// 配列の条件付き加算(ブランチレス版)
long sum_branchless(const int *data, int size) {
long sum = 0;
for (int i = 0; i < size; i++) {
// data[i] >= 128 のとき mask = 0xFFFFFFFF, そうでなければ 0
int mask = -(data[i] >= 128); // bool → 0 or 1 → 0 or -1
sum += (data[i] & mask);
}
return sum;
}6. CISC vs RISC
6.1 設計哲学の違い
コンピュータアーキテクチャの歴史において、命令セットの設計方針は大きく2つの流派に分かれた。CISC(Complex Instruction Set Computer)とRISC(Reduced Instruction Set Computer)である。
CISC (Complex Instruction Set Computer):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
設計哲学: 「1命令で多くの仕事をさせる」
代表: x86/x64 (Intel, AMD), VAX, MC68000
歴史的背景:
- 1970-80年代、メモリは高価で遅かった
- 命令数を減らす → メモリ使用量を削減
- 1命令で複雑な処理 → メモリアクセス回数を削減
- コンパイラが未熟 → ハードウェアで複雑な処理をサポート
特徴:| - 命令が可変長(x86: 1〜15バイト) |
|---|
| - 1命令でメモリアクセス + 演算が可能 |
| - 多数のアドレッシングモード |
| - マイクロコードで内部的にRISC風の |
| マイクロオペレーション(uop)に分解 |
| - 命令数が多い(1500+) |
| - 後方互換性を重視(40年以上) |
RISC (Reduced Instruction Set Computer):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
設計哲学: 「命令を単純にし、パイプラインを最大限に効率化する」
代表: ARM, RISC-V, MIPS, SPARC, PowerPC
歴史的背景:
- 1980年代、研究により実際に使われる命令の80%が
全命令の20%であることが判明
- パターソンとヘネシーが「単純な命令 + 高効率パイプライン」
の方が高性能であることを実証
- コンパイラ技術の発展により、複雑な命令は不要に
特徴:| - 命令が固定長(4バイト) |
|---|
| - Load/Store アーキテクチャ |
| (メモリアクセスは専用命令のみ) |
| - 単純な命令 → パイプラインに最適化 |
| - レジスタ数が多い(ARM: 31個, RISC-V: 32個) |
| - 命令数が少ない(基本100〜300) |
| - 消費電力が低い |
6.2 詳細比較表
| 比較項目 | CISC (x86-64) | RISC (AArch64 / ARM) | RISC-V (RV64G) |
|---|---|---|---|
| 代表的CPU | Intel Core, AMD Ryzen | Apple M-series, Snapdragon, AWS Graviton | SiFive, Alibaba Xuantie |
| 命令長 | 可変長 (1-15バイト) | 固定長 (4バイト) | 固定長 (4バイト, C拡張で2バイトも可) |
| 命令数 | 1500+ | ~1000 (拡張含む) | ~300 (基本+標準拡張) |
| 汎用レジスタ | 16個 (x64) | 31個 (X0-X30) | 32個 (x0-x31, x0は常に0) |
| メモリアクセス | 演算命令で直接可能 | Load/Store命令のみ | Load/Store命令のみ |
| アドレッシング | 多数のモード | 比較的少数 | 少数 |
| 条件実行 | CMOV命令 | 条件分岐 + CSEL命令 | 条件分岐のみ(基本ISA) |
| 消費電力 | 高い (15-125W TDP) | 低い (1-15W) | 設計による(超低消費可能) |
| 主な用途 | デスクトップ、サーバー | モバイル、組込み、サーバー | IoT、組込み、学術研究 |
| 互換性 | 40年以上の後方互換性 | バージョン間で変更あり | ISAが凍結・安定 |
| ライセンス | Intel/AMDが独占 | ARM社にライセンス料 | オープンソース・無料 |
| デコード | 複雑(マイクロコード) | 比較的単純 | 非常に単純 |
6.3 同じ処理のアセンブリ比較
; 処理: memory[C] = memory[A] + memory[B]
; 3つのアーキテクチャで比較
; ============================================================
; x86-64 (CISC)
; ============================================================
; メモリ上のデータを直接演算命令で操作できる
mov eax, [A] ; メモリ[A]の値をEAXにロード
add eax, [B] ; EAXにメモリ[B]の値を直接加算(メモリ+演算が1命令)
mov [C], eax ; 結果をメモリ[C]にストア
; → 3命令(ただし命令長は可変: 合計約10-18バイト)
; ============================================================
; AArch64 (ARM RISC)
; ============================================================
; Load/Storeアーキテクチャ: メモリアクセスと演算は別命令
; アドレスは別のレジスタに予め設定されているものとする
ldr w0, [x3] ; メモリ[x3]の値をW0にロード
ldr w1, [x4] ; メモリ[x4]の値をW1にロード
add w2, w0, w1 ; W2 = W0 + W1(レジスタ間の演算のみ)
str w2, [x5] ; W2の値をメモリ[x5]にストア
; → 4命令(全て4バイト固定: 合計16バイト)
; ============================================================
; RISC-V (RV32I)
; ============================================================
; ARMと同様のLoad/Storeアーキテクチャ
lw t0, 0(s0) ; メモリ[s0+0]の値をt0にロード
lw t1, 0(s1) ; メモリ[s1+0]の値をt1にロード
add t2, t0, t1 ; t2 = t0 + t1
sw t2, 0(s2) ; t2の値をメモリ[s2+0]にストア
; → 4命令(全て4バイト固定: 合計16バイト)
; CISCは命令数が少ないが、各命令のデコードが複雑で可変長。
; RISCは命令数が多いが、各命令が単純で固定長のため
; パイプラインが効率的に動作する。
; 現代のCISC (x86)は内部でRISC風のuopに分解して実行している。6.4 現代における収束
歴史的にCISCとRISCは対立概念として語られてきたが、現代のプロセッサでは両者の境界は曖昧になっている。
現代のCPUにおけるCISC/RISCの収束:
x86 (CISC) の内部:| x86命令 → フロントエンド → uop (マイクロ命令) |
|---|
| (デコーダ) |
| ADD EAX,[mem] → uop1: LOAD tmp,[mem] |
| uop2: ADD EAX,tmp |
| → 内部的にはRISC風の単純命令に分解して |
| アウトオブオーダーエンジンで実行 |
ARM (RISC) の拡張:| - 命令セットは拡張を続け、複雑化 |
|---|
| - SVE/SVE2 (スケーラブルベクトル拡張) |
| - 暗号化命令、行列演算命令の追加 |
| - Apple M-series は超広帯域デコーダ搭載 |
| (8命令同時デコード) |
| → 単純なRISCの枠を超えた高性能設計 |
結論:
- 純粋なCISC/RISCの区別は薄れている
- x86はISAレベルではCISCだが、実行エンジンはRISC的
- ARMはISAレベルではRISCだが、実装は高度に複雑化
- 重要なのは「ISAの設計」より「マイクロアーキテクチャの効率」
6.5 RISC-Vの革新性
RISC-Vは2010年にカリフォルニア大学バークレー校で誕生したオープンソースISAである。
RISC-V の特徴と意義:
1. オープンソースISA
├── ライセンス料が不要(ARMは数百万〜数千万ドル)
├── 誰でもRISC-V準拠のCPUを設計・製造可能
└── 仕様は RISC-V International が管理
2. モジュラー設計
├── 基本命令セット: RV32I / RV64I(最小限の整数命令)
├── 標準拡張:
│ ├── M: 整数乗除算
│ ├── A: アトミック操作
│ ├── F: 単精度浮動小数点
│ ├── D: 倍精度浮動小数点
│ ├── C: 圧縮命令(16ビット命令)
│ └── V: ベクトル拡張
└── カスタム拡張: AI用命令、暗号命令等を自由に追加可能
3. 主なユースケース
├── IoT / 組込み: 超小型・超低消費電力のコア
├── 学術研究: 自由に改変・実験可能
├── 中国の半導体産業: ARM制裁回避と自給自足
└── データセンター: Ventana, Tenstorrent等がサーバー向け開発中
4. エコシステムの成長
├── Linux カーネルが公式サポート
├── GCC, LLVM が対応済み
├── Android が RISC-V をサポート
└── 出荷済みチップ数: 100億個超(2024年時点)
7. キャッシュメモリの詳細
7.1 メモリ階層と局所性の原理
プログラムのメモリアクセスには「局所性」(Locality)と呼ばれる統計的な偏りがある。キャッシュメモリはこの局所性を利用して高速なデータアクセスを実現する。
メモリ階層のピラミッド:| g |
|---|
局所性の原理:
- 時間的局所性: 最近アクセスしたデータは近い将来また使われる
例: ループ変数、関数内の局所変数
- 空間的局所性: あるアドレスにアクセスした後、近傍のアドレスもアクセスされる
例: 配列の連続要素、命令の逐次実行
7.2 キャッシュの構造
キャッシュの内部構造(セットアソシアティブ方式):
8ウェイ・セットアソシアティブ・L1データキャッシュ(32KB)の例:
メモリアドレスの分解:| タグ | セット | オフセット |
|---|---|---|
| (上位ビット) | インデックス | (下位ビット) |
キャッシュの内部:
セット0: [Way0][Way1][Way2][Way3][Way4][Way5][Way6][Way7]
セット1: [Way0][Way1][Way2][Way3][Way4][Way5][Way6][Way7]
...
セットN: [Way0][Way1][Way2][Way3][Way4][Way5][Way6][Way7]
各エントリ:| Valid | Tag | Data (キャッシュライン: 64B) |
|---|---|---|
| (1bit) |
アソシアティビティの種類:| ダイレクトマップ | Nウェイセット | フルアソシアティブ |
|---|---|---|
| (1ウェイ) | アソシアティブ | |
| 高速だが衝突多い | バランス型 | 衝突なし・低速 |
7.3 キャッシュの書き込みポリシー
| ポリシー | 動作 | 利点 | 欠点 |
|---|---|---|---|
| ライトスルー | 書き込み時にキャッシュとメモリの両方を更新 | データの一貫性が保たれる | 書き込みが遅い |
| ライトバック | キャッシュのみ更新し、追い出し時にメモリに書き戻す | 書き込みが高速 | 一貫性管理が複雑 |
| ライトアロケート | 書き込みミス時にキャッシュラインを割り当てる | 後続の書き込みがヒットする | 余分なメモリ読み出しが発生 |
| ノーライトアロケート | 書き込みミス時にキャッシュに割り当てない | 一時的なデータで無駄なキャッシュ使用を防ぐ | 書き込みの局所性を利用できない |
7.4 キャッシュフレンドリーなプログラミング
// キャッシュ効率の実験: 行優先 vs 列優先アクセス
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 4096
// 動的確保(スタックオーバーフロー防止)
static int matrix[N][N];
// 行優先アクセス: キャッシュフレンドリー
long sum_row_major(void) {
long sum = 0;
for (int row = 0; row < N; row++) {
for (int col = 0; col < N; col++) {
sum += matrix[row][col];
// メモリ上の配置: [0][0], [0][1], [0][2], ...
// → 連続アドレスへのアクセス
// → 1つのキャッシュライン(64B)で16個のint(4B)をカバー
// → キャッシュヒット率: 約 15/16 = 93.75%
}
}
return sum;
}
// 列優先アクセス: キャッシュに不利
long sum_col_major(void) {
long sum = 0;
for (int col = 0; col < N; col++) {
for (int row = 0; row < N; row++) {
sum += matrix[row][col];
// メモリ上の配置: [0][0], [1][0], [2][0], ...
// → stride = N * sizeof(int) = 16384 バイト飛び
// → 毎回異なるキャッシュラインにアクセス
// → キャッシュミスが多発
}
}
return sum;
}
// ブロッキング(タイリング): 大きな行列でもキャッシュ効率を維持
#define BLOCK 64 // L1キャッシュに収まるブロックサイズ
long sum_blocked(void) {
long sum = 0;
for (int bi = 0; bi < N; bi += BLOCK) {
for (int bj = 0; bj < N; bj += BLOCK) {
// BLOCK × BLOCK のサブ行列を処理
// このサブ行列はL1キャッシュに収まる
int row_end = (bi + BLOCK < N) ? bi + BLOCK : N;
int col_end = (bj + BLOCK < N) ? bj + BLOCK : N;
for (int row = bi; row < row_end; row++) {
for (int col = bj; col < col_end; col++) {
sum += matrix[row][col];
}
}
}
}
return sum;
}
int main(void) {
// 初期化
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
matrix[i][j] = (i + j) % 100;
clock_t start, end;
start = clock();
long s1 = sum_row_major();
end = clock();
printf("Row-major: sum=%ld, time=%.4f sec\n",
s1, (double)(end - start) / CLOCKS_PER_SEC);
start = clock();
long s2 = sum_col_major();
end = clock();
printf("Col-major: sum=%ld, time=%.4f sec\n",
s2, (double)(end - start) / CLOCKS_PER_SEC);
start = clock();
long s3 = sum_blocked();
end = clock();
printf("Blocked: sum=%ld, time=%.4f sec\n",
s3, (double)(end - start) / CLOCKS_PER_SEC);
// 典型的な結果 (N=4096):
// Row-major: time=0.03 sec
// Col-major: time=0.25 sec (8〜10倍遅い)
// Blocked: time=0.03 sec (行優先と同等)
return 0;
}メモリレイアウトとキャッシュラインの関係(詳細図解):
C言語の2次元配列 int matrix[4][4] のメモリ配置:
アドレス: 0x100 0x104 0x108 0x10C 0x110 0x114 0x118 0x11C
値: [0][0] [0][1] [0][2] [0][3] [1][0] [1][1] [1][2] [1][3]
←──── キャッシュライン0 ────→ ←──── キャッシュライン1 ────→
アドレス: 0x120 0x124 0x128 0x12C 0x130 0x134 0x138 0x13C
値: [2][0] [2][1] [2][2] [2][3] [3][0] [3][1] [3][2] [3][3]
←──── キャッシュライン2 ────→ ←──── キャッシュライン3 ────→
行優先アクセス (row=0,1,2,3; col=0,1,2,3):
[0][0]→[0][1]→[0][2]→[0][3] → キャッシュライン0で4回ヒット
[1][0]→[1][1]→[1][2]→[1][3] → キャッシュライン1で4回ヒット
→ キャッシュミス: 4回(ライン読み込み時のみ)
→ ヒット率: 12/16 = 75%(実際にはライン=64Bで16個のintが入るため更に高い)
列優先アクセス (col=0,1,2,3; row=0,1,2,3):
[0][0]→[1][0]→[2][0]→[3][0] → 毎回異なるキャッシュライン
[0][1]→[1][1]→[2][1]→[3][1] → 毎回異なるキャッシュライン
→ Nが大きい場合、キャッシュ容量を超えてキャッシュミスが多発
8. 命令レベル並列性(ILP)
8.1 スーパースカラ実行
スーパースカラプロセッサは、1サイクルで複数の命令を同時にフェッチ・デコード・実行できる。現代の高性能CPUはほぼ全てスーパースカラ設計を採用している。
スカラ(1命令/サイクル):
Clock: 1 2 3 4 5 6
Issue: ADD MUL SUB AND OR XOR
→ 6命令に6サイクル、IPC = 1.0
2-wayスーパースカラ(2命令/サイクル):
Clock: 1 2 3
Port0: ADD SUB OR
Port1: MUL AND XOR
→ 6命令に3サイクル、IPC = 2.0
Apple M4 Firestormコア(最大10命令/サイクル):
Clock: 1
Port0: ALU命令
Port1: ALU命令
Port2: ALU命令
Port3: ALU命令
Port4: FP/SIMD命令
Port5: FP/SIMD命令
Port6: FP/SIMD命令
Port7: FP/SIMD命令
Port8: LOAD
Port9: LOAD
Port10: STORE
→ 理論上は1サイクルで最大10命令を発行可能
(依存関係がなければ)
8.2 アウトオブオーダー実行(OoO)
プログラム中の命令を記述順序どおりではなく、データ依存関係がない命令を先に実行することで、パイプラインの利用効率を高める技術。
インオーダー実行(プログラム順序通り):
命令1: a = LOAD [addr1] ; メモリアクセス: 100サイクル
命令2: b = a + 1 ; 命令1に依存 → 100サイクル待ち
命令3: c = LOAD [addr2] ; 命令1,2に無関係だが、順番を待つ
命令4: d = c * 2 ; 命令3に依存
→ 合計: ~200+ サイクル
アウトオブオーダー実行:
サイクル 1: 命令1 発行 (LOAD [addr1])
サイクル 1: 命令3 発行 (LOAD [addr2]) ← 命令1と並列に発行!
サイクル ~100: 命令1, 命令3 両方完了
サイクル ~101: 命令2 実行 (a + 1)
サイクル ~101: 命令4 実行 (c * 2) ← 命令2と並列に実行!
→ 合計: ~102 サイクル(ほぼ半分)
8.3 レジスタリネーミング
アウトオブオーダー実行を効率的に行うために、WAR(Write After Read)やWAW(Write After Write)のような偽の依存関係を排除する技術。
# レジスタリネーミングの概念を疑似コードで示す
# 元のコード(アーキテクチャレジスタ R1 を再利用)
# ADD R1, R2, R3 ; R1 = R2 + R3
# MUL R4, R1, R5 ; R4 = R1 * R5 (RAW: 真の依存)
# ADD R1, R6, R7 ; R1 = R6 + R7 (WAW: R1への再書き込み)
# SUB R8, R1, R9 ; R8 = R1 - R9 (RAW: 真の依存)
# リネーミング後(物理レジスタ P1, P2, ... を割り当て)
# ADD P10, P2, P3 ; P10 = P2 + P3 (R1 → P10)
# MUL P4, P10, P5 ; P4 = P10 * P5 (RAW依存はそのまま)
# ADD P11, P6, P7 ; P11 = P6 + P7 (R1 → P11: 別の物理レジスタ!)
# SUB P8, P11, P9 ; P8 = P11 - P9
# → 命令1,2のグループと命令3,4のグループが独立に実行可能
# → WAW依存が消え、並列度が向上
# 現代CPUの物理レジスタ数:
# Intel Skylake: アーキテクチャレジスタ16個 → 物理レジスタ180個
# Apple M1: アーキテクチャレジスタ31個 → 物理レジスタ ~300個以上
# → この差分(リネーミングバッファ)がOoO実行の「深さ」を決める8.4 投機的実行とその代償
アウトオブオーダー実行と分岐予測を組み合わせた「投機的実行」は強力な高速化手段だが、セキュリティリスクも伴う。
投機的実行のセキュリティ問題:
Spectre / Meltdown (2018年に公開) の概要:
1. 分岐予測により投機的に実行される命令が、
本来アクセスできないメモリ領域のデータを読む
2. 投機的実行は結果的に「なかったこと」にされるが、
キャッシュへの副作用(サイドチャネル)が残る
3. キャッシュのタイミング差を測定することで、
秘密データを推測できる
影響:
- ほぼ全ての現代CPU(Intel, AMD, ARM)が影響を受けた
- OS・ブラウザ・コンパイラレベルでの対策が必要に
- 対策によるパフォーマンス低下: 2-30%(ワークロードによる)
教訓:
- 性能とセキュリティはトレードオフの関係にある
- ハードウェアの最適化がソフトウェアの安全性を脅かしうる
- マイクロアーキテクチャの詳細がセキュリティに直結する時代
9. マルチコアとハイパースレッディング
9.1 マルチコアの必然性
なぜマルチコアが必要になったか:
クロック周波数の推移:
1990: 25MHz │
1995: 100MHz │ ムーアの法則に沿って
2000: 1GHz │ 周波数が上昇
2004: 3.8GHz │ ← Power Wall に到達!
2024: ~5.8GHz │ ← 20年間でわずか +50%
Power Wall(消費電力の壁):| 消費電力 ∝ 電圧^2 × 周波数 |
|---|
| 周波数を2倍にすると: |
| - 電圧も上げる必要がある (~1.1倍) |
| - 消費電力 ≈ 1.1^2 × 2 = 2.4倍 |
| - 発熱も2倍以上 → 冷却限界を超える |
| 解決策: 周波数を上げる代わりに |
| コア数を増やして並列処理する |
9.2 マルチコアの構成
典型的なマルチコアCPUの構成:| 4コアCPU | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Core 0 | Core 1 | Core 2 | Core 3 | ||||||||||||||||||||
| ┌───┐┌───┐ | ┌───┐┌───┐ | ┌───┐┌───┐ | ┌───┐┌───┐ | ||||||||||||||||||||
| L1i | L1d | L1i | L1d | L1i | L1d | L1i | L1d | ||||||||||||||||
| └───┘└───┘ | └───┘└───┘ | └───┘└───┘ | └───┘└───┘ | ||||||||||||||||||||
| ┌─────┐ | ┌─────┐ | ┌─────┐ | ┌─────┐ | ||||||||||||||||||||
| L2 | L2 | L2 | L2 | ||||||||||||||||||||
| └─────┘ | └─────┘ | └─────┘ | └─────┘ | ||||||||||||||||||||
| ┌─────────────────────────────────────────────────────┐ | |||||||||||||||||||||||
| 共有 L3 キャッシュ (8-96MB) | |||||||||||||||||||||||
| └─────────────────────────────────────────────────────┘ | |||||||||||||||||||||||
| ┌─────────────────────────────────────────────────────┐ | |||||||||||||||||||||||
| メモリコントローラ | |||||||||||||||||||||||
| └─────────────────────────────────────────────────────┘ |
│| DRAM (DDR5) |
|---|
9.3 アムダールの法則
マルチコアによる高速化には理論的な上限がある。これを定式化したのがアムダールの法則である。
アムダールの法則:
高速化比 = 1 / ((1 - P) + P/N)
P = 並列化可能な割合 (0.0 ~ 1.0)
N = プロセッサ(コア)数
例: プログラムの75%が並列化可能な場合| コア数 | 1 | 4 | 16 | ∞ |
|---|---|---|---|---|
| 高速化 | 1.00x | 2.29x | 3.16x | 4.00x |
→ どんなにコアを増やしても、逐次部分(25%)がボトルネックとなり
高速化は最大 1/(1-0.75) = 4倍 が上限
グラフ(並列化率ごとの理論上限):
高速化
20x ┤ . P=95%
│ .....
16x ┤ ....
│ .... .... P=90%
12x ┤ .... .....
│ .... ....
8x ┤ ......... ....... P=75%
│ ..... ......
4x ┤ ...................... P=50%
│ .............
2x ┤..........
│.......
1x ┼──┬──┬──┬──┬──┬──┬──┬──→ コア数
1 2 4 8 16 32 64 128
9.4 ハイパースレッディング(SMT: 同時マルチスレッディング)
ハイパースレッディング(SMT)の仕組み:
1つの物理コアに2つ(または4つ)の論理スレッドを載せる技術。
ALUやキャッシュなどの実行リソースを共有しつつ、
レジスタファイルやPC等のスレッド状態を複製する。
物理コア1個 (2-way SMT):| ┌─────────────┐ ┌─────────────┐ | ||||
|---|---|---|---|---|
| Thread 0 | Thread 1 | 複製 | ||
| (論理CPU 0) | (論理CPU 1) | 部分 | ||
| - PC | - PC | |||
| - レジスタ | - レジスタ | |||
| - TLB一部 | - TLB一部 | |||
| └──────┬──────┘ └──────┬──────┘ | ||||
| └───────┬────────┘ | ||||
| ▼ | ||||
| ┌─────────────────────────────────┐ | ||||
| 共有リソース | ||||
| - ALU / FPU / SIMD | ||||
| - L1i / L1d キャッシュ | ||||
| - L2 キャッシュ | ||||
| - デコーダ | ||||
| - リオーダーバッファ | ||||
| └─────────────────────────────────┘ |
効果:
- 一方のスレッドがメモリ待ちの間に、他方のスレッドが
ALUを使用できる → リソースの有効活用
- スループット向上: 15-30%(ワークロードによる)
- 2倍にはならない理由: 実行リソースを共有しているため
- 逆効果の場合: 両スレッドがキャッシュを競合する場合
10. Apple Siliconとヘテロジニアスコンピューティング
10.1 SoC(System on Chip)の設計思想
従来のPC構成:| CPU | ───────── | GPU |
|---|---|---|
| (Intel) | バス | (NVIDIA) |
│ DDR │ GDDR ┌────┴────┐ ┌────┴────┐
│CPU RAM │ │GPU VRAM │ ← CPUとGPUで別のメモリ
│(DDR5) │ │(GDDR6X) │ データコピーが必要
└─────────┘ └─────────┘Apple Silicon SoC:| Apple M4 Max | ||||||
|---|---|---|---|---|---|---|
| ┌──────────┐ ┌───────────┐ ┌────────────┐ | ||||||
| P-Core | E-Core | GPU | ||||
| ×12 | ×4 | 40コア | ||||
| (性能) | (効率) | |||||
| └──────────┘ └───────────┘ └────────────┘ | ||||||
| ┌──────────┐ ┌───────────┐ ┌────────────┐ | ||||||
| Neural | Media | Display | ||||
| Engine | Engine | Engine | ||||
| 16コア | H.264/5 | |||||
| (ML推論) | ProRes | |||||
| └──────────┘ └───────────┘ └────────────┘ | ||||||
| ┌─────────────────────────────────────────┐ | ||||||
| 統合メモリ (LPDDR5) | ||||||
| 最大128GB / 帯域: 546GB/s | ||||||
| CPU・GPU・NPU全てが同一メモリに | ||||||
| 直接アクセス → データコピー不要 | ||||||
| └─────────────────────────────────────────┘ |
10.2 big.LITTLE(効率コア+性能コア)
| 特性 | P-Core (Performance) | E-Core (Efficiency) |
|---|---|---|
| 目的 | 最大シングルスレッド性能 | 省電力での軽量処理 |
| クロック | 高 (~4.5GHz) | 低 (~2.8GHz) |
| パイプライン幅 | 広い(8-10命令同時デコード) | 狭い(4命令程度) |
| リオーダーバッファ | 大きい(600+エントリ) | 小さい |
| 消費電力 | 高 | 低(P-Coreの1/3〜1/5) |
| 使用場面 | コンパイル、動画編集、ゲーム | メール、ブラウジング、バックグラウンド |
OSのスケジューラ(macOSの場合はQoS: Quality of Service)がタスクの優先度と負荷に応じて、どのコアで実行するかを自動的に決定する。
11. アンチパターン
アンチパターン1: 「クロック周波数至上主義」
アンチパターン: クロック周波数だけでCPU性能を判断する
誤った思考:
「5.8GHzのCPU は 3.5GHzのCPU より 1.66倍速い」
現実:
性能 = IPC × クロック周波数 × コア数(の有効活用度)
Intel i9-14900K: 5.8GHz × IPC ~4 = 実効 ~23.2 (相対値)
Apple M4 Max: 4.5GHz × IPC ~8 = 実効 ~36.0 (相対値)
※ IPCの値は概念的な比較のための概数
なぜ間違いか:
1. IPC (Instructions Per Cycle) がアーキテクチャごとに大きく異なる
2. パイプラインの深さ、幅、分岐予測精度が違う
3. キャッシュ構成、メモリ帯域幅が違う
4. 実際のワークロードでの命令ミックスが違う
正しい評価方法:
- 実際のワークロードでのベンチマーク結果を参照する
- Geekbench, SPEC CPU, Cinebench等の標準ベンチマーク
- 対象アプリケーションでの実測が最も信頼できる
アンチパターン2: 「キャッシュ無視のデータ構造設計」
アンチパターン: メモリアクセスパターンを考慮せずにデータ構造を選択する
誤った設計(AoS: Array of Structures):| struct Particle { |
|---|
| float x, y, z; // 位置 (12B) |
| float vx, vy, vz; // 速度 (12B) |
| float mass; // 質量 (4B) |
| int type; // 種類 (4B) |
| char name[32]; // 名前 (32B) |
| }; // 合計: 64バイト |
| Particle particles[10000]; |
| // 全粒子の位置だけを更新する場合: |
| for (int i = 0; i < 10000; i++) { |
| particles[i].x += particles[i].vx * dt; |
| // 64バイトのうち4+4=8バイトしか使わない |
| // キャッシュラインの87.5%が無駄 |
| } |
正しい設計(SoA: Structure of Arrays):| struct ParticleSystem { |
|---|
| float *x, *y, *z; // 位置の配列 |
| float *vx, *vy, *vz; // 速度の配列 |
| float *mass; // 質量の配列 |
| int *type; // 種類の配列 |
| }; |
| // 位置の更新: |
| for (int i = 0; i < 10000; i++) { |
| ps.x[i] += ps.vx[i] * dt; |
| // x[] と vx[] は連続メモリ |
| // キャッシュラインを100%活用 |
| } |
| // さらにSIMD (AVX) で4個同時処理可能: |
| // __m128 vx4 = _mm_load_ps(&ps.vx[i]); |
| // __m128 dx = _mm_mul_ps(vx4, dt4); |
| // __m128 x4 = _mm_add_ps(..., dx); |
性能差: SoAはAoSに対して2〜8倍高速になることがある
(特にSIMD最適化と組み合わせた場合)
アンチパターン3: 「不必要な条件分岐の多用」
アンチパターン: ループ内で予測困難な条件分岐を多用する
問題のあるコード:| for (int i = 0; i < N; i++) { |
|---|
| if (data[i] > threshold) { |
| result[i] = func_a(data[i]); |
| } else if (data[i] > threshold2) { |
| result[i] = func_b(data[i]); |
| } else { |
| result[i] = func_c(data[i]); |
| } |
| } |
| // dataがランダムな場合、分岐予測ミスが多発 |
改善策:
1. データを事前にソートして分岐パターンを予測しやすくする
2. 関数ポインタテーブル(ルックアップテーブル)を使う
3. SIMD命令でブランチレスに処理する
4. データを分類別にバケットに分けてから処理する
12. 実践演習
演習1: 命令サイクルのトレース(基礎レベル)
以下の疑似アセンブリコードについて、5段パイプライン(IF, ID, EX, MEM, WB)の各段階で何が起きるか、パイプラインダイアグラムを記述せよ。
LOAD R1, 100 ; メモリアドレス100の値をR1にロード
LOAD R2, 104 ; メモリアドレス104の値をR2にロード
ADD R3, R1, R2 ; R3 = R1 + R2
STORE R3, 108 ; R3の値をメモリアドレス108にストア
課題:
- ハザードなしの場合のパイプラインダイアグラムを描け
- データハザードが発生する箇所を特定せよ
- フォワーディングありの場合、何サイクルで完了するか計算せよ
解答の指針:
- ADD命令はLOAD命令の結果(R1, R2)に依存する(Load-Useハザード)
- フォワーディングがあってもLOAD→使用は1サイクルのストールが必要
- STORE命令はADD命令の結果(R3)に依存する
演習2: キャッシュ性能の分析(中級レベル)
以下の条件でキャッシュヒット率を計算せよ。
条件:
- L1dキャッシュ: 32KB, 8ウェイセットアソシアティブ, ラインサイズ64B
- アクセスパターン: int配列(4B要素)のシーケンシャルアクセス
- 配列サイズ: 8192要素 (32KB)
問1: 1回のシーケンシャルスキャンでのキャッシュヒット率を計算せよ
問2: 同じ配列を2回連続スキャンした場合の2回目のヒット率は?
問3: 配列サイズが64KB(キャッシュの2倍)の場合はどうなるか?
解答の指針:
- 1キャッシュラインに int が 64/4 = 16個収まる
- 初回アクセスで必ずミス → 以降15回はヒット → コールドミス率 = 1/16
- 配列全体がキャッシュに収まるかどうかが2回目のヒット率を左右する
演習3: マルチコア性能の見積もり(上級レベル)
アムダールの法則を用いて、以下のプログラムの並列化効果を見積もれ。
プログラムの構成:
- データ読み込み: 全体の10%(逐次処理のみ)
- 前処理: 全体の15%(80%が並列化可能)
- メイン計算: 全体の60%(95%が並列化可能)
- 後処理: 全体の10%(70%が並列化可能)
- 結果出力: 全体の 5%(逐次処理のみ)
問1: 全体の並列化可能割合 P を計算せよ
問2: 4コア、8コア、64コアでの理論的高速化比を計算せよ
問3: 並列化のオーバーヘッド(通信コスト、同期コスト)を考慮した場合、
現実的な高速化比はどの程度になると推定されるか論じよ
解答の指針:
- P = 0.10×0 + 0.15×0.80 + 0.60×0.95 + 0.10×0.70 + 0.05×0
- P = 0 + 0.12 + 0.57 + 0.07 + 0 = 0.76
- アムダールの法則: Speedup = 1 / ((1-P) + P/N)
13. FAQ
Q1: クロック周波数が高い = 速いCPUですか?
A: 必ずしもそうではない。CPUの性能は「IPC(1サイクルで実行できる命令数)× クロック周波数 × コア数の有効活用度」で総合的に決まる。Apple M4(約3.5-4.5GHz)がIntel i9(約5.8GHz)よりシングルスレッド性能で上回る場面があるのは、IPCが大幅に高いためである。パイプラインの幅(同時デコード・発行できる命令数)、分岐予測精度、キャッシュ容量と帯域幅、メモリレイテンシなどが総合的に性能を決定する。
Q2: x86はなぜまだ使われているのですか?
A: 最大の理由は後方互換性である。40年以上にわたって蓄積されたx86バイナリ形式のソフトウェア資産が膨大に存在し、それらを再コンパイルなしに動作させ続ける必要がある。特に企業のレガシーシステムや、Windows向けの商用ソフトウェアの多くがx86バイナリである。ただし、サーバー分野ではAWS Graviton(ARM)やAmpere Altraなどへの移行が進行中であり、デスクトップではApple SiliconによるMacのARM移行が完了した。x86は内部的にRISC風のマイクロオペレーション(uop)に変換して実行しているため、ISAレベルの複雑さが必ずしも実行効率の低下には直結しない。
Q3: RISC-Vは何が画期的ですか?
A: RISC-Vはオープンソースの命令セットアーキテクチャ(ISA)であり、ライセンス料なしで誰でもRISC-V準拠のCPUを設計・製造できる点が画期的である。ARMはISAライセンスに数百万〜数千万ドルの費用がかかるが、RISC-Vにはそれがない。さらに、モジュラー設計により、用途に応じて必要な拡張(乗除算、浮動小数点、ベクトル演算、カスタムAI命令等)を選択的に実装できる。Linux、GCC、LLVMが公式にサポートしており、エコシステムも急速に成長している。2024年時点で100億個以上のRISC-Vチップが出荷済みであり、IoT・組込みから将来的にはサーバー市場まで幅広い展開が期待されている。
Q4: キャッシュミスが発生するとどのくらい遅くなりますか?
A: アクセスするメモリ階層によって大きく異なる。L1キャッシュヒットが約4サイクル(約1ns)なのに対し、L2ミスでL3にアクセスすると約40サイクル(約10ns)、L3ミスでDRAMにアクセスすると約100-200サイクル(約50-100ns)かかる。つまり、L1ヒットとDRAMアクセスでは25-50倍の差がある。プログラムのホットループ内でキャッシュミスが頻発すると、CPUのパイプラインが大部分の時間をメモリ待ちで浪費することになり、見た目のクロック周波数からは想像できないほど性能が低下する。
Q5: パイプラインが深いとなぜ分岐予測ミスのペナルティが大きくなるのですか?
A: パイプラインの深さ(段数)は、分岐命令がフェッチされてから分岐先が確定するまでのサイクル数に直結する。例えば5段パイプラインなら分岐ミスのペナルティは2-3サイクル程度だが、20段パイプラインでは10-20サイクルに達する。分岐先が確定するまでに投機的に実行された命令は全て破棄(フラッシュ)しなければならず、その間のパイプラインは空になる。Intel Pentium 4のNetBurstアーキテクチャ(31段)が深すぎるパイプラインの弊害を示した歴史的な事例として知られている。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
14. まとめ
| 概念 | ポイント |
|---|---|
| 命令サイクル | Fetch→Decode→Execute→(Memory)→WriteBackの4-5段階 |
| ISA | ソフトウェアとハードウェアのインターフェース。CISC/RISCの設計哲学が存在 |
| パイプライン | 命令の各段階を重ね合わせてスループット向上。深さはトレードオフ |
| ハザード | データ/制御/構造の3種。フォワーディングと分岐予測で対処 |
| 分岐予測 | 動的予測(2ビットカウンタ、TAGE等)で97%以上の精度。ミスは10-20サイクルのペナルティ |
| CISC vs RISC | x86(複雑・互換性) vs ARM/RISC-V(単純・省電力)。現代は収束傾向 |
| キャッシュ | L1/L2/L3の階層構造。空間的・時間的局所性を活用。メモリアクセスパターンが性能を決定 |
| ILP | スーパースカラ、OoO実行、レジスタリネーミングで命令レベル並列性を引き出す |
| マルチコア | Power Wallの解決策。アムダールの法則で高速化に理論的上限あり |
| SMT | 1物理コアで複数論理スレッド。15-30%のスループット向上 |
| Apple Silicon | SoC設計、統合メモリ、big.LITTLE。電力効率と性能の両立 |
| セキュリティ | Spectre/Meltdown: 投機的実行がサイドチャネル攻撃を生む |
次に読むべきガイド
参考文献
- Patterson, D. A. & Hennessy, J. L. Computer Organization and Design: The Hardware/Software Interface. 6th Edition, Morgan Kaufmann, 2020. -- 命令サイクル、パイプライン、RISC設計の基礎を体系的に学べるコンピュータアーキテクチャの定番教科書。
- Hennessy, J. L. & Patterson, D. A. Computer Architecture: A Quantitative Approach. 6th Edition, Morgan Kaufmann, 2017. -- キャッシュ階層、分岐予測、ILP、マルチプロセッサなど、定量的な性能分析手法を扱う上級者向けの教科書。
- Bryant, R. E. & O'Hallaron, D. R. Computer Systems: A Programmer's Perspective. 3rd Edition, Pearson, 2015. -- プログラマーの視点からメモリ階層、キャッシュ最適化、プロセッサの動作原理を解説した実践的なテキスト。
- Intel Corporation. Intel 64 and IA-32 Architectures Software Developer Manuals. https://www.intel.com/ -- x86-64アーキテクチャの公式仕様書。命令セット、マイクロアーキテクチャの詳細な技術文書。
- ARM Limited. ARM Architecture Reference Manual (ARMv8-A). https://developer.arm.com/ -- AArch64(ARM 64ビット)アーキテクチャの公式仕様書。命令セット、例外処理モデル、メモリモデルを網羅。
- Fog, A. The Microarchitecture of Intel, AMD and VIA CPUs. https://agner.org/optimize/ -- Intel・AMDプロセッサのマイクロアーキテクチャ(パイプライン構成、実行ユニット、分岐予測器)を詳細に分析した技術文書。
- RISC-V International. The RISC-V Instruction Set Manual. https://riscv.org/technical/specifications/ -- RISC-Vの公式ISA仕様書。基本整数命令セットから各種標準拡張まで定義している。
- Kocher, P. et al. "Spectre Attacks: Exploiting Speculative Execution." IEEE Symposium on Security and Privacy, 2019. -- 投機的実行のサイドチャネル脆弱性を報告した論文。現代CPUのセキュリティ問題を理解する上で重要。