Skip to content
Published on

コンピュータアーキテクチャ完全ガイド: ISAからGPU並列アーキテクチャまで

Authors

はじめに

コンピュータアーキテクチャは、ハードウェアとソフトウェアの境界を理解するための基礎的な学問分野です。電子・情報工学の学生、システムプログラマ、半導体設計者にとって、プロセッサの動作原理を深く理解することは不可欠です。

このガイドはPatterson & HennessyのComputer Organization and DesignおよびComputer Architecture: A Quantitative Approachを基礎とし、ISA設計から現代のGPU並列アーキテクチャまでを体系的に解説します。


1. コンピュータアーキテクチャの概要

フォン・ノイマンアーキテクチャ

1945年にJohn von Neumannが提案したフォン・ノイマンアーキテクチャは、現代コンピュータの基礎です。プログラムのコードとデータを同じメモリ空間に格納するのが特徴です。

構成要素:

  • CPU (Central Processing Unit): ALU + 制御ユニット + レジスタ
  • メモリ: プログラムコードとデータを格納
  • 入出力装置: キーボード、ディスプレイ、ディスクなど
  • バス: データ・アドレス・制御信号を伝達

フォン・ノイマンボトルネック: CPUとメモリ間のバス帯域幅が性能を制限します。現代のキャッシュ階層は、このボトルネックを緩和するために設計されています。

ハーバードアーキテクチャ

ハーバードアーキテクチャは命令メモリとデータメモリを分離し、同時アクセスを可能にします。DSP、マイクロコントローラ(AVR、PIC)、および現代CPUのL1キャッシュ(I-Cache/D-Cache分離)に使用されます。

項目フォン・ノイマンハーバード
メモリ統合分離
帯域幅制限あり高い
複雑さ低い高い
用途汎用CPUDSP、マイコン

コンピュータの抽象化階層

アプリケーションソフトウェア
オペレーティングシステム
ISA (命令セットアーキテクチャ)  ← ハードウェア/ソフトウェア境界
マイクロアーキテクチャ
論理ゲート
トランジスタ

ISAはハードウェアとソフトウェアの契約です。同じISAを実装するプロセッサであれば、内部のマイクロアーキテクチャに関わらず同じソフトウェアが動作します。

性能測定指標

CPU実行時間の公式:

CPU時間 = 命令数 × CPI × クロック周期
        = 命令数 × CPI / クロック周波数
  • CPI (Cycles Per Instruction): 命令1つあたりの平均クロックサイクル数
  • クロック周波数: Hz単位 (例: 3.5 GHz)
  • MIPS (Millions of Instructions Per Second): 毎秒実行命令数 (百万単位)
  • FLOPS (Floating Point Operations Per Second): 浮動小数点演算性能

アムダールの法則 (並列化の限界):

スピードアップ = 1 / ((1 - f) + f/s)

fは並列化可能な割合、sは並列化部分の高速化倍率です。プログラムの40%しか並列化できない場合、コア数をいくら増やしても最大1.67倍が限界です。


2. 命令セットアーキテクチャ (ISA)

RISC と CISC

RISC (Reduced Instruction Set Computer):

  • シンプルで固定長の命令
  • レジスタ中心の演算 (ロード/ストアアーキテクチャ)
  • ハードウェアパイプラインに有利
  • 代表例: ARM、RISC-V、MIPS、PowerPC

CISC (Complex Instruction Set Computer):

  • 複雑で可変長の命令
  • メモリへの直接演算が可能
  • コード密度が高い
  • 代表例: x86-64、VAX

現代のx86-64プロセッサは内部でCISC命令をRISC的なマイクロ命令(micro-ops)に変換して実行するため、実際の境界は曖昧です。

命令形式 (RISC-V基準)

RISC-Vは6種類の命令形式を使用します:

R形式 (レジスタ演算):
[funct7 | rs2 | rs1 | funct3 | rd | opcode]
  7ビット 5ビット 5ビット 3ビット 5ビット 7ビット

I形式 (即値/ロード):
[imm[11:0] | rs1 | funct3 | rd | opcode]
  12ビット   5ビット 3ビット 5ビット 7ビット

S形式 (ストア):
[imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode]

B形式 (分岐):
[imm[12|10:5] | rs2 | rs1 | funct3 | imm[4:1|11] | opcode]

U形式 (上位即値):
[imm[31:12] | rd | opcode]

J形式 (JAL):
[imm[20|10:1|11|19:12] | rd | opcode]

RISC-V アセンブリ例

RISC-VはUC Berkeleyで開発されたオープンソースISAで、教育・産業両方で急速に普及しています。

# RISC-V アセンブリ例
# 基本的な算術演算
add  a0, a0, a1      # a0 = a0 + a1
sub  t0, t1, t2      # t0 = t1 - t2
addi t0, t0, 10      # t0 = t0 + 10 (即値)
mul  a0, a1, a2      # a0 = a1 * a2

# メモリアクセス (ロード/ストア)
lw   t0, 0(a0)       # t0 = Memory[a0 + 0]   (ワードロード)
lh   t1, 2(a0)       # t1 = Memory[a0 + 2]   (ハーフワードロード)
lb   t2, 1(a0)       # t2 = Memory[a0 + 1]   (バイトロード)
sw   t0, 4(a0)       # Memory[a0 + 4] = t0   (ワードストア)

# 論理演算
and  t0, t1, t2      # t0 = t1 & t2
or   t0, t1, t2      # t0 = t1 | t2
xor  t0, t1, t2      # t0 = t1 ^ t2
sll  t0, t1, t2      # t0 = t1 << t2 (論理左シフト)
srl  t0, t1, t2      # t0 = t1 >> t2 (論理右シフト)

# 分岐とジャンプ
beq  t0, t1, label   # t0 == t1 なら label へ
bne  t0, t1, label   # t0 != t1 なら label へ
blt  t0, t1, label   # t0 < t1  なら label へ
bge  t0, t1, label   # t0 >= t1 なら label へ
jal  ra, func        # ra = PC+4; func へジャンプ
jalr ra, 0(t0)       # ra = PC+4; t0+0 へジャンプ

# RISC-V レジスタ規約
# x0  (zero): 常に0
# x1  (ra):   リターンアドレス
# x2  (sp):   スタックポインタ
# x10-x17 (a0-a7): 関数引数/戻り値
# x5-x7, x28-x31 (t0-t6): 一時レジスタ
# x8-x9, x18-x27 (s0-s11): 保存レジスタ

C関数をRISC-Vアセンブリへ変換

// Cコード
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
# RISC-V アセンブリ (再帰的階乗)
factorial:
    addi sp, sp, -16     # スタックフレーム確保
    sw   ra, 12(sp)      # リターンアドレス保存
    sw   a0, 8(sp)       # n 保存

    addi t0, zero, 1
    bgt  a0, t0, recurse # n > 1 なら再帰
    addi a0, zero, 1     # return 1
    j    done

recurse:
    addi a0, a0, -1      # n - 1
    jal  ra, factorial   # 再帰呼び出し
    lw   t0, 8(sp)       # n 復元
    mul  a0, t0, a0      # n * factorial(n-1)

done:
    lw   ra, 12(sp)      # リターンアドレス復元
    addi sp, sp, 16      # スタックフレーム解放
    jalr zero, 0(ra)     # リターン

3. ALUとデータパス

ALU 設計

ALU (算術論理演算装置) はCPUの演算コアです:

ADD:  結果 = A + B
SUB:  結果 = A + (~B + 1) = A - B  (2の補数)
AND:  結果 = A AND B
OR:   結果 = A OR B
XOR:  結果 = A XOR B
SLT:  結果 = (A < B) ? 1 : 0  (Set Less Than)
SLL:  結果 = A << B

リップルキャリー加算器:

FA0: S0 = A0 XOR B0 XOR Cin;  C1 = キャリーアウト
FA1: S1 = A1 XOR B1 XOR C1;  C2 = キャリーアウト
...

Nビットのリップルキャリー加算器の遅延はO(N)です。64ビット加算には64段のゲート遅延が発生します。

先行キャリー加算器 (CLA):

生成(Generate)と伝播(Propagate)を定義します:

Gi = Ai AND Bi       (キャリー生成)
Pi = Ai XOR Bi       (キャリー伝播)
Ci+1 = Gi OR (Pi AND Ci)

4ビットグループ単位でキャリーを並列計算すると、遅延をO(log N)に削減できます。

シングルサイクルデータパス

シングルサイクル実装では、全ての命令が1クロックサイクルで完了します。

命令の流れ (R形式 ADD):
1. IF:  PC → 命令メモリ → 命令フェッチ
2. ID:  レジスタファイルからrs1, rs2を読み出し、制御信号生成
3. EX:  ALUでrs1 + rs2を計算
4. MEM: (R形式はメモリアクセスなし)
5. WB:  ALU結果をrdに書き込み

問題点: 最も遅い命令(メモリロード等)がクロック周期を決定します。高速な命令も遅いクロックに合わせる必要があります。


4. パイプライン処理

パイプライン処理は洗濯機・乾燥機・アイロンを同時に使うように、複数命令の異なるステージを重ねて実行する技法です。

5段パイプライン

ステージ  略称  役割
--------+------+--------------------------------------------
フェッチ  IF   PCからメモリの命令を取得
デコード  ID   命令デコード、レジスタ読み出し、制御信号生成
実行      EX   ALU演算実行
メモリ   MEM   データメモリの読み書き
書き込み  WB   レジスタファイルへ結果を書き込み

タイミング図:

サイクル: 1    2    3    4    5    6    7    8
命令 1:  IF   ID   EX  MEM   WB
命令 2:       IF   ID   EX  MEM   WB
命令 3:            IF   ID   EX  MEM   WB
命令 4:                 IF   ID   EX  MEM   WB

理想的には、5段パイプラインはシングルサイクルに対して最大5倍のスループット向上を実現します。

パイプラインハザード

1. 構造的ハザード (Structural Hazard)

2つの命令が同時に同じハードウェアリソースを使用しようとするときに発生します。

  • 解決策: リソースの複製 (I-CacheとD-Cacheの分離)、レジスタポートの追加

2. データハザード (Data Hazard)

前の命令の結果が完成する前に、次の命令がその値を必要とするときに発生します。

add t0, t1, t2    # t0 書き込み (WB: 5サイクル目)
sub t3, t0, t4    # t0 読み出し (ID: 3サイクル目) → RAW ハザード!

種類:

  • RAW (Read After Write): 最も一般的。書き込み完了前に読み出し。
  • WAR (Write After Read): アウトオブオーダー実行で発生。
  • WAW (Write After Write): 2命令が同じレジスタに書き込む。

解決方法:

  • フォワーディング (バイパッシング): EX/MEMステージの結果を次のEXステージ入力に直接転送。ストールなしで解決。
  • ストール (バブル挿入): NOPサイクルを挿入してパイプラインを一時停止。性能低下。
  • コード再配置: コンパイラが独立した命令を間に挿入。
# ロード・ユースハザード (1サイクルストール不可避)
lw  t0, 0(a0)      # メモリからロード
add t1, t0, t2     # t0 をすぐ使用 → MEM→EXフォワーディング不可
# 解決策: コンパイラが独立命令を挿入
lw  t0, 0(a0)
add t3, t4, t5     # 独立した命令を挿入
add t1, t0, t2     # t0 が準備完了

3. 制御ハザード (Control Hazard)

分岐命令により次に実行すべき命令が不確かになるときに発生します。

beq t0, t1, label  # 分岐判定はEXステージで完了
# その間にIF/IDでフェッチされた命令は正しいか?

解決方法:

  • フラッシュ: 誤ってフェッチした命令を破棄 (2-3サイクルのペナルティ)
  • 分岐予測 (Branch Prediction):
    • 静的予測: 常にNot-Takenまたは常にTaken
    • 動的予測: 1ビット/2ビット予測器、BTB (Branch Target Buffer)
    • 現代CPUは95%以上の予測精度を実現
  • 遅延分岐 (Delayed Branch): 分岐命令直後のスロットに常に有効な命令を配置 (MIPS)

スーパースカラーパイプライン

現代のCPUは複数のパイプラインを並列に実行します:

Intel Core: 6-way アウトオブオーダー実行 (OoO)
AMD Zen 4:  4-way デコード + OoO実行
ARM Cortex-X4: 5-way デコード

アウトオブオーダー実行 (OoO):

  1. 命令を順番にフェッチ・デコード
  2. オペランドが準備できた命令から実行 (トマスロアルゴリズム)
  3. 結果はプログラム順に確定 (リオーダーバッファ使用)

5. メモリ階層構造

メモリ階層

レジスタ
  容量: ~1KB  |  レイテンシ: 1サイクル    |  コスト: 非常に高

L1キャッシュ (オンチップ)
  容量: 32-64KB  |  レイテンシ: 4-5サイクル  |  コスト:
L2キャッシュ (オンチップ)
  容量: 256KB-1MB  |  レイテンシ: 12-15サイクル  |  コスト:
L3キャッシュ (オンチップ, 共有)
  容量: 8-64MB  |  レイテンシ: 30-40サイクル  |  コスト:
DRAM (主記憶)
  容量: 8-256GB  |  レイテンシ: 200-300サイクル  |  コスト: 非常に低

SSD / NVMe
  容量: 1-4TB  |  レイテンシ: 10,000+サイクル  |  コスト: 極めて低

局所性の原理

  • 時間的局所性 (Temporal Locality): 最近アクセスしたデータは近いうちに再アクセスされる可能性が高い。(ループ変数、カウンタ)
  • 空間的局所性 (Spatial Locality): アクセスしたデータの近くのデータも近いうちにアクセスされる。(配列の連続アクセス)
// 空間的局所性の最適化例
// 悪い例: 列優先アクセス (キャッシュミスが多発)
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += A[i][j];  // A[0][0], A[1][0], A[2][0]... (行を飛び越す)

// 良い例: 行優先アクセス (キャッシュフレンドリー)
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += A[i][j];  // A[0][0], A[0][1], A[0][2]... (連続メモリ)

キャッシュ構造

ダイレクトマップキャッシュ: メモリの各ブロックがキャッシュの1箇所にのみマップされます。

  • 長所: シンプルで高速
  • 短所: コンフリクトミス (Conflict Miss) が発生しやすい

Nウェイセット連想キャッシュ: メモリブロックがN個のスロットのいずれかに格納可能。

  • 実際のCPUで最もよく使用 (L1: 4ウェイ、L2: 8ウェイ、L3: 16ウェイ)

フル連想キャッシュ: メモリブロックがどのスロットにも格納可能。

  • 長所: コンフリクトミスなし
  • 短所: 並列検索のコストが高い (TLBに使用)

キャッシュアドレス分解 (32ビットアドレス、4KBキャッシュ、64Bブロック、4ウェイ):

[タグ (20ビット) | インデックス (6ビット) | オフセット (6ビット)]

交換ポリシー

  • LRU (Least Recently Used): 最も長く未使用のブロックを交換。性能最高、実装複雑。
  • FIFO (First In First Out): 最初に入ったブロックを交換。実装シンプル。
  • Random: ランダムに交換。ハードウェア実装が容易で、実際の性能はLRUに近い。

書き込みポリシー

  • ライトスルー (Write-through): キャッシュとメモリを同時に更新。一貫性保証、帯域幅消費大。
  • ライトバック (Write-back): キャッシュのみ更新し、退出時にメモリへ書き込み (Dirty bit使用)。性能良好、一貫性管理が複雑。

6. 仮想メモリ

概要

仮想メモリは各プロセスに独立したアドレス空間を提供し、保護・隔離・メモリのオーバーコミットを可能にします。

仮想アドレス (VA): プロセスが使用するアドレス (0x00000xFFFFFFFF)
物理アドレス (PA): 実際のDRAMのアドレス
ページ:           仮想/物理メモリの固定サイズブロック (通常 4KB)

ページテーブルによる変換

仮想アドレス [VPN | ページオフセット]
            ページテーブル参照
物理アドレス [PFN | ページオフセット]

64ビットシステムでは4段ページテーブルを使用します (x86-64):

[PML4 (9ビット) | PDPT (9ビット) | PD (9ビット) | PT (9ビット) | オフセット (12ビット)]

TLB (Translation Lookaside Buffer)

ページテーブルの参照はメモリアクセスを追加で必要とします。TLBは最近の変換結果をキャッシュするフル連想キャッシュです。

TLBヒット:  仮想アドレス → TLB参照 → 物理アドレス (1-2サイクル)
TLBミス:    仮想アドレス → ページテーブルウォーク → 物理アドレス (100+サイクル)

TLBサイズ: 通常64-1024エントリ。99%以上のヒット率を維持することが重要。

ページ交換アルゴリズム

物理メモリが満杯のとき、どのページをディスクに退去させるかを決定します。

  • Optimal: 将来最も長く使用されないページを交換 (理論的最適、実装不可)
  • LRU: 最も長く未使用のページを交換 (性能良好、実装コスト高)
  • Clock (FIFO + 第2チャンス): 多くのOSカーネルで使用されるLRUの近似

7. 入出力システム

I/O 制御方法

ポーリング: CPUが定期的にデバイスの状態レジスタを確認します。

  • 長所: シンプル、低レイテンシ
  • 短所: CPUサイクルの無駄遣い (ビジーウェイト)

割り込み (Interrupt): デバイスが準備完了時にCPUへ信号を送ります。

  • 長所: CPUが他の処理を実行可能
  • 短所: 割り込み処理のオーバーヘッド

DMA (Direct Memory Access): DMAコントローラがCPUを介さずメモリ・デバイス間のデータ転送を直接実行します。

  • 大量データ転送に必須 (ディスク、ネットワーク、GPU)
  • CPUは転送の開始と完了のみを処理

バスアーキテクチャ

PCIe 5.0:   x16 スロット = 128 GB/s 双方向
NVMe (PCIe): 最大 7 GB/s 順次読み取り (Gen4)
USB 4.0:    最大 40 Gbps
DDR5-6400:  最大 51.2 GB/s (チャネル当たり)

8. 並列アーキテクチャ

フリンの分類

分類命令ストリームデータストリーム
SISD単一単一シングルコアCPU
SIMD単一複数GPU、AVXベクトル演算
MISD複数単一フォールトトレラントシステム
MIMD複数複数マルチコアCPU、クラスタ

マルチコアとキャッシュコヒーレンシ

複数のコアが同じキャッシュラインのコピーを持つ場合、一貫性を維持する必要があります。

MESI プロトコル (Modified, Exclusive, Shared, Invalid):

Modified (M):  このコアのみが変更済みの最新コピーを保有
Exclusive (E): このコアのみが保有し、メモリと一致
Shared (S):    複数コアが読み取り専用コピーを保有
Invalid (I):   無効 (別のコアが変更した)

状態遷移:

  • コアAがShared状態のデータを書き込み → AはModifiedに遷移、他はInvalidに無効化
  • コアBがInvalid状態のデータを読み出し → バーススヌーピングでAから転送

OpenMP 並列プログラミング

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

// 並列配列合計
int main() {
    int n = 1000000;
    long long sum = 0;
    int *arr = malloc(n * sizeof(int));

    for (int i = 0; i < n; i++)
        arr[i] = i + 1;

    // reduction句でスレッドセーフな並列合計
    #pragma omp parallel for reduction(+:sum) schedule(static)
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }

    printf("Sum = %lld\n", sum);  // 期待値: 500000500000

    // スレッドID表示
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        printf("Thread %d of %d\n", tid, nthreads);
    }

    free(arr);
    return 0;
}
// キャッシュフレンドリーな並列行列積
void matmul(float *A, float *B, float *C, int N) {
    #pragma omp parallel for collapse(2) schedule(dynamic, 64)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            float sum = 0.0f;
            for (int k = 0; k < N; k++) {
                sum += A[i*N + k] * B[k*N + j];
            }
            C[i*N + j] = sum;
        }
    }
}

NUMAアーキテクチャ

NUMA (Non-Uniform Memory Access) は各CPUソケットがローカルメモリを持つ構造です。

ソケット0 (コア0-15)  ←→ ローカルDRAM (64GB)  [~80ns]
QPI / Infinity Fabric
ソケット1 (コア16-31) ←→ ローカルDRAM (64GB) [~80ns]

ソケット0からソケット1のメモリへのアクセス: ~160ns (2倍のレイテンシ)

numactlを使用してプロセスを特定のNUMAノードに固定できます。


9. GPUアーキテクチャ

GPU vs CPU の設計哲学

項目CPUGPU
コア数数十個数千〜数万個
コアの複雑さ非常に複雑 (OoO、分岐予測)シンプル
キャッシュサイズ大きく多段階小さくシンプル
設計目標シングルスレッドのレイテンシ最小化スループット最大化
用途汎用逐次処理大規模並列計算

SIMT 実行モデル

SIMT (Single Instruction, Multiple Thread) はGPUのコア実行モデルです。

ワープ (Warp): 32スレッドのグループ (NVIDIA)
32スレッドが同一命令を同時実行
→ 各スレッドは異なるデータに対して処理

ワープスケジューラ: メモリアクセス等で遅延発生時に即座に別ワープへ切り替え
→ 数千のワープを高速に切り替えることでレイテンシを隠蔽

NVIDIA GPU の階層構造

GPU
├── GPC (Graphics Processing Cluster) x 8
│   └── SM (Streaming Multiprocessor) x 7-12
│       ├── CUDAコア x 128 (FP32演算)
│       ├── テンソルコア x 4 (行列演算、AI)
│       ├── RTコア x 1 (レイトレーシング)
│       ├── ワープスケジューラ x 4
│       ├── レジスタファイル (256KB)
│       └── 共有メモリ / L1キャッシュ (128-256KB)
└── L2キャッシュ (共有、数十MB)

NVIDIA H100 (Hopper, 2022):

  • 132個のSM、16,896個のCUDAコア
  • 528個のテンソルコア (第4世代、FP8対応)
  • 80GB HBM3メモリ、3.35 TB/sメモリ帯域幅
  • 4 PetaFLOPS FP8テンソルコア性能

CUDA プログラミング入門

#include <cuda_runtime.h>
#include <stdio.h>

// GPUカーネル: ベクトル加算
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
    // グローバルスレッドインデックスの計算
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    int n = 1 << 20;  // 100万要素
    size_t size = n * sizeof(float);

    // ホスト (CPU) メモリ
    float *h_a = (float*)malloc(size);
    float *h_b = (float*)malloc(size);
    float *h_c = (float*)malloc(size);

    // デバイス (GPU) メモリ
    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, size);
    cudaMalloc(&d_b, size);
    cudaMalloc(&d_c, size);

    // ホスト → デバイスへ転送
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);

    // カーネル実行: 1ブロックあたり256スレッド
    int threadsPerBlock = 256;
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);

    // デバイス → ホストへ転送
    cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);

    cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
    free(h_a); free(h_b); free(h_c);
    return 0;
}

共有メモリを使ったGPUメモリ最適化

// タイリングを使った行列積の最適化
#define TILE_SIZE 16

__global__ void matmulShared(float *A, float *B, float *C, int N) {
    __shared__ float tileA[TILE_SIZE][TILE_SIZE];
    __shared__ float tileB[TILE_SIZE][TILE_SIZE];

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;

    for (int t = 0; t < N / TILE_SIZE; t++) {
        // タイルを共有メモリへロード
        tileA[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
        tileB[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        __syncthreads();  // 全スレッドのロード完了を待機

        for (int k = 0; k < TILE_SIZE; k++)
            sum += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
        __syncthreads();
    }

    if (row < N && col < N)
        C[row * N + col] = sum;
}

テンソルコアとAI加速

テンソルコアは行列積-累積演算 (MMA: Matrix Multiply-Accumulate) をハードウェアで高速化します。

通常のCUDAコア: 1サイクルあたり1 FP32 MAC
4世代テンソルコア: 16x16x16の行列積 = 1サイクルあたり4096 FP16 MAC

NVIDIA cuBLAS、cuDNN、TensorRTはデータサイズが適合する場合にテンソルコアを自動的に活用します。


10. 最新アーキテクチャのトレンド

チップレット設計

一枚の大型ダイ (モノリシックダイ) の代わりに、複数の小さなチップレットをインターポーザーで接続します。

  • AMD EPYC (Genoa): 12個のCCD (Core Complex Die, 5nm) + 1個のIOD (I/O Die, 6nm)
  • Intel Meteor Lake: CPU・GPU・SoCタイルを分離
  • 利点: 歩留まり向上、プロセスの最適化、コスト削減
  • 技術: TSMC CoWoS、Intel EMIB、UCIe標準

HBM (High Bandwidth Memory)

DRAMを3D積層することで超高帯域幅を実現します。

HBM3E (2024): 9.6 Gbps/ピン、8スタック = パッケージ当たり 1.2 TB/s
GDDR6X (RTX 4090): 21 Gbps/ピン、384ビット = 1 TB/s
DDR5-6400: チャネル当たり 51.2 GB/s (CPU)

NPU/TPU: AI専用アクセラレータ

  • Google TPU v5p: 459 TFLOPS BF16、メッシュ相互接続
  • Apple Neural Engine: iPhone 15 Pro、35 TOPS INT8
  • Qualcomm Hexagon: スマートフォンNPU、75 TOPS
  • Intel Gaudi 3: 1835 TFLOPS BF16

RISC-Vの台頭

オープンソースISAとして低消費電力組み込みからデータセンターサーバーまで拡大中:

  • ベンダー: SiFive、StarFive、Alibaba T-Head
  • RISC-V International: 3,000社以上のメンバー
  • Linux 5.19 公式サポート、Androidポーティング完了

11. 性能最適化の実践

キャッシュフレンドリーなプログラミング

#include <stdlib.h>

#define N 4096

float A[N][N], B[N][N], C[N][N];

// 非効率: 列優先アクセス (キャッシュミスが多発)
void matmul_naive() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; k++)
                C[i][j] += A[i][k] * B[k][j];  // B[k][j]が非連続アクセス
}

// 効率的: ブロックタイリング (キャッシュを再利用)
#define BLOCK 64
void matmul_tiled() {
    for (int ii = 0; ii < N; ii += BLOCK)
        for (int jj = 0; jj < N; jj += BLOCK)
            for (int kk = 0; kk < N; kk += BLOCK)
                for (int i = ii; i < ii+BLOCK && i < N; i++)
                    for (int j = jj; j < jj+BLOCK && j < N; j++)
                        for (int k = kk; k < kk+BLOCK && k < N; k++)
                            C[i][j] += A[i][k] * B[k][j];
}

SIMD ベクトル化 (AVX2)

#include <immintrin.h>  // AVX2

// AVX2で8つのfloatを同時に加算
void vector_add_avx2(float *a, float *b, float *c, int n) {
    int i;
    for (i = 0; i <= n - 8; i += 8) {
        __m256 va = _mm256_loadu_ps(&a[i]);   // 8個のfloatをロード
        __m256 vb = _mm256_loadu_ps(&b[i]);
        __m256 vc = _mm256_add_ps(va, vb);    // 8個を同時加算
        _mm256_storeu_ps(&c[i], vc);          // 8個のfloatをストア
    }
    // 残りの要素を処理
    for (; i < n; i++)
        c[i] = a[i] + b[i];
}

12. クイズ

Q1. パイプラインにおけるデータハザード (Data Hazard) の発生原因と解決方法は?

正解: 前の命令の書き込み (Write) が完了する前に次の命令がそのレジスタを読み出そうとするとき (RAW: Read After Write) に発生します。

解決方法:

  • フォワーディング (バイパッシング): EX/MEMステージの結果を次の命令のEXステージ入力に直接転送し、ストールなしで解決。
  • ストール (バブル挿入): NOPサイクルを挿入してパイプラインを一時停止。性能低下。
  • コード再配置: コンパイラが独立した命令を依存命令の間に挿入。
  • ロード・ユースハザードの場合は1サイクルのストールが不可避です (MEMステージの結果をEXへのフォワーディング不可)。
Q2. ダイレクトマップキャッシュと4ウェイセット連想キャッシュの違いは?

正解: ダイレクトマップキャッシュはメモリの各ブロックがキャッシュの1箇所にのみマップされますが、4ウェイセット連想キャッシュは同じインデックスを持つ4つのスロットのいずれかに格納できます。

主な違い:

  • ダイレクトマップ: コンフリクトミスが発生しやすい。ハードウェアがシンプルで高速。
  • 4ウェイSA: コンフリクトミスが大幅に減少。LRU等の交換ポリシーが必要。コスト増加。
  • 実際のCPUのL1キャッシュは通常4ウェイまたは8ウェイを使用。フル連想はTLBにのみ使用。
Q3. TLB (Translation Lookaside Buffer) の役割とTLBミス時の処理手順を説明してください。

正解: TLBは仮想アドレスから物理アドレスへの変換結果をキャッシュするフル連想キャッシュです。メモリ上のページテーブルへのアクセスオーバーヘッドを削減します。

TLBミスの処理手順:

  1. TLBに対象の仮想ページ番号 (VPN) が存在しない
  2. ハードウェア (x86) またはソフトウェア (RISC-V、MIPS) がページテーブルウォークを実行
  3. 多段ページテーブルを順に参照 (PML4 → PDPT → PD → PT)
  4. 最終的な物理フレーム番号 (PFN) を取得
  5. TLBに新しいエントリを追加 (必要に応じてLRUで既存エントリを退去)
  6. 元のメモリアクセスをリトライ
  • 全処理で数百サイクルかかるため、TLBヒット率 (99%以上) の維持が重要です。
Q4. GPUにおけるワープダイバージェンス (Warp Divergence) とは何か、性能への影響は?

正解: ワープ内の32スレッドが異なる分岐 (if-else) を取るときにワープダイバージェンスが発生します。

動作の仕組み:

  • SIMTモデルではワープの全スレッドが同一命令を実行する必要があります。
  • ifブロック実行時: ifを取ったスレッドのみアクティブ、elseスレッドはマスキング (非アクティブ)
  • elseブロック実行時: elseを取ったスレッドのみアクティブ、ifスレッドはマスキング
  • 2つのパスを直列に実行するため、最悪の場合2倍の時間がかかります。

対策:

  • ワープ内のスレッドが同じ分岐を取るようにデータを整理
  • 条件分岐の代わりに算術演算で代替 (条件付き選択)
  • カーネルのホットパスにおける分岐を最小化
Q5. アムダールの法則を用いて、プログラム全体の80%を並列化したときの最大理論スピードアップを計算してください。

正解: 無限に多くのプロセッサを使用した場合、最大スピードアップは 5倍 です。

計算:

  • 並列化可能な割合: f = 0.8
  • 逐次実行割合: 1 - 0.8 = 0.2
  • 並列化倍率 s を無限大にすると: f/s → 0
  • スピードアップ = 1 / ((1 - f) + f/s) = 1 / (0.2 + 0) = 1 / 0.2 = 5倍

意味: コアをいくら追加しても、逐次実行部分 (20%) が全体のスピードアップを5倍に制限します。逐次ボトルネックを除去することが並列最適化の核心である理由がここにあります。


参考文献