コンピュータアーキテクチャにおいて、遅延スロットとは、先行する命令の影響を受けずに実行される命令スロットのことである。[ 1 ]最も一般的な形式は、RISCまたはDSPアーキテクチャ上の分岐命令の直後に配置された単一の任意の命令である。この命令は、先行する分岐が実行された場合でも実行される。これにより、元のアセンブリ言語コードにおける位置と比較して、命令の実行順序が変わる。
現代のプロセッサ設計では、一般的に遅延スロットは使用されず、代わりにますます複雑な分岐予測が行われます。これらのシステムでは、CPUは分岐の正しい側であると判断された側に即座に移行するため、コンパイル時に必ずしも明らかではない無関係な命令をコードで指定する必要がなくなります。想定が間違っていて、分岐の反対側を呼び出す必要がある場合は、長い遅延が発生する可能性があります。しかし、このような遅延はまれにしか発生しないため、遅延スロットを回避することによる高速化は、誤った判断の回数が少ないことで容易に相殺されます。
中央処理装置は一般的に、機械語からの命令を4つのステップで実行します。まずメモリから命令を読み込み、次にデコードして実行すべき内容を理解し、それらのアクションを実行し、最後に結果をメモリに書き戻します。初期の設計では、これらの各段階が直列に実行されていたため、命令の完了にはマシンのクロックサイクルの何倍かの時間が必要でした。たとえば、Zilog Z80では、命令を完了するために必要な最小クロック数は4でしたが、一部の(まれな)命令では最大23クロックになる場合もありました。[ 2 ]
命令処理のどの段階においても、チップの1つの部分のみが関与します。たとえば、実行段階では、通常、算術論理演算ユニット(ALU)のみがアクティブであり、メインメモリとやり取りしたり命令をデコードしたりする他のユニットはアイドル状態です。コンピュータの全体的なパフォーマンスを向上させる方法の1つは、命令パイプラインを使用することです。これは、命令がユニットを通過する際に、命令の中間状態を保持するための追加回路を追加します。これは個々の命令のサイクルタイミングを改善するものではありませんが、前の命令が次の命令に進んだ後に、2番目の命令が他のCPUサブユニットを使用できるようにするという考え方です。[ 3 ]
例えば、ある命令がALUを使用している間に、プログラムの次の命令はデコーダにあり、3番目の命令はメモリからフェッチされる。このようなアセンブリライン型の構成では、パイプラインステージの数だけ、一度に処理できる命令の総数を増やすことができる。例えば、Z80では、4段パイプラインによって全体のスループットを4倍に向上させることができた。しかし、命令タイミングの複雑さから、これを実装するのは容易ではなかった。MOS 6502の命令セットアーキテクチャ(ISA)ははるかに単純で、 2段パイプラインを組み込むことができたため、任意のクロック速度でZ80の約2倍のパフォーマンスを実現できた。[ 4 ]
初期のシステムにおけるパイプラインの実装における大きな問題は、命令のサイクル数が大きく異なっていたことでした。例えば、2つの値を加算する命令は、データの読み込み場所によって異なる複数のバージョン(オペコード)で提供されることがよくありました。あるバージョンでは、プロセッサのレジスタaddにある値を別のレジスタの値に加算し、別のバージョンでは、メモリにある値をレジスタに加算し、さらに別のバージョンでは、あるメモリ位置の値を別のメモリ位置に加算していました。これらの命令はそれぞれ、メモリ内で表現するために異なるバイト数を必要とするため、フェッチに要する時間も異なり、値を取得するためにメモリインターフェースを複数回通過する必要がある場合もありました。これはパイプラインロジックを非常に複雑にしていました。RISCチップ設計コンセプトの目標の1つは、これらのバリエーションを排除してパイプラインロジックを簡素化することであり、その結果、1サイクルごとに1つの命令を実行する古典的なRISCパイプラインが誕生しました。
しかし、パイプラインシステムにはパフォーマンスを低下させる可能性のある問題が1つあります。これは、次の命令が前の命令の結果に応じて変化する場合に発生します。ほとんどのシステムでは、これは分岐が発生したときに起こります。たとえば、次の擬似MIPSアセンブリを考えてみましょう。
top: lw t0 , 0 ( a0 ) ; レジスタ a0 のメモリ アドレスからレジスタ t0 に値をロードします。lw t1 , 4 ( a0 ) ; レジスタ a0 の値に 4 を加えたメモリ アドレスからレジスタ t1 に値をロードします。add t2 , t0 , t1 ; レジスタ t0、t1 の値をレジスタ t2 に加算します。 sw t2 , 0 ( a1 ) ; レジスタ t2 の値をレジスタ a1 のメモリ アドレスに格納します。lw t4 , 8 ( a0 ) ; レジスタ a0 の値に 8 を加えたメモリ アドレスからレジスタ t4 に値をロードします。この場合、プログラムは線形であり、容易にパイプライン化できます。最初のlw(ロードワード)命令が読み込まれてデコードされるとすぐに、2番目のlw命令をメモリから読み込むことができます。最初の命令が実行に移ると、add2番目の命令がデコードされている間にlw、2番目の命令がメモリから読み込まれます。以下同様です。最初の命令の完了には依然として同じ数のサイクルが必要ですがlw、完了する頃には2番目の命令の値が準備できており、CPUはすぐにそれらを加算できます。パイプライン化されていないプロセッサでは、最初の4つの命令の完了に16サイクルかかりますが、パイプライン化されたプロセッサではわずか5サイクルで済みます。
次に、ブランチが追加された場合に何が起こるかを考えてみましょう。
top: lw t0 , 0 ( a0 ) ; レジスタ a0 のメモリ アドレスからレジスタ t0 に値をロードしますlw t1 , 4 ( a0 ) ; レジスタ a0 の値 + 4 のメモリ アドレスからレジスタ t1 に値をロードしますadd t2 , t0 , t1 ; レジスタ t0、t1 の値をレジスタ t2 に加算しますli t3 , 1000 ; レジスタ t3 を 1000 に設定しますbgt t2 , t3 , top ; t2 > t3 の場合、「top:」に戻ります; それ以外の場合: sw t2 , 0 ( a1 ) ; レジスタ t2 の値をレジスタ a1 のメモリ アドレスに格納しますlw t4 , 8 ( a0 ) ; レジスタ a0 の値 + 8 のメモリ アドレスからレジスタ t4 に値をロードしますこの例では、4行目の比較結果によって「次の命令」が変わります。次の命令はsw、メモリへの次の命令(ストアワード)になる場合もあれば、lwメモリの先頭にある命令になる場合もあります。プロセッサのパイプラインは通常、swALUがどのパスに進むかを計算するまでに、次の命令を既に読み込んでいます。これは分岐ハザードとして知られています。先頭に戻る必要がある場合は、sw命令を破棄し、lw代わりにメモリから命令を読み込まなければなりません。これには少なくとも1つの命令サイクルが必要となり、パイプラインが少なくとも1命令分の間空になります。これは「パイプラインストール」または「バブル」として知られており、コード内の分岐の数によっては、全体的なパフォーマンスに顕著な影響を与える可能性があります。
この問題に対処する戦略の一つとして、遅延スロットを使用する方法があります。遅延スロットとは、完了に時間がかかる命令の後の命令スロットのことです。上記の例では、より多くの時間を必要とする命令は分岐命令であり、これは最も一般的なタイプの遅延スロットであり、一般的には分岐遅延スロットと呼ばれます。
初期の実装では、分岐命令の後に続く命令は、パイプラインを埋めてNOPタイミングが適切になるように、つまりNOPメモリからがロードされるまでに分岐が完了し、プログラムカウンタが正しい値で更新されるように、何もしない命令、またはで埋められていました。この単純な解決策は、利用可能な処理時間を無駄にします。より高度な解決策では、代わりに、通常はコード内の近くにある別の命令を特定して遅延スロットに配置し、有用な処理を実行しようとします。
上記の例では、lw末尾の(ロード)命令は完全に独立しており、他の情報に依存せず、いつでも実行できます。そのため、分岐遅延スロットに配置するのに適しています。通常、これはアセンブラプログラムまたはコンパイラによって自動的に処理され、命令の順序が変更されます。
top: lw t0 , 0 ( a0 ) ; レジスタ a0 のメモリ アドレスからレジスタ t0 に値をロードしますlw t1 , 4 ( a0 ) ; レジスタ a0 の値 + 4 のメモリ アドレスからレジスタ t1 に値をロードしますadd t2 , t0 , t1 ; レジスタ t0、t1 の値をレジスタ t2 に加算しますli t3 , 1000 ; レジスタ t3 を 1000 に設定しますbgt t2 , t3 , top ; t2 > t3 の場合、「top:」に戻ります; 遅延スロット (無条件に実行されます) lw t4 , 8 ( a0 ) ; レジスタ a0 の値 + 8 のメモリ アドレスからレジスタ t4 に値をロードします; それ以外の場合: sw t2 , 0 ( a1 ) ; レジスタ t2 の値をレジスタ a1 のメモリ アドレスに格納します分岐命令が実行されると、次の命令が実行されます。その命令がプロセッサに読み込まれ、デコードが開始される頃には、比較結果が準備できており、プロセッサは次に読み込む命令(先頭の命令lwかsw末尾の命令か)を決定できます。これにより、時間の無駄がなくなり、パイプラインは常に満杯の状態を維持できます。
スロットを埋める命令を見つけるのは難しい場合があります。コンパイラは通常、検査できる「ウィンドウ」が限られているため、そのコード範囲内で適切な命令が見つからない可能性があります。さらに、命令は分岐内のデータに依存してはなりません。命令がadd以前の計算結果を入力として受け取る場合、その入力は分岐先のコードの一部であってはなりません。レジスタリネーミングが存在する場合、これが正しいかどうかを判断するのは非常に複雑になる可能性があります。レジスタリネーミングでは、コンパイラが認識しないまま、プロセッサがコードで指定されたレジスタ以外のレジスタにデータを配置する可能性があるためです。
もう1つの副作用は、分岐遅延スロット内でのデバッグ中に、命令のブレークポイントを管理したりステップ実行したりする際に、特別な処理が必要になることです。分岐遅延スロット内では割り込みは発生せず、分岐遅延スロットの後に延期されます。 [ 5 ] [ 6 ] 分岐命令を分岐遅延スロットに配置することは禁止または非推奨です。[ 7 ] [ 8 ] [ 9 ]
特定のパイプライン実装における理想的な分岐遅延スロット数は、パイプラインステージ数、レジスタ転送の有無、分岐条件が計算されるパイプラインのステージ、分岐ターゲットバッファ(BTB)の使用の有無、その他多くの要因によって決まります。ソフトウェアの互換性要件により、アーキテクチャは世代間で遅延スロット数を変更することはできません。そのため、新しいハードウェア実装では、アーキテクチャの動作がもはや適切ではないにもかかわらず、その動作が確実に実行されるように、追加のハードウェアが必要となります。
分岐遅延スロットは、主にDSPアーキテクチャと古いRISCアーキテクチャに見られます。MIPS 、PA-RISC(遅延分岐または非遅延分岐を指定できます)、[ 10 ] ETRAX CRIS、SuperH(無条件分岐命令には1つの遅延スロットがあります)、[ 11 ] Am29000、[ 12 ] Intel i860(無条件分岐命令には1つの遅延スロットがあります)、[ 13 ] MC88000(遅延分岐または非遅延分岐を指定できます)、[ 14 ]およびSPARCは、それぞれ1つの分岐遅延スロットを持つRISCアーキテクチャです。PowerPC 、ARM、Alpha、V850、およびRISC-Vには、そのようなスロットはありません。それぞれ1つの分岐遅延スロットを持つDSPアーキテクチャには、 μPD77230 [ 15 ]およびVS DSPが含まれます。SHARC DSPとMIPS-Xは、2つの分岐遅延スロットを使用します。[ 16 ]このようなプロセッサは、分岐命令が有効になる前に、分岐命令に続く一対の命令を実行します。TMS320C3x [ 17 ]とTMS320C4x [ 8 ]はどちらもトリプル分岐遅延スロットを使用します。TMS320C4x には、遅延なし分岐と遅延あり分岐の両方があります。[ 8 ]
以下の例は、SHARC DSP 用のアセンブリ言語による遅延分岐を示しています。RTS 命令の後に一対の遅延分岐命令が含まれています。レジスタ R0 から R9 は、番号順にゼロにクリアされます (R6 の後にクリアされるレジスタは R7 であり、R9 ではありません)。どの命令も複数回実行されることはありません。
R0 = 0 ; CALL fn ( DB ); /* 下記のラベル「fn」で関数を呼び出す */ R1 = 0 ; /* 最初の遅延スロット */ R2 = 0 ; /* 2 番目の遅延スロット */ /***** ここで不連続 (CALL が有効になる) *****/R6 = 0 ; /* CALL/RTS は「R1 = 0」ではなく、ここで戻ってきます */ JUMP end ( DB ); R7 = 0 ; /* 最初の遅延スロット */ R8 = 0 ; /* 2 番目の遅延スロット */ /***** ここで不連続 (JUMP が有効になります) *****//* 次の 4 つの命令は、関数 "fn" として上から呼び出されます */ fn : R3 = 0 ; RTS ( DB ); /* 呼び出し元の遅延スロットを過ぎて呼び出し元に戻ります */ R4 = 0 ; /* 最初の遅延スロット */ R5 = 0 ; /* 2 番目の遅延スロット */ /***** ここで不連続 (RTS が有効になります) *****/終了: R9 = 0 ;ロード遅延スロットとは、メモリからレジスタをロードした直後に実行される命令ですが、ロードの結果を参照したり、結果を待つ必要もありません。現代のハードウェアではロード遅延が非常に予測しにくいため、ロード遅延スロットは非常にまれです。ロードはRAMまたはキャッシュから実行される可能性があり、リソース競合によって遅くなる場合があります。ロード遅延は、ごく初期のRISCプロセッサ設計で一般的に見られました。MIPS I ISA(R2000およびR3000マイクロプロセッサに実装)には、このようなスロットがあります。
以下の例は、ロード遅延スロットと分岐遅延スロットの両方を示すMIPS Iアセンブリコードです。
lw v0 , 4 ( v1 ) # アドレス v1+4 から v0 にワードをロードnop # ロード遅延スロットを無駄にするjr v0 # v0 で指定されたアドレスにジャンプnop # 分岐遅延スロットを無駄にする