前章の宿題 — なぜIPCは1を超えられるのか

前章では「クロック周波数(GHz)」と「IPC(Instructions Per Cycle、1サイクルあたりの命令実行数)」という2つの指標を学びました。 そこで少し不思議な事実に触れたはずです。IPCは1を超えることがある、つまり1サイクルの間に2命令、3命令を処理できるCPUが実在する、という点です。 1つのクロックの中で複数の命令が「同時に」処理されるとはどういうことなのか。本章ではCPUの内部構造にまで踏み込み、この謎を解き明かします。

鍵となるのは3つの技術です。命令処理を工程分割してオーバーラップさせるパイプライン、 1サイクルに複数命令を並列発行するスーパースカラ、 そしてプログラムに書かれた順序を無視して実行できる命令から先に片付けるアウトオブオーダー実行。 この3つが組み合わさることで、現代のCPUはクロックを上げなくてもスループットを底上げできるようになっています。

古典的5段パイプライン — 命令処理を組立ラインにする

CPUが1つの命令を処理するには、大まかに次の5つの工程が必要です。

  • Fetch(フェッチ): プログラムカウンタ(PC)が指すアドレスからメモリ上の命令を読み込む
  • Decode(デコード): 命令の種類を解読し、レジスタファイルから必要なオペランド(演算対象の値)を読み出す
  • Execute(実行): ALU(算術論理演算ユニット)で演算を行う、または分岐命令の条件を判定する
  • Memory(メモリアクセス): load/store命令の場合のみ、データメモリへの読み書きを行う
  • Writeback(ライトバック): 演算結果をレジスタファイルに書き戻す

もし1つの命令がFetchからWritebackまで完全に終わるまで次の命令に着手しないなら、5サイクルに1命令しか処理できません。 そこで工場の組立ラインのように、各工程を専用の回路(ステージ)に分割し、異なる命令の異なる工程を同じサイクルで同時に処理させるのがパイプラインです。 1命令の処理時間(レイテンシ)自体は5サイクルのまま変わりませんが、パイプラインが埋まった状態では毎サイクル新しい命令をFetchに投入できるため、 理想的には段数倍のスループット向上が得られます。

graph LR
  IF["Fetch\n命令フェッチ"] --> ID["Decode\nデコード+レジスタ読み出し"] --> EX["Execute\nALU演算/分岐判定"] --> MEM["Memory\nload/store時のみ"] --> WB["Writeback\nレジスタ書き戻し"]

  style IF fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style ID fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style EX fill:#f97316,stroke:#ea580c,color:#fff
  style MEM fill:#14b8a6,stroke:#0d9488,color:#fff
  style WB fill:#ec4899,stroke:#be185d,color:#fff
古典的5段パイプライン。命令処理を5つの工程に分割し、各サイクルで異なる命令の異なる工程を並行処理することでスループットを稼ぐ

パイプラインだけではIPC1が上限 — 3つの拡張

ここまでのパイプラインは「1サイクルにつき1命令をFetchする」設計であり、理想状態でもIPCは1が上限です。 IPCを1超に押し上げるには、パイプラインに加えてさらに3つの仕組みが必要になります。

スーパースカラ実行 — 1サイクルに複数命令を流す

スーパースカラ実行とは、CPUが複数の実行ユニット(ALUやFPU、load/storeユニットなど)を持ち、 1サイクルに複数命令をまとめてデコード・発行する設計です。目的はプログラム中に存在する命令レベル並列性(ILP: Instruction-Level Parallelism)を活用することにあります。 互いに依存関係のない命令が並んでいれば、それらを別々の実行ユニットに同時に割り当てることで、1サイクルで複数命令を完了させられます。

アウトオブオーダー実行 — 待たされている命令を追い越す

プログラム上の命令列には、前の命令の結果を必要とする依存関係がしばしば存在します。もしプログラム順(インオーダー)にしか実行できないと、 1つの命令がメモリアクセス待ちなどで止まった瞬間、その後ろに続く独立した命令まで巻き添えでストールしてしまいます。

アウトオブオーダー実行(OoO)は、プログラム順ではなくオペランドが揃った命令から先に実行することでこの無駄を解消します。 依存関係で待たされている命令があっても、実行ユニットに空きがあれば後方の独立命令を先に処理してしまう発想です。 これを支えるのが次の3つの仕組みです。

  • リザベーションステーション: オペランドが揃うのを待つ命令を一時的にプールしておくキュー
  • レジスタリネーミング: 見かけ上同じレジスタを使う命令同士の偽の依存関係を、物理レジスタを付け替えることで解消する仕組み
  • リオーダーバッファ(ROB: Reorder Buffer): 実行が完了した命令の結果を一時的に保持し、プログラム順が回ってきた命令から順にレジスタへ確定(コミット)させるバッファ

重要なのは、命令の実行はアウトオブオーダーで進む一方、命令のコミット(結果の確定)は必ずインオーダーで行われる、という点です。 これにより例外や分岐予測ミスが起きた際も、プログラムから見た「正しい状態」をいつでも復元できます。

flowchart TD
  FE["Fetch\nインオーダー"] --> DE["Decode + Register Rename\nインオーダー"]
  DE --> RS["Reservation Station\nオペランド待ちでキュー"]
  DE --> ROB["Reorder Buffer\nプログラム順を記録"]
  RS -->|オペランド揃い次第| EU1["実行ユニット1"]
  RS -->|オペランド揃い次第| EU2["実行ユニット2"]
  RS -->|オペランド揃い次第| EU3["実行ユニット3"]
  EU1 --> ROB
  EU2 --> ROB
  EU3 --> ROB
  ROB -->|インオーダーでコミット| WB["Writeback\nレジスタ確定"]

  style FE fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style DE fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style RS fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style EU1 fill:#f97316,stroke:#ea580c,color:#fff
  style EU2 fill:#f97316,stroke:#ea580c,color:#fff
  style EU3 fill:#f97316,stroke:#ea580c,color:#fff
  style ROB fill:#14b8a6,stroke:#0d9488,color:#fff
  style WB fill:#ec4899,stroke:#be185d,color:#fff
アウトオブオーダー実行の概念図。FetchとDecodeはインオーダーで進み、実行ユニットへの割り当てはオペランドが揃った命令から自由な順序で行われ、ROBを経て最終的にインオーダーでコミットされる

分岐予測と投機的実行 — 未確定の道を先読みする

分岐命令(if文やループの判定など)の行き先は、パイプラインの後段(Execute)まで確定しません。 もし分岐が確定するまでパイプラインを止めて待つと、分岐のたびにストールが発生し性能が大きく落ちてしまいます。

そこで現代のCPUは分岐予測器が過去の実行履歴などから分岐の行き先を予測し、その予測に基づいて先の命令を投機的に実行し続けます。 予測が当たっていればストールなしで処理を継続でき、外れた場合は投機的に実行した分をすべて破棄してパイプラインをフラッシュし、正しい行き先からやり直します。

ROBサイズの世代推移 — 「見渡せる範囲」を実測値で見る

アウトオブオーダー実行の実力は、ROBが一度に抱えられる命令数、つまり命令ウィンドウの広さに強く依存します。 ROBが大きいほど、CPUはより遠くの命令まで見渡して並列実行可能な組合せを見つけられます。 Henry WongやTravis Downsらによる実測データベース、Chips and Cheese・Hardware Timesの解析記事をもとに、主要CPUのROBエントリ数の推移を見てみましょう。

CPU発表年ROBエントリ数備考
Intel Sandy Bridge2011168
Intel Haswell2013192
Intel Skylake2015224
Intel Golden Cove2021512Skylake比 約2.3倍
Intel Lion Cove2024576
AMD Zen2017192
AMD Zen22019224
AMD Zen32020256
AMD Zen42022320
AMD Zen52024448Zen4比 +40%
Apple A7 (Cyclone)2013192Apple初の自社設計コア
Apple A13 (Lightning)2019560
Apple A14 / M1 (Firestorm)2020630当時最大級

数字を並べると、Intel/AMD/Appleいずれも10年強でROBエントリ数を3倍前後に拡大してきたことが分かります。 特にApple A14/M1(630)は登場時点でIntel/AMDの当時の主力(Zen2の224、当時のSkylake系の224)を大きく上回っており、 Apple Siliconの高IPC設計を裏付ける数値の1つとしてよく引用されます。

発行幅の拡大 — デコード/ディスパッチ/リタイア幅の比較

ROBサイズと並んで重要なのが、1サイクルに何命令をデコード・発行・リタイアできるかという「幅」です。 幅が広いほど、命令ウィンドウの中からより多くの独立命令を同時に処理に回せます。

CPUデコード/ディスパッチ/リタイア幅補足
AMD Zen58-wideディスパッチ・リタイアZen4の6-wideから拡大。ALUも6基に増加(Zen4は4基)
Apple M4 Pコア8-wide命令デコード商用CPUで最大級とされる(非公式解析値)
ARM Cortex-X92510-wideデコード・ディスパッチ前世代Cortex-X4からさらに拡大
ARM Neoverse V26-wideデコードIntel Golden Coveと同等水準

発行幅の拡大・ROBの拡大・実行ポートの増加は、どれか1つだけを増やしても効果が薄く、 3つが揃って初めて「より広い命令ウィンドウの中から並列実行可能な命令を見つけ出す」効果が発揮されます。 理論的には、あるIPCを実現するために必要なROBサイズはディスパッチ幅に対して2乗に近い比率で増加する関係にあるとされ、 これが世代ごとのROB拡大が緩やかな階段状(急激な倍増ではなく段階的な増加)になっている理由の1つです。

なぜ最近のCPUはパイプライン段数を公表しないのか

ここまで数値を細かく追ってきましたが、実は近年のハイエンドCPUについて総パイプライン段数の明確な公式数値はほとんど公表されていません。 AMD Zen5、Apple M4、ARM Cortex-X925のいずれについても、ベンダー公式資料やサードパーティの詳細解析記事を当たっても、確定的な段数は見つかりません。

分岐ミスとメモリ待ち — 次章への橋渡し

本章で見てきたパイプライン・スーパースカラ・アウトオブオーダー実行・分岐予測は、いずれも「実行ユニットを遊ばせない」ための工夫でした。 しかし、これらの仕組みにも避けられない弱点が2つあります。1つは分岐予測が外れた際のパイプラインフラッシュ、 そしてもう1つがメモリアクセスの待ち時間です。

どれだけROBを大きくし、実行ユニットを並べても、load命令がメインメモリからのデータ到着を待つ間はその命令(とそれに依存する命令)をコミットできません。 ROBが命令で埋まりきってしまえば、パイプライン全体が事実上停止します。 この「メモリの壁」をどう緩和するかが、次章で扱うキャッシュ階層のテーマです。

理解度チェック

問題 0 / 50%
Q1

CPUのIPC(1サイクルあたりの命令実行数)を1超に押し上げる仕組みとして、最も本質的な組合せはどれですか?

キーボード: 1〜4 で選択、Enter で回答