科学・技術
80386の早期開始メモリアクセス
80386 Early Start Memory Access (nand2mario.github.io)
要約
この記事は、Intel 80386プロセッサの「早期開始(Early Start)」という、メモリレイテンシを隠蔽する技術について詳細に解説しています。これは、現在の命令の最終サイクルで次の命令のアドレス計算を開始することで性能を向上させるもので、同機能を持たなかったFPGAコアz386に実装したところ、Doomなどのベンチマークでao486クラスの性能を達成しました。また、この早期開始が有名なPOPADバグの原因でもあることも説明しています。
全文翻訳
Intelが80386を設計した際、彼らはメモリレイテンシを隠蔽する巧妙なトリック、「早期開始(Early Start)」を組み込みました。386は、命令がメモリマイクロオペレーションに到達するのを待つ代わりに、現在の命令の最終サイクルで次の命令のアドレス処理(実効アドレス、セグメント再配置、バスサイクル)を開始します。Intelは、これによって全体的なパフォーマンスが約9%向上すると見積もっていました。また、これはPOPADバグの原因でもあります。
私が5月にリリースしたz386 FPGAコアは、オリジナルの386マイクロコードを実行しましたが、早期開始機能は持っていませんでした。この1ヶ月間で、私はそれを一連の他の最適化とともに実装し、z386は現在ao486クラスのパフォーマンスに到達しています。
| | Doom (FPS) | 3DBench | Landmark |
|------------------|------------|---------|----------|
| z386 0.1 (May) | 16.6 | 33.7 | 147 |
| z386 0.4 (June) | 23.0 | 44.5 | 170 |
| ao486 | 21.0 | 43.8 | 204 |
Doom(オリジナル、最大詳細設定)は約39%向上し(16.6 → 23.0)、ao486の21.0を上回り、16ビットの3DBenchも現在ao486をわずかに上回っています。ボードのクロックはv0.1の85 MHzから変更されていないため、この性能向上は完全にCPI(Clock cycles per instruction)を削減し、クロックあたりにより多くの処理を行うことによって達成されました。命令ごとでは、z386はほとんどすべての命令で、386のサイクル数を大幅に上回っていたものから、同等かそれ以下になりました。
命令のタイミング: z386 0.1 → 0.4 vs オリジナルの80386。
このシリーズの以前のメモリパイプラインに関する投稿で、早期開始が概念として紹介されました。この投稿は、FPGA上でそれを構築することに加え、z386をパリティに到達させた残りのCPI削減作業についてです。
早期開始
Intelは、SlagerのICCD '86論文「80386のパフォーマンス最適化」で早期開始について議論しました。その仕組みの手がかりはマイクロコードにあります。メモリオペランドを読み取るALU命令(ADD reg, [mem])のエントリは次のとおりです。
```
; ADD/OR/ADC/SBB/AND/SUB/XOR m,r
04A EFLAGS -> FLAGSB FLGSBA RD 9
04B DLY
04C OPR_R -> TMPB WRITE_RESULT JMP UNL
04D TMPB SRCREG +-&|^
```
興味深いのは、最初のマイクロ命令04AがすでにRDを発行していることです。つまり、メモリ読み取りを開始しています。その前に、実効アドレスを計算したり、セグメントベースを追加したり、リミットをチェックしたりするマイクロ命令はありません。アドレス生成は暗黙的で、ハードワイヤードロジックによって行われます。
具体的な例でこれを明確にしましょう。
```
add eax, 16
mov ebx, [eax+4]
```
実行順序では、マイクロコードは以下の表のように実行されます。23行目はALU(EAX + 16)を実行し、RNI(「次の命令を実行」)をアサートします。これにより、マシンはすでに次のMOV r,mの開始をコミットしています。24行目は結果をEAXに書き戻し、その同じ24サイクルが次の命令(ロード)の早期開始ウィンドウになります。
| cycle | add eax, 16 | mov ebx, [eax+4] |
|-------|-----------------------------|--------------------------------------------------------------------------------|
| 1 | 023: EAX + 16 in the ALU, RNI | — |
| 2 | 024: write EAX (= old EAX + 16) | early-start window: peek at the next instruction, forward the just-produced EAX, compute EA = EAX + 4, relocate, and issue RD |
| 3 | — | 019: RD microcode |
| 4 | — | 01A: DLY data arriving, write OPR_R |
| 5 | — | 01B: RNI |
| 6 | — | 01C: OPR_R -> EBX |
このオーバーラップにより、メモリアクセスが少なくとも1サイクル早く開始され、ロード/ストアのレイテンシが削減されます。微妙な点は、前の命令の最後のマイクロ命令がレジスタに書き戻す可能性があり、データハザードを発生させることです。ここではEAXがそのまさにサイクルに書き込まれているため、その新しい値はまだレジスタファイルにありません。解決策は通常のものです。つまり、フォワーディングネットワークを使用して、早期開始が最新の値を見るようにします。386DXのフォワーディングネットワークには、有名なPOPADバグを引き起こすコーナーケースバグがありました。POPADの後に[EAX+...]を使用する命令が続く場合、早期開始機構が間違った値をフォワードしてしまうのです。
早期開始を別の方法で考えると、マクロ命令の粒度での粗いパイプライン処理と捉えることができます。前の命令の最終サイクル(RNI遅延スロット)がその命令のライトバックステージであり、それが次の命令の最初のサイクル、つまり早期開始サイクルとオーバーラップします。
早期開始の実装
z386は、各命令を小さなライフサイクルで追跡します。ここで重要な2つのイベントは、i_pop(命令がプリフェッチキューから取り出されるサイクル。前の命令のRNI遅延スロットにあたる)とi_first(自身のマイクロコードの最初のサイクル)です。i_popは、上記のサイクル2の386の早期開始ウィンドウとまったく同じです。
したがって、z386での早期開始は次のとおりです。i_popで、インフライトのレジスタ書き込みをフォワーディングしながら、実効アドレスと線形アドレスを組み合わせ的に計算します。デコーダはベース/インデックス/ディスプレイスメントセレクタを生成します。
```
wire [31:0] ea_early = calc_ea_core(fwd_onehot_gpr(ea_dec_base_sel_r), fwd_onehot_gpr(ea_dec_index_sel_r), ...);
```
fwd_onehot_gprはバイパスです。前の命令の遅延スロットライトバックがEAのベースまたはインデックスレジスタをターゲットにしている場合、レジスタファイルのコピーの代わりにライトバック値(dest_value)を代入します。部分的な書き込みはレジスタの一部のみを更新するため、バイト、ワード、Dワードの書き込みを個別に処理します。
```
FWD_BLO: fwd_onehot_gpr = {cur[31:8], dest_value[7:0]}; // AL
FWD_W: fwd_onehot_gpr = {cur[31:16], dest_value[15:0]}; // AX
default: fwd_onehot_gpr = dest_value; // EAX
```
スタックポインタもforwarded_espを介して同様の処理を受けます。これにより、ESPを調整する命令の直後のプッシュでも新しい値が見られます。ea_earlyはi_popでea_regに登録され、i_firstでロード/ストアマイクロコードの準備ができます。機能的には、これは386のハードワイヤードEAジェネレータとまったく同じであり、POPADを含む、マイクロコードが静かに依存しているのと同じフォワーディングのコーナーケースを再現します。フォワードされたGPRとスタックポインタが準備されると、早期開始サイクルは実効アドレスを計算し、次にリロケーション(セグメントベースの追加)を行って線形アドレスを生成します。このパスはタイミングのホットスポットであることが判明し、その長さをサイクル予算内に収めるためにかなりの試行錯誤が必要でした。
メモリアクセスのさらなる高速化
早期開始の約9%は重要ですが、30%を超えるにはさらなる作業が必要でした。そのほとんどはメモリアクセスパイプの短縮です。
ストアキューの厳格化。ストアは386では2サイクルかかるところを3サイクルかかっていました。CPUで書き込みレイテンシを削減する一般的な方法は、ストアキューを使用することです。CPUは、直接メモリに書き込む代わりに、保留中の書き込みを小さなキューにバッファリングします。z386にはすでに3エントリのストアキューがありましたが、そのインターフェースが保守的すぎて1サイクル無駄にしていました。遅延(DLYマイクロオペレーション)を早期に解放することで、そのサイクルを取り戻しました。
i_firstでの読み書きの発行。i_firstは命令の最初のサイクルであり、早期開始によりほとんどの読み書きは自然にここで発行されます。しかし、古いメモリパイプは、TLBルックアップとメモリ/キャッシュリクエストを2つのサイクルに分割することがありました。それらを両方ともi_firstサイクルに統合することで、早期開始に加えてさらに1サイクル節約できます。
以下は、キャッシュヒット時にストールがない場合のメモリパイプの動作例です。
```
; ADD/OR/ADC/SBB/AND/SUB/XOR r,m
i_pop forward GPR, set IND(early-EA), relocate
027 RD TLB, cache request
028 DLY tag compare, write OPR_R
029 OPR_R->TMPB OPR_R ready
02A RNI
02B SIGMA->DSTREG
```
キャッシュの分割。キャッシュのジオメトリを試した結果、16KB+16KBの分割設計が最も効果的であることがわかりました。これはao486のキャッシュの2倍のサイズで、新しいメモリパイプではよりシンプルなPIPT(物理インデックス、物理タグ)キャッシュがより適していたため、それも採用しました。分割キャッシュは、コード(プリフェッチャによって読み取られる)とデータ(データパスによって読み取られる)がめったに重ならないという事実を利用します。また、iキャッシュは読み取り専用であるため、より面積効率が良いです。唯一の複雑な点は、スヌーピングプロトコルで両方のコヒーレンシを維持することです。コードは、プログラムがロードされたとき、またはよりまれに自己修正するときに、データパスによって書き込まれます。
早期分岐リダイレクト
z386の目標は、80386と100%サイクル正確であることではありません。むしろ、目標は正確な動作であり、オリジナルのマイクロコードがそのほとんどの作業を行っています。少し余分なロジックで多くのパフォーマンスが得られる場合や、FPGAプリミティブが重い処理を行う場合は、より高速な設計を採用します。乗算器はそのようなケースの1つで、FPGA DSPブロックが多くのサイクルを節約しました。早期分岐リダイレクトは、少しの面積で386よりも高速になる例です。jmp rel、call rel、または取られるjccのような直接相対分岐の場合、ターゲットは単にEIP +ディスプレイスメントであり、デコード時にレジスタまたはメモリの依存性なしに完全にわかります。これらはしばしばパフォーマンス上重要ですが、386はマイクロコードが分岐を解決した後にのみプリフェッチャをリダイレクトしていました。そのため、z386