命令レベル並列性(ILP)とは?わかりやすく解説

単一の半導体において、単一のプログラム中から、いかに並列に計算するかを、命令レベル並列性(ILP, Instruction Level Parallelism)といいます。一方で、複数のプログラムを同時に並列に実行するスレッドレベル並列性(TLP, Thread Level Parallelism)もあり、TLPの処理のためにマルチコアの利用が進んできています。

本記事の内容

  • 命令レベル並列性(ILP)の基本概念
  • パイプライン処理の仕組み
  • データハザードとその対策
  • スーパースカラアーキテクチャ
  • Pythonでのパイプラインシミュレーション

パイプライン処理の基礎

パイプライン処理は、ILPを活用する最も基本的な手法です。CPUの命令実行を複数のステージに分割し、各ステージを並列に動作させます。

5段パイプライン

RISC型プロセッサの典型的な5段パイプラインは以下の通りです。

  1. IF (Instruction Fetch): 命令の読み込み
  2. ID (Instruction Decode): 命令のデコードとレジスタ読み出し
  3. EX (Execute): 演算の実行
  4. MEM (Memory Access): メモリアクセス
  5. WB (Write Back): 結果のレジスタへの書き戻し

スピードアップ

パイプラインのない場合、1命令に $k$ クロックサイクルかかるとします。$n$ 個の命令を実行する場合、パイプラインなしでは、

$$ T_{\text{seq}} = n \cdot k $$

クロックサイクルが必要です。$k$ 段パイプラインでは、最初の命令が完了するまで $k$ サイクルかかりますが、以降は1サイクルごとに1命令が完了します。

$$ T_{\text{pipe}} = k + (n – 1) $$

スピードアップ比は、

$$ S = \frac{T_{\text{seq}}}{T_{\text{pipe}}} = \frac{nk}{k + (n-1)} $$

$n \to \infty$ の極限では $S \to k$ となり、理想的にはステージ数と同じ倍率の高速化が得られます。

データハザード

パイプライン処理では、連続する命令間のデータ依存により、パイプラインが停止(ストール)する場合があります。これをデータハザードと呼びます。

ハザードの種類

3種類のデータハザードが存在します。

  • RAW(Read After Write): 先行命令の書き込み結果を後続命令が読む。最も頻繁に発生
  • WAR(Write After Read): 先行命令の読み出し前に後続命令が書き込む
  • WAW(Write After Write): 同じレジスタへの2つの書き込みの順序が逆転する

フォワーディング(バイパス)

RAWハザードの主要な解決策がフォワーディングです。先行命令の演算結果を、レジスタへの書き戻しを待たずに後続命令のEXステージに直接転送します。

フォワーディングにより、多くのRAWハザードを1サイクルのストールなしで解消できます。

スーパースカラアーキテクチャ

スーパースカラプロセッサは、1クロックサイクルに複数の命令を同時に発行・実行できるアーキテクチャです。$m$ 命令同時発行のスーパースカラの理想的なスピードアップは、

$$ S_{\text{ideal}} = m \cdot k $$

実際にはデータ依存や資源の競合により、理想値に達することは稀です。実効IPC(Instructions Per Cycle)は通常1.5から3程度です。

Pythonでパイプラインをシミュレーション

import numpy as np
import matplotlib.pyplot as plt

def simulate_pipeline(n_instructions, n_stages, hazard_rate=0.0):
    """パイプライン処理のシミュレーション

    Parameters
    ----------
    n_instructions : int
        実行する命令数
    n_stages : int
        パイプラインの段数
    hazard_rate : float
        ハザード発生率(0.0 ~ 1.0)

    Returns
    -------
    total_cycles : int
        総サイクル数
    """
    total_cycles = n_stages  # 最初の命令の完了まで
    stalls = 0

    for i in range(1, n_instructions):
        # ハザードの確率的発生
        if np.random.random() < hazard_rate:
            stalls += 1  # 1サイクルのストール

    total_cycles += (n_instructions - 1) + stalls
    return total_cycles, stalls

# パラメータ
n_instructions = 1000
n_stages = 5

# ハザード率を変化させたシミュレーション
hazard_rates = np.linspace(0, 0.5, 50)
speedups = []
ipcs = []

np.random.seed(42)

for hr in hazard_rates:
    cycles, stalls = simulate_pipeline(n_instructions, n_stages, hr)
    seq_cycles = n_instructions * n_stages
    speedups.append(seq_cycles / cycles)
    ipcs.append(n_instructions / cycles)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# スピードアップ
axes[0].plot(hazard_rates * 100, speedups, 'b-', linewidth=2)
axes[0].axhline(y=n_stages, color='r', linestyle='--',
                label=f'Ideal ({n_stages}x)')
axes[0].set_xlabel('Hazard Rate [%]')
axes[0].set_ylabel('Speedup')
axes[0].set_title('Pipeline Speedup vs Hazard Rate')
axes[0].legend()
axes[0].grid(True)

# IPC
axes[1].plot(hazard_rates * 100, ipcs, 'g-', linewidth=2)
axes[1].axhline(y=1.0, color='r', linestyle='--', label='Ideal IPC = 1.0')
axes[1].set_xlabel('Hazard Rate [%]')
axes[1].set_ylabel('IPC (Instructions Per Cycle)')
axes[1].set_title('IPC vs Hazard Rate')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()

# アムダールの法則
def amdahl_speedup(f_parallel, n_cores):
    """アムダールの法則によるスピードアップ"""
    return 1 / ((1 - f_parallel) + f_parallel / n_cores)

n_cores_range = np.arange(1, 65)
fig, ax = plt.subplots(figsize=(8, 6))

for f in [0.5, 0.75, 0.9, 0.95, 0.99]:
    s = [amdahl_speedup(f, n) for n in n_cores_range]
    ax.plot(n_cores_range, s, label=f'f = {f}')

ax.set_xlabel('Number of Cores')
ax.set_ylabel('Speedup')
ax.set_title("Amdahl's Law")
ax.legend()
ax.grid(True)
plt.tight_layout()
plt.show()

アムダールの法則

並列化可能な部分の割合を $f$、プロセッサ数を $n$ とすると、スピードアップの上限は次のように与えられます。

$$ S(n) = \frac{1}{(1-f) + \frac{f}{n}} $$

$n \to \infty$ の極限では、

$$ S_{\max} = \frac{1}{1 – f} $$

となり、逐次実行部分の割合 $1-f$ がボトルネックとなります。例えば、プログラムの5%が逐次実行の場合、いくらプロセッサを増やしてもスピードアップは最大20倍です。

まとめ

本記事では、命令レベル並列性(ILP)について解説しました。

  • パイプライン処理は命令を複数ステージに分割して並列実行する基本手法である
  • 理想的には $k$ 段パイプラインで $k$ 倍のスピードアップが得られる
  • データハザード(特にRAW)がパイプラインの性能を制限する
  • フォワーディングによりハザードの影響を軽減できる
  • アムダールの法則により、並列化のスピードアップには理論的な上限がある