プロセッサ・アーキテクチャ

パイプライン

命令処理を分割し並行実行して高速化する仕組み。

概要

パイプライン(Pipeline)は、CPUが命令を処理する際に、処理工程を複数のステージに分割し、各ステージで異なる命令を同時進行させることで実行スループットを向上させる技術です。工場の組み立てラインになぞらえて「命令パイプライン」と呼ばれます。

パイプラインがない場合(逐次処理)、1命令の処理が完了してから次の命令を開始します。パイプラインを使うと、ある命令の実行中に次の命令のデコードや、その次の命令のフェッチを並行して行えます。理想的には、N段パイプラインでN倍のスループット向上が得られます。

ARM Cortex-M3/M4は3段パイプライン、Cortex-M7は6段デュアルイシューパイプライン、ARM Cortex-A53は8段パイプラインを採用しています。x86/x64の高性能コアはさらに複雑な14〜20段以上のパイプラインを持ちます。

歴史・背景

パイプライン処理の概念はコンピュータ以前から存在しましたが、コンピュータアーキテクチャへの適用は1960年代のIBM System/360 Model 91(1966年)が先駆者です。

マイクロプロセッサでのパイプラインは1980年代から本格化しました:

  • 1985年: Intel 80386が3段パイプラインを採用
  • 1989年: Intel 486が5段パイプラインを採用(1命令/サイクル達成)
  • 1993年: Intel Pentiumがスーパースカラー(2命令/サイクル)
  • 1995年: Pentium Proがアウトオブオーダー実行・12段パイプライン

RISC系では:

  • 1986年: MIPS R2000(5段パイプライン・RISC定番)
  • 1991年: ARM 7TDMIが3段パイプラインを採用
  • 2004年: ARM Cortex-M3が3段パイプライン(Thumb-2)

パイプラインの段数は増加の一途をたどりましたが、2004年のNetBurst(Pentium 4 Prescott、31段!)は分岐予測ミス時のペナルティが大きすぎて失敗。その反省からIntelはCore Microarchitecture(2006年)で14段に戻しました。

技術仕様

基本5段パイプライン(RISC標準)

IF   : 命令フェッチ(Instruction Fetch)
       PC→メモリ/I$キャッシュ→命令レジスタへ
ID   : 命令デコード(Instruction Decode)
       オペコード解析・レジスタ読み出し・即値生成
EX   : 実行(Execute)
       ALU演算・アドレス計算
MEM  : メモリアクセス(Memory Access)
       ロード/ストア(D$キャッシュアクセス)
WB   : ライトバック(Write Back)
       演算結果をレジスタファイルに書き込み

タイムチャートで見ると:

時刻:    T1   T2   T3   T4   T5   T6   T7
命令A:   IF   ID   EX   MEM  WB
命令B:        IF   ID   EX   MEM  WB
命令C:             IF   ID   EX   MEM  WB
命令D:                  IF   ID   EX   MEM  WB
命令E:                       IF   ID   EX   MEM  WB

T5以降は毎クロックサイクルで1命令が完了(スループット = 1命令/サイクル)。

ARM Cortex-Mのパイプライン

Cortex-M0/M0+(2〜3段)

Cortex-M0+(2段):
  Fetch → Decode/Execute

より単純なパイプラインで、電力・面積を最小化。

Cortex-M3/M4(3段)

Fetch → Decode → Execute

Execute段でALU演算・メモリアクセスを実行(合体)。

Cortex-M7(6段、デュアルイシュー)

Fetch → Decode → Dispatch → Execute → Memory → Writeback

同時に2命令を発行できる(スーパースカラー)。
L1 I$(32/64KB)・L1 D$(32/64KB)を搭載。

パイプラインハザード

パイプラインの理想的な動作を妨げる「ハザード」が3種類あります:

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

前の命令の結果を次の命令がすぐに使おうとする:

; RAWハザード(Read After Write)
ADD R0, R1, R2   ; R0への書き込み
SUB R3, R0, R4   ; R0の読み込み(まだ書き込まれていない!)

解決策:

  • フォワーディング(Bypassing): EX段の結果をID段に直接転送
  • パイプラインストール(バブル挿入): NOP命令を挿入して待機
フォワーディングなし(ストール):
ADD: IF  ID  EX  MEM WB
SUB: IF  ID  --  --  EX  MEM WB  (2サイクルストール)

フォワーディングあり:
ADD: IF  ID  EX  MEM WB
SUB: IF  ID  EX  MEM WB  (EX→EXフォワーディング)

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

分岐命令でどの命令を次にフェッチすべきか不明:

CMP  R0, #0
BEQ  label      ; 条件分岐:EQならlabelへ
ADD  R1, R2, R3 ; これはフェッチしていいか?(分岐が判明するまで不明)

解決策:

  • 分岐遅延スロット(MIPS): 分岐後の1命令は必ず実行(コンパイラが有効命令を配置)
  • 分岐予測(Branch Prediction): 投機的に実行を進め、予測失敗時にフラッシュ
  • 早期分岐解決: パイプラインの早い段で分岐先を決定

Cortex-M3/M4の3段パイプラインでは分岐のペナルティは1〜3サイクルです。

3. 構造ハザード(Structural Hazard)

複数の命令が同一ハードウェアリソースを同時に使おうとする:

  • 単一メモリポート:命令フェッチとデータアクセスが競合
  • 解決策:ハーバードアーキテクチャ(命令メモリとデータメモリを分離)

Cortex-Mはハーバード内部バスを採用しており、命令とデータのバスが分離されています。

スーパースカラーとアウトオブオーダー実行

スカラー(1命令/サイクル):
  ALU ──→ [命令1][命令2][命令3]...

スーパースカラー(複数命令/サイクル):
  ALU ──→ [命令1][命令3]...
  FPU ──→ [命令2][命令4]...(並列実行)

アウトオブオーダー(OoO)実行:
  プログラム順序を無視して実行できる命令を先行実行
  → Cortex-A9以降のアプリケーションプロセッサで採用

動作原理

コンパイラによるパイプライン最適化

GCCなどのコンパイラはパイプラインを考慮した命令スケジューリングを行います:

// Cコード
int a = x + y;
int b = z * w;
int c = a + b;

ナイーブな生成コード(データハザードあり):

ADD R0, R1, R2   ; a = x + y
MUL R3, R4, R5   ; b = z * w (MULの結果待ちはない)
ADD R6, R0, R3   ; c = a + b (R0の結果待ち2サイクル!)

最適化済みコード(パイプライン考慮):

ADD R0, R1, R2   ; a = x + y → 開始
MUL R3, R4, R5   ; b = z * w → 並行して実行(ADD完了前に開始)
NOP              ; 必要ならNOP(フォワーディングで不要なことも)
ADD R6, R0, R3   ; c = a + b → R0が揃ってから実行

割り込みとパイプライン

割り込み発生時、パイプライン中の命令は適切に処理する必要があります:

  • Cortex-M: ハードウェアが自動でR0-R3・R12・LR・PC・xPSRをスタックに退避(ハードウェアスタッキング)
  • パイプライン中の命令はロールバックまたは完了を待ってから例外処理へ
  • テールチェイニング: 連続する割り込みでプロローグ/エピローグを共有し6サイクルで処理

用途・ユースケース

リアルタイム制御でのパイプライン影響

RTOSの割り込みレイテンシはパイプラインに依存します:

Cortex-M3(3段パイプライン):
  外部割り込み → ハードウェアスタッキング(12サイクル)→ ISR実行
  
Cortex-M7(6段パイプライン):
  外部割り込み → より多くのパイプラインフラッシュ → わずかにレイテンシ増
  
パイプラインが長いほど分岐ペナルティ・割り込みレイテンシが増す可能性がある

DSP処理とパイプライン

DSP演算(FIRフィルタ等)はループ処理が多く、パイプラインの効率が重要です:

// FIRフィルタ(データ依存性が少なくパイプラインに適した処理)
void fir_filter(float *output, const float *input,
                const float *coeffs, int n, int taps) {
    for (int i = 0; i < n; i++) {
        float acc = 0.0f;
        for (int j = 0; j < taps; j++) {
            acc += coeffs[j] * input[i - j];  // 各反復は独立(高並列性)
        }
        output[i] = acc;
    }
}
// コンパイラがベクトル化・パイプラインスケジューリングを適用

実装・開発のポイント

コンパイラオプションとパイプライン最適化

# GCCの最適化オプション
gcc -O2                    # 基本最適化(パイプラインスケジューリング含む)
gcc -O3                    # 積極的最適化(アンロール・ベクトル化)
gcc -Os                    # サイズ最適化(パイプラインより密度優先)

# ARM Cortex-M4向け
arm-none-eabi-gcc -O2 -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 \
  -fschedule-insns2        # 命令スケジューリング有効(Oで自動的に有効)

パイプラインバブルを避けるコーディング

// 悪い例(データ依存チェーン)
a = func1(x);
b = func2(a);  // aに依存
c = func3(b);  // bに依存(データ依存チェーン)

// 良い例(並列性を高める)
a = func1(x);
d = func1(y);  // aに依存しない→並行実行可能
b = func2(a);
e = func2(d);  // dに依存しない
c = func3(b);
f = func3(e);

分岐予測に配慮したコーディング

// 分岐予測ヒント(GCCの__builtin_expect)
// 例外処理はほぼ発生しない→「false」が多い分岐
if (__builtin_expect(error_condition, 0)) {
    handle_error();  // まずここには来ない
}

// ループの脱出は稀→「true」が多い分岐
while (__builtin_expect(data_available, 1)) {
    process();
}

他技術との比較

パイプラインとスーパースカラーの違い

技術説明スループット向上の仕組み
パイプライン1命令を複数ステージに分割ステージを並行処理
スーパースカラー複数の実行ユニット1クロックで複数命令を発行
OoO実行命令の依存関係を動的解析データ依存のない命令を先行実行
VLIWコンパイラが複数命令をパック静的に並列性を抽出

ARM Cortex-M7はスーパースカラー(デュアルイシュー)のパイプラインで、条件が揃えば1サイクルで2命令を実行できます。

パイプラインとキャッシュの関係

パイプラインの性能はメモリアクセス速度に大きく依存します。命令キャッシュ(I$)がミスするとIF段が止まり、データキャッシュ(D$)がミスするとMEM段が止まります。Cortex-M7がL1 I$/D$キャッシュを持つのはパイプラインのストールを最小化するためです。キャッシュの効率的な利用がパイプライン性能を最大化する鍵です。

関連用語

参考リンク