スーパースカラとは?CPUが1クロックで複数命令を実行する仕組みをわかりやすく解説

最新のスマホやノートPCの性能を語るとき、「クロック周波数(GHz)」だけでは説明がつかない場面が増えてきました。たとえばApple M系チップは同じくらいのクロックでも、旧世代より大幅に速く動きます。その秘密の一つがスーパースカラです。スーパースカラとは、CPUが1クロックサイクルで複数の命令を同時に実行する仕組みのことです。

なぜこんな工夫が必要なのでしょうか。クロックを上げ続けるのは発熱と電力の壁にぶつかります。そこで「同じ1クロックの間にどれだけ多くの仕事をこなせるか」を増やす方向に進化しました。スーパースカラの仕組みを理解すると、次のような場面で役立ちます。

  • CPU・SoCの性能評価: ベンチマークの差が「クロック」ではなく「IPC(1クロックあたり命令数)」で決まる理由がわかり、M系チップやx86の世代差を正しく読めるようになります。
  • 高性能なコードの書き方: 依存関係の少ないコードがなぜ速いのか、ループ展開やSIMDがなぜ効くのかを、CPU内部の動きから理解できます。

本記事では、パイプラインの復習から出発し、IPC=1の限界をスーパースカラがどう突破するかを段階的に見ていきます。命令レベル並列性(ILP)と依存ハザード、インオーダーとアウトオブオーダー実行、リオーダーバッファやレジスタリネーミングといった現代CPUの中核技術を、タイミング図とPythonシミュレーションでわかりやすく解説します。

本記事の内容

  • パイプライン復習とIPC=1の限界
  • スーパースカラの基本(複数実行ユニット・同時発行)と4方式(スカラ/パイプライン/スーパースカラ/VLIW)の比較
  • 命令レベル並列性(ILP)と3種のハザード(データ/制御/構造)
  • インオーダー vs アウトオブオーダー実行、ROBとレジスタリネーミング
  • 分岐予測との関係と実CPU例(Apple M系・x86の発行幅)
  • ILPの限界とマルチコア・SIMDへの流れ、依存グラフからのIPC計算

前提知識

この記事は単体で読めますが、以下の記事を読んでおくと並列化の文脈が深まります。

パイプライン復習とIPC=1の限界

スーパースカラを理解する前に、その土台となるパイプラインを簡単に復習します。CPUが1つの命令を実行するには、おおまかに次の5段階の作業が必要です。

  1. IF(命令フェッチ): メモリから命令を取ってくる
  2. ID(デコード): 命令を解読し、必要なデータを読む
  3. EX(実行): 演算する
  4. MEM(メモリアクセス): 必要ならメモリを読み書きする
  5. WB(ライトバック): 結果をレジスタに書き戻す

パイプラインは、この5段階を流れ作業(工場のベルトコンベア)のように重ねる工夫です。命令1がEXに進んだら、命令2のIFを始める——こうして各段を埋めていきます。

5段パイプライン(スカラ)のタイミング図。1サイクルに1命令ずつ発行しIPCの上限は1

図のように、各段が1サイクルずつずれて重なります。パイプラインが満たされた後は、1サイクルごとに1命令が完了します。この「1サイクルあたり何命令を完了できるか」をIPC(Instructions Per Cycle)と呼びます。

ここで重要な限界があります。普通のパイプライン(スカラ・プロセッサ)は「1サイクルに1命令ずつしか発行(投入)できない」ため、IPCの上限は1です。どんなにパイプラインを深くしても、流れ込む命令が1サイクル1本である限り、完了も1サイクル1本を超えられません。

では、IPCを1より大きくするにはどうすればよいのでしょうか。答えはシンプルで、「1サイクルに複数命令を同時に流す」ことです。これがスーパースカラの出発点です。

スーパースカラの基本 — 複数の実行ユニットで同時発行

スーパースカラの発想は単純です。実行ユニット(演算器)を複数用意し、デコード・発行の経路も複数にして、1サイクルに2命令以上を同時にパイプラインへ投入するのです。1サイクルに2命令投入できる設計を「2-wayスーパースカラ」、4命令なら「4-way」と呼びます。

スカラ・パイプラインと2-wayスーパースカラのタイミング図の比較

上段(スカラ)では4命令の完了に多くのサイクルがかかりますが、下段(2-wayスーパースカラ)では命令1と命令2、命令3と命令4をそれぞれ同時に発行するため、同じ4命令をほぼ半分の時間で処理できます。理想的にはIPCが2になります。実行ユニットを増やすことで、1サイクルあたりの「仕事の幅」を広げているのです。

この「1サイクルに複数命令を実行する」目標に対しては、実は複数のアプローチがあります。代表的な4つを比較しておきましょう。

逐次実行・パイプライン・スーパースカラ・VLIWの4方式のIPC比較

  • 逐次実行(非パイプライン): 1命令の全段が終わってから次へ。IPCは1未満で最も遅い
  • パイプライン(スカラ): 段を重ねてIPC=1を狙う
  • スーパースカラ(動的): ハードウェアが実行時に命令の依存関係を解析し、並列に発行できるものを選んで同時実行する。IPC>1
  • VLIW(静的): コンパイラが事前に並列実行できる命令を1つの長い命令語に束ねる。ハードは単純だが、最適化をコンパイラに任せる

スーパースカラとVLIWはどちらもIPC>1を狙いますが、「並列にできる命令を誰が見つけるか」が違います。スーパースカラはハードウェアが動的に、VLIWはコンパイラが静的に決めます。現代の汎用CPU(x86・Arm)はほぼすべてスーパースカラです。VLIWは実行時の状況(キャッシュミスなど)に適応できない弱点があり汎用CPUでは主流になりませんでしたが、命令の並びが予測しやすいDSP(信号処理プロセッサ)や一部のGPU・AIアクセラレータでは、ハードを単純にできる利点を活かして使われています。スーパースカラが「実行時の賢いハード」、VLIWが「コンパイル時の賢いソフト」に賭けた設計、と整理すると違いが見えてきます。

しかし、ここで素朴な疑問が湧きます。実行ユニットを2倍にすれば本当にIPCも2倍になるのでしょうか。残念ながら、そう単純ではありません。命令どうしの「依存関係」が並列実行を妨げるからです。次にこれを見ていきます。

命令レベル並列性(ILP)と3種のハザード

「同時に実行できる命令がどれだけあるか」を命令レベル並列性(ILP: Instruction-Level Parallelism) と呼びます。ILPが高ければスーパースカラの恩恵を受けられますが、命令どうしに依存があるとILPは下がります。

最も基本的な依存はデータ依存です。次の2命令を考えます。

命令1: R1 = R2 + R3
命令2: R4 = R1 + R5

命令2は命令1の結果 $R1$ を使います。つまり命令1が結果を書き終えるまで、命令2は計算を始められません。これを同時に発行しようとしても、待たされてしまいます。

データ依存RAWで命令2が命令1の結果を待ち同時発行できないタイミング図

図のように、命令2のEX(実行)は命令1のWB(結果の書き戻し)の後までずれ込みます。この「前の命令の書いた値を後の命令が読む」依存をRAW(Read After Write)依存と呼びます。これがあると2命令を同時に進められず、パイプラインにストール(待ち)が生じます。

依存によって並列実行が妨げられる現象をハザードと総称します。ハザードは大きく3種類あります。

ILPを妨げるデータ・制御・構造の3種ハザードの概念図

  • データハザード: 上の例のように、前の命令の結果を次の命令が使う依存(RAW)。最も頻繁に起きる
  • 制御ハザード: 分岐命令(if文・ループ)の行き先が確定するまで、次にどの命令を実行すべきか分からない。分岐予測で緩和する
  • 構造ハザード: 同じ実行ユニット(例: 1つしかない除算器)を複数の命令が同時に使いたがる。ユニットを増やすか順番待ちで解決する

ハザードがあるとILPが制限され、実行ユニットを増やしてもIPCが上がりません。とくにデータハザードと制御ハザードは厄介で、これらをいかに回避・隠蔽するかが現代CPUの設計の腕の見せどころです。

データハザードの一部はフォワーディング(バイパス) という仕組みで緩和できます。命令1の結果を、WBでレジスタに書き戻すのを待たず、EXの直後に直接次の命令へ転送する工夫です。これにより一部のRAW依存はストールなしで処理できます。しかし、ロード命令の結果を直後の命令が使うようなケース(ロード遅延)や、分岐の結果待ちは、フォワーディングだけでは隠しきれません。こうした「どうしても生じる待ち時間」を有効活用する仕組みが、次に述べるアウトオブオーダー実行なのです。

なお、ハザードの頻度はプログラムの性質に強く依存します。行列積のように独立した積和演算が大量に並ぶコードはILPが高くスーパースカラの恩恵を受けやすい一方、ポインタを辿るような処理(リンクリスト探索など)は依存の鎖が長くILPが乏しく、いくら広いCPUでも速くなりません。同じCPUでもプログラムによってIPCが大きく変わるのはこのためです。

では、データハザードでパイプラインが止まってしまうとき、その「待ち時間」を有効に使う方法はないのでしょうか。実はあります。それがアウトオブオーダー実行です。

インオーダー vs アウトオブオーダー実行

命令をプログラムに書かれた順序のまま実行する方式をインオーダー実行と呼びます。シンプルですが弱点があります。前の命令が遅い(たとえばキャッシュミスするロード命令)と、それより後ろの命令が独立であっても、順番待ちで止まってしまうのです。

インオーダー実行とアウトオブオーダー実行のタイミング比較

上段(インオーダー)を見てください。命令2が遅いロードで何サイクルもかかると、命令3はそれと無関係(独立)なのに、命令2の完了を待たされて遅れています。これは大きな無駄です。

下段のアウトオブオーダー実行(OOO: Out-of-Order) では、命令2の完了を待たずに、独立した命令3を先に実行します。プログラムの順序を入れ替えてでも、実行できる命令を見つけて隙間を埋めるのです。結果として実行ユニットの稼働率が上がり、IPCが向上します。

ただし、勝手に順序を入れ替えると問題が起きないのでしょうか。プログラムの正しさを保ったまま順不同に実行するには、2つの巧妙な仕組みが必要です。次節で見ていきます。

リオーダーバッファとレジスタリネーミング

アウトオブオーダー実行を支える中核技術が、リオーダーバッファ(ROB)レジスタリネーミングです。

まずレジスタリネーミングから。先ほどのデータ依存(RAW)は本物の依存ですが、実は「偽の依存」も存在します。たとえば次の2命令を考えます。

命令A: R1 = R2 + R3
命令B: R1 = R4 + R5   ← 同じR1に書く(中身は独立)

命令Bは命令Aと計算内容は無関係ですが、たまたま同じレジスタ $R1$ に書こうとしているため、順序を入れ替えると最終的な $R1$ の値がおかしくなります(WAW: Write After Write 依存)。これは本質的な依存ではなく、「レジスタの名前が足りないせいで生じた偽の依存」です。

レジスタリネーミングは、プログラム上のレジスタ名を、物理的にたくさんある内部レジスタに動的に割り当て直すことで、この偽の依存を解消します。命令Aと命令Bの $R1$ を別々の物理レジスタに割り当てれば、2つは完全に独立になり、並列・順不同に実行できるようになります。

次にリオーダーバッファ(ROB) です。命令を順不同に実行すると、結果が出てくる順番もバラバラになります。しかしプログラムの見かけ上は「元の順序で正しく実行された」ように見せる必要があります。ROBは実行された結果を一時的に保持し、プログラム順に並べ直してから確定(コミット) する仕組みです。

アウトオブオーダー実行の流れ。リネームと発行とROBでのコミットの概念図

全体の流れはこうです。命令はプログラム順にフェッチされ、デコード時にレジスタリネーミングで偽の依存を取り除きます。その後は順不同に発行・実行して隙間を埋め、最後にROBが元の順序へ並べ直して結果を確定します。「内部はバラバラに高速実行、外から見ると正しい順序」を両立させているのです。これが現代の高性能CPUの基本構造です。

アウトオブオーダー実行は隙間を埋めますが、制御ハザード(分岐)だけは別の対策が要ります。分岐の行き先が分からないと、そもそも次にフェッチすべき命令が決まらないからです。これを解決するのが分岐予測です。

分岐予測との関係

if文やループのような分岐命令では、「条件が成立するか」が実行されるまで分からず、次にどの命令を取ってくるべきかが確定しません。これが制御ハザードです。広いスーパースカラほど、1サイクルに多数の命令を投入するので、分岐で止まると損失が大きくなります。

そこで現代CPUは分岐予測を使い、「たぶんこちらに進むだろう」と予測して、確定前から投機的に命令を実行し続けます。予測が当たればパイプラインは止まらず、ILPを最大限に活かせます。外れた場合は、投機実行した結果をROBの仕組みで捨て(コミットしない)、正しい方からやり直します。

つまり、スーパースカラ・アウトオブオーダー・分岐予測は三位一体です。広い発行幅で多くの命令を流し(スーパースカラ)、依存で止まらないよう順不同に実行し(OOO)、分岐で途切れないよう先読みする(分岐予測)。この3つが揃って初めて、高いIPCが実現します。分岐予測の精度が99%を超える現代CPUでは、投機実行が性能の大きな部分を支えています。

予測が外れたときのペナルティは、パイプラインが深く発行幅が広いほど大きくなります。深いパイプラインでは投機実行した命令が何十個も溜まっており、予測ミスのたびにそれらを破棄して正しい経路から再フェッチするため、十数サイクル分の損失が生じます。だからこそ現代CPUは、過去の分岐履歴を学習する高度な予測器(2レベル分岐予測やニューラル分岐予測)に多大なハードウェア資源を割いています。投機実行はまた、近年では情報漏洩(SpectreやMeltdownといった脆弱性)の温床にもなり、性能と安全性の両立という新たな課題も生んでいます。

ここまでで仕組みは出そろいました。では、実際のCPUはどれくらい「広い」のでしょうか。代表的なプロセッサの発行幅を見てみましょう。

実CPUの発行幅

CPUが1サイクルに何命令をデコード・発行できるか(発行幅)は、世代とともに広がってきました。

Pentiumから Apple M1・Zen4までの実CPUの発行幅の推移の棒グラフ

1993年のPentiumは2-way(2命令同時)でした。その後IntelのCore 2やSkylakeは4-way、AMD Zen4は約6-wayです。とりわけ注目されるのが、AppleのM1(Firestormコア)の8-wayという非常に広い設計です。Apple M系がクロックを抑えながら高性能なのは、この広い発行幅と巨大なROB・物理レジスタで多くの命令を同時に処理し、高いIPCを稼いでいるからです。x86が比較的狭め(4〜6-way)にとどまるのは、命令長が可変で同時にデコードしにくいという事情もあります。

しかし、発行幅をどんどん広げれば性能も比例して上がるのでしょうか。実はそうはなりません。プログラムに含まれるILPそのものに限界があるからです。最後に、これをシミュレーションで確かめます。

Pythonで依存グラフから理論IPCを計算する

プログラム中の命令の依存関係は、有向グラフ(依存グラフ)で表せます。ノードが命令、矢印が「この命令はあの命令の結果を待つ」という依存です。このグラフから、理論的に到達できるIPCの上限を計算してみましょう。

考え方はこうです。同時に実行できる命令の最大数は、依存グラフを「依存の段(レベル)」に分けたときの並びで決まります。全体を最短何段で実行し切れるか(クリティカルパス長)が分かれば、理論IPCは「命令数 ÷ クリティカルパス長」で求まります。

import numpy as np

# 命令の依存関係: {命令: [依存している命令のリスト]}
# 例: 命令4は命令1と命令2の結果を待つ
deps = {1: [], 2: [], 3: [], 4: [1, 2], 5: [2, 3], 6: [4, 5]}

def earliest_level(deps):
    """各命令を最も早く実行できる段(レベル)を求める"""
    level = {}
    for inst in sorted(deps):
        if not deps[inst]:
            level[inst] = 0  # 依存なし → 最初の段で実行可
        else:
            level[inst] = 1 + max(level[d] for d in deps[inst])
    return level

level = earliest_level(deps)
n_inst = len(deps)
critical_path = max(level.values()) + 1   # 段数 = クリティカルパス長
theoretical_ipc = n_inst / critical_path

print("各命令の実行段:", level)
print(f"命令数={n_inst}, クリティカルパス長={critical_path}")
print(f"理論IPC = {n_inst}/{critical_path} = {theoretical_ipc:.2f}")

このコードは各命令を「依存元の最大段+1」に配置し、最も深い段の数(クリティカルパス長)を求めています。実行すると、6命令・クリティカルパス長3で理論IPC=2.0となります。つまり、この命令列はどんなに広いハードを用意しても、依存の鎖(命令1or2 → 命令4or5 → 命令6)のせいでIPC2.0が上限です。

これを発行幅 $W$ を変えながら可視化すると、ILPの限界がはっきり見えます。

命令依存グラフから理論IPCを求める図と発行幅を広げてもILP上限で頭打ちになるグラフ

左の依存グラフでは、クリティカルパス長が3なので理論IPCは $6/3=2.0$ です。右のグラフを見ると、発行幅 $W$ を1から2に広げるとIPCは2.0まで上がりますが、それ以降はいくら発行幅を広げても2.0で頭打ちになります。プログラムが本質的に持つILP以上には並列化できないのです。

この「ILPの壁」こそ、CPUが発行幅を無限に広げない理由です。発行幅を広げるとハードの複雑さ・消費電力は急増しますが、実プログラムのILPはせいぜい数命令分しかありません。そこで業界は方向を変えました。1つのコアでILPを掘り続ける代わりに、コアを複数並べる(マルチコア) ことでスレッドレベルの並列性を活かし、またSIMD(1命令で複数データを処理)でデータレベルの並列性を活かす方向へ進んだのです。スーパースカラの限界が、マルチコア時代を呼び込んだと言えます。

まとめ

本記事では、スーパースカラの仕組みを段階的に解説しました。

  • IPC=1の限界: スカラ・パイプラインは1サイクル1命令しか発行できず、IPCは1が上限
  • スーパースカラ: 複数の実行ユニットで1サイクルに複数命令を同時発行し、IPC>1を狙う。VLIWはこれをコンパイラに任せる方式
  • ILPとハザード: 同時実行できる命令の量(ILP)は、データ・制御・構造の3種ハザードで制限される
  • アウトオブオーダー実行: 独立な命令を順不同に実行して隙間を埋める。レジスタリネーミングで偽の依存を消し、ROBで元の順序に並べ直して確定する
  • 分岐予測: 制御ハザードを投機実行で隠蔽し、広いパイプラインを止めない
  • ILPの限界: 発行幅を広げてもプログラムのILP以上には並列化できない。これがマルチコア・SIMDへの転換点になった

スーパースカラとアウトオブオーダー実行は、現代のあらゆる高性能CPUの基盤です。ここで身につけた「依存関係が並列性を決める」という視点は、マルチコアプログラミングやGPU・行列演算の並列化、さらには深層学習の分散学習にもそのまま通じます。

次のステップとして、以下の記事も参考にしてください。