命令サイクル — 取り出し、解読、実行
一言でいうと
CPUは、メモリから命令を1つ取り出して(フェッチ)、意味を解読し(デコード)、実際に実行する(実行)という作業を、延々と繰り返す機械です。
なぜ必要なのか
プログラムは、結局のところメモリに並べた数字の塊です。この数字を誰がどんな順序で読んで、何をするかを決めなければなりません。初期の設計者たちが下した決定が今まで続いていて、核心は2つです。1つ目は、命令とデータを同じメモリに置くことです(ノイマン型アーキテクチャ)。2つ目は、次に実行する命令のアドレスを、1つのレジスターに持たせておくことです(プログラムカウンター)。
この2つの決定のおかげで、CPUは非常に単純な規則を1つだけ守ればよいことになります。「プログラムカウンターが指す場所から命令を取り出して実行し、カウンターを次へ進める。」ループも関数呼び出しも条件分岐も、すべてこのカウンターの値を変えることにすぎません。
どう動くのか
1サイクルは、おおよそ次のように流れます。
- フェッチ(Fetch): プログラムカウンターが指すアドレスから命令を読み、命令レジスターに入れます。
- デコード(Decode): ビットパターンを見て、どの演算か、オペランドがどこにあるかを判断します。
- 実行(Execute): 算術論理装置が計算したり、メモリにアクセスしたり、カウンターを別の場所へ移したりします。
問題は、これを順番にだけ行うと、ほとんどの回路が遊んでしまうことです。デコーダーが働いている間、フェッチ側は休んでいます。そこで登場したのがパイプラインです。洗濯機と乾燥機を分けて使うように、1番目の命令がデコードされるときに、2番目の命令を先にフェッチします。段階が5つなら、理論上、スループットが5倍になります。
タダではありません。パイプラインは、3つのハザード(hazard)を生みます。
| ハザード | 状況 | 対応 |
|---|---|---|
| 構造ハザード | 2つの段階が同じ回路を同時に使いたがる | リソースを分けるか、片方を遅らせる |
| データハザード | 前の命令の結果を、後ろの命令が必要とする | フォワーディング、それでもだめならパイプラインを停止 |
| 制御ハザード | 分岐の結果がまだわからない | 分岐予測、外れたらパイプラインを空にする |
3つ目が特に高くつきます。分岐予測が外れると、すでに進行中だった命令をすべて捨てなければなりません。現代のCPUのパイプラインは15段を超えることもあるので、外れるたびに数十サイクルが失われます。ソート済みの配列を回すループが、ソートされていない配列より数倍速いという有名な現象は、ここから生まれます。条件が規則的なら、予測器が当てるからです。
現場での姿
性能測定をするとき、よく目にする指標がIPC(サイクルあたりの命令数)です。クロックが同じでも、IPCが2倍なら2倍速いです。逆に、IPCが0.3のように低く出るなら、CPUが計算できないからではなく、何かを待っているというシグナルです。たいていはメモリです。
そのため、「CPU使用率100パーセント」をそのまま「演算ユニットが100パーセント計算中」と読んではいけません。コアは非アイドル状態でも、キャッシュミスのようにメモリ階層を待って、実行が停止することがあります。ただし、ブロッキングI/Oで眠ったタスクの時間は、通常そのプロセスのCPU使用時間には数えないので、メモリのレイテンシとI/O待ちを区別する必要があります。
続くクイズで確認すること
パイプラインが、なぜスループットを増やしながら命令1つのレイテンシは減らせないのか、分岐予測の失敗がなぜ高くつくのかを、自分で説明できるかを点検します。