
2026/04/15 1:00
Intel 80386 のメモリ・パイプライン機構についてご質問でしょうか? **メモリ・パイプライン(Memory Pipeline)** とは、CPU がメモリアクセスを高速化する技術で、複数のインストラクションを並列的に処理するため、メモリアクセスのオーバーヘッドを軽減します。Intel 80386 では初めてこの機構が採用され、1979 年にリリースされた x86 アーキテクチャ史上重要なステップとなりました。 具体的な仕組みや詳細な技術内容についてさらに説明が必要でしたら、お知らせください!
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
インテル製 80386 は、単純な逐次処理ではなく、仮想メモリの管理を効率的に行うための複合的な並列マイクロアーキテクチャ機能を活用することで驚異的な速度を実現しています。その核心的な戦略は、処理を順次行うのではなく、複数のタスクを同時に実行することにあります。具体的には、プロセッサは事前計算とパイプライン化を用いて、論理アドレスから実アドレスへの変換を約 1.5 クロックサイクルで完了させます。セグメンテーションは記述子キャッシュによって反復的なテーブル参照を回避し、制限値チェックは線形アドレス計算と並行して実行されます。「Early Start」という最適化手法により、前の命令のサイクル中に処理を開始することでスループットが約 9% 向上しますが、この複雑さが後に生産チップにおいて POPAD バグを引き起こす要因となりました。歴史的に、これらの技術は元のチップが Companion キャッチングチップ(補助キャッシュチップ)を活用して大幅な速度向上(30% から 40%)を実現することを可能にしました。現在では、現代の FPGA インプルメンテーションがこのような動作の多くを再現しており、DOS や Doom のようなアプリケーションを高周波数で実行することが可能です。特に、一つのコアは現在 DE10-Nano で 75 MHz で動作しています。しかし、現在の FPGA デザインではブロック RAM 内の厳格なハードウェアタイミング制約のため、完全なアドレスパイプライン化を実行略することはよくあります。並列性がレガシーチップの設計をどのように推進したかを理解することが依然として重要であり、これは「Early Start」のような技術がもたらす強力なパフォーマンス利点と、最適化の際に過度な複雑性を追加することによって引き起こされる安定性バグなどの歴史的リスクの両方を浮き彫りにしています。
本文
私が現在設計を進めている FPGA 用の 386 コアは、すでに DOS を起動し、Norton Commander のようなアプリケーションを実行したり、Doom のようなゲームを遊んだりする状態まで達しています。DE10-Nano プレート上で動作しているクロック周波数は現在 75MHz です。コアが実用的なソフトウェアを実行できる段階に到達したという好機を捉え、80386 のパフォーマンスを左右する重要なサブシステムの一つである「メモリパイプライン」の詳細を探ってみる良い時だと思いました。
保護モードと仮想メモリの基盤
32 ビット保護モードは、80386 を特徴づける決定的な機能でした。前回の投稿では、その物語の一面として「仮想メモリの保護メカニズム」に取り組みました。ここでは、80386 が専用 PLA(プログラムロジックエリア)、セグメントキャッシュ、およびハードウェア製のページウォーカーを用いてどのように保護を実現しているかを見てきました。今回は別の角度から仮想メモリを探ります:メモリアクセスパイプラインのマイクロアーキテクチャ、アドレス翻訳がどのように効率化されているか、マイクロコードがこのプロセスをどのように駆動しているか、そして設計が達成する RTL タイミングについてです。
80386 のマイクロコードによるメモリアクセス
Intel 製の 80386 プログラマースマニュアルは、80386 のアドレス翻訳を以下のように記述しています:
「80386 は、論理的アドレス(つまり、プログラマが見るアドレス)を物理アドレス(つまり、物理メモリの実際のアドレス)に変換します。この変換は 2 つのステップで成されます:セグメント翻訳…およびページ翻訳…」
ハードウェアを見る前に、まずはマイクロコードから着手するのが有益です。例えば
ADD [BX+4], 8 という ALU 命令(メモリを読み取り、改ざんし、書き戻す操作)に対応するマイクロコードは以下のようになります。
; ADD/OR/ADC/SBB/AND/SUB/XOR m,i 039 EFLAGS -> FLAGSB FLGSBA RD 9 03A DLY 03B OPR_R -> TMPB WRITE_RESULT JMP UNL 03C TMPB IMM +-&|^ ; WRITE_RESULT 046 SIGMA -> OPR_W RNI WR 0 047 DLY
もしあなたがこのシリーズの最初の記事を読み始めたばかりなら、必要な最小限の構文ルールは以下の通りです:
- 行頭の hexadecimal 番号(039, 03A など)はマイクロコードアドレスを示します。
は内部レジスタやバスの間での値移動を表します。SRC -> DST
とRD
はそれぞれメモリアドレスの読み出し開始と書き出し開始を示します。WR
は「メモリアドレスの応答までここで待ちましょう」という意味です。DLY
は ALU の演算結果レジスタです。SIGMA
とOPR_R
はメモリオペランド用の読み込みデータレジスタと書き込みデータレジスタです。OPR_W
は「次の命令を実行せよ」を意味し、つまりこのマイクロルーチンが終了することを示します。RNI
ここで重要なのは以下の 3 つの点です:
がメモリアドレスの読み出しを開始する。RD
がメモリアドレスの書き込みを開始する。WR
は結果が入手可能になるまでマイクロコードが待機する地点である。DLY
注目すべき点は、
RD + DLY や WR + DLY のようなパターンがマイクロコードのあちこちに存在することです。もしアドレス生成、翻訳、およびバス仲裁が遅かったら、システム全体が停滞してしまいます。したがって、興味深い問いは:**「ハードウェアがどのようにこれらの微細な RD および WRフックを安価なものにし、それでもマシン全体が機能するようにしているのか」**となります。
Intel の答えは、通常追加のサイクルだけ(彼らの言い回しならアドレスパイプライン自身で約 1.5 クロック分)をかける専用のアドレス経路(パス)を構築したという点にあります。
エフィシエントなセグメンテーション
ここでは、論理的アドレスを線形アドレスに変換するセグメンテーションの馴染み深い仕組みを示します:
- セグメント翻訳 (図 5-2, 80386 プログラマースマニュアル参照)
セグメンテーションは保護モードにおいてもリアルモードにおいても必須かつ有効です。上記の図は保護モードの場合には直ちに理解でき、セグメントベースアドレスはメモリ上の GDT(グローバル定義テーブル)または LDT(局所定義テーブル)から検索されるためです。しかし、図だけでは明白ではないのは、線形アドレスが一見してテーブル検索を通過しないリアルモードでも、同じセグメント計算が有効であることを知ることです。以下でその点について触れます。
なぜセグメンテーションは高価になり得たのか
上記の
RD/DLY パターンは、マイクロコードとメモリスイステムとの間の契約(合意)を示しています:読み出し命令が発令された直後、マシンの残りはアドレス経路がその作業を速やかに完了することを期待します。単なる命令境界だけでも、そこに余分な余裕(スラック)がどれほど少ないかがわかります。
以下のような一組の命令を考えてみましょう:
MOV AX, 123h ADD [AX+45h], 2
その実行順序でのマイクロコードは以下のようになります:
; MOV r,i 005 IMM PASS RNI 006 SIGMA -> DSTREG ; ADD m,i 039 EFLAGS -> FLAGSB FLGSBA RD 9 03A DLY 03B OPR_R -> TMPB WRITE_RESULT JMP UNL ...
行
006 は AX の新しい値を書き込みます。80386 では命令は背負うように実行されるため、極めて次のサイクル(直後)に、行 039 がその値を効果アドレスの一部として使用するためにメモリアドレスの読み出しを開始しようとします。これにはほとんど余裕がありません。アドレスハードウェアは命令境界で即座に対応する必要があります。
ここでセグメンテーションは単なる正しさを保証する機構からパフォーマンス上の問題へと変化します。もし安直に実装された場合、各アクセスごとにセグメント記述子をフェッチし、ベースを追加し、制限値と比較した後でのみ先に進む必要がありました。それは絶望的に遅いものでした。
キャッシュされたセグメント状態
最初の最適化は、単に各アクセスで記述子の検索を繰り返さないようにすることです。セレクタがセグメントレジスタにロードされた際、プロセッサはその記述子のベース、制限、属性をレジスタの目に見えない部分にもロードします。この隠れた状態(記述子キャッシュと呼ばれる)が存在する理由は、プロセッサが各メモリアクセスで記述子テーブルを確認する必要がないためです。ウェハ写真上で見ると、これは実際にはかなりの空間を占めています。
- 80386 のウェハ。 赤でハイライトされた部分は「記述子キャッシュ」である。(元の画像:Intel 80386 DX のウェハ、Wikimedia Commons より)
これは重要な設計上の選択です。これがなければ、セグメンテーションは各参照に追加のメモリアクセスを必要としたり、はるかに複雑な記述子キャッシュ階層を導入したりする必要がありました。これにより、通常のアクセスではセグメンテーションをローカルな状態として扱い、テーブルウォーク(行列巡回)のようなものにはならないという点が実現されました。さらに、これはアーキテクチャ上の微妙だが重要な特性も生み出します:メモリ上の記述子を変更しても、すでにその記述子をロードしたセグメントレジスタには影響しません。キャッシュされたコピーは、セレクタが再ロードされるまで有効に留まります。
記述子キャッシュはまた、リアルモードのアドレス翻訳を支援します。以下のマイクロコードは、セグメントレジスタへの
MOV 命令(リアルモードおよび保護モード)を示しています:
; r MOV ES/SS/DS/FS/GS,rw 009 DSTREG DES_SR PASS RnI DLY SBRM 0 00A SIGMA -> SEGREG ; p MOV ES/DS/FS/GS,rw 580 DES_SR TST_DES_SIMPLE PTSAV1 DLY SPTR 0 581 LD_DESCRIPTOR LCALL 582 DSTREG -> SLCTR TST_SEL_NONSS PTSELE DLY 583 SLCTR2 -> SEGREG TMPC RNI SDEL 584 DLY
私たちは前回の保護メカニズムに関する投稿で、保護モードのセグメントロードマイクロコードについてすでに話しました。
LD_DESCRIPTOR ルーチンが重い荷を担い、一般的に記述子を対応するセグメント記述子キャッシュにロードします。詳細については、その投稿へご参照ください。
しかし、リアルモードの場合、マイクロコードは非常に異なります。行
009 は特別な操作 SBRM(ベース設定−リアルモード)を使用して、適切なセグメント (DES_SR) のレジスタ (DSTREG) 内の隠れた記述子キャッシュのベース値を変更します。つまり、対応するベースアドレスを seg<<4 に設定しています。この方法により、後続のリアルモードと保護モードでのセグメンテーション処理は同一のロジックで統合され、面積が削減され効率性が向上します。
おそらく気づいたかもしれませんが、リアルモードのルーチンは制限値(リアルモードでは 64KB とすべき)を記述子キャッシュに触れることはありません。さらに、ハードウェアがこの制限値を暗黙的に設定するものも存在しません;実際には、プロセッサ起動時に一度だけ初期化されるだけです。Intel のこの興味深い設計上の選択は、有名な「アンリアルモード(unreal mode)」トリックを可能にします。保護モードに入り、制限値を大きな値に設定してからリアルモードに戻ることにより、ソフトウェアは 4GB のデータセグメントへのアクセスを許すようなリアルモードのバリアントを作成できます。
パラレルなリロケーションと制限チェック
記述子状態がキャッシュされた後、次に問題となるのは実際の算術処理です。線形アドレスを形成するために、プロセッサは以下の計算を行います:
効果的アドレス = ベース + インデックス * スケール + 相対偏移量
線形アドレス = セグメントベース + 効果的アドレス
同時に、効果的アドレスがセグメントの制限内にあることを確認する必要があります。これを効率的に行う方法は、最終的な線形アドレスを計算してから何か調整された限界値と比較するのを待つのではなく、線形アドレスを計算し、制限チェックを並列に進めることです。
- 一つの算術パス: 効果的アドレスにセグメントベースを加算します。
- 別の算術パス: 最後にアクセスしたバイトオフセットをセグメント制限と比較します。
制限テストについてはさらに詳細があります:実際には
オフセット + サイズ - 1 <= 制限 であるかどうかを確認する必要があります。「サイズ」は現在の操作のバイト長です。例えば、0x100 で DWORD(4 バイト)アクセスを行う場合、最後にアクセスされたバイトは 0x103 です。ここでの安直な実装では、同じサイクル内で総和を計算し比較を行うために 2 つのフルアッダ(加法器)をシリアルに必要とします。より良い実装は、制限 - オフセット のようなものを計算することです。そして、残りのスペースがバイト、ワード、または DWORD に十分かどうかを決めるために少量の浅いロジックを使用します。これはトップ 30 ビットがすべてゼロかどうかをチェックするために単一の広い NOR ゲートを使用でき、さらにいくつかのゲートで最低 2 ビットの有効性をチェックすれば十分です。これは Intel の ICCD paper のアドレス翻訳議論で述べられているような最適化に一致します。
80386 は豊かなアドレッシングモードをサポートしています:
EA = base + index*scale + displacement
まず、スケール係数(1, 2, 4, または 8)は 4 重マルチプレクサを用いた固定シフトで処理でき、コストは低いです。その後、2 つ未満の加算項が存在する場合、全体 EA を単一のフルアッダだけで計算できます。しかし、3 つすべての項が存在する場合、私たちは再びシリアルに 2 つのフルアッダを必要とします。Intel の設計者はここでも共通ケースに対して最適化を行いました。効果アドレスハードウェアが高速ケースでは 2 つの 32 ビット項のみを加算する必要があるのであれば、EA は単一のサイクルで計算されます。稀な
base + index*scale + displacement フォームは、各メモリアクセスをより深い組合せ論理を通して強制する代わりに、追加のステップだけを取ることができます。
早期開始 (Early Start)
80386 の最も興味深いメモリオプティマイゼーションの一つが**「早期開始(Early Start)」**です。一部の命令では、アドレス経路はマイクロコードの意味での通常の「命令開始」を待つのではなく、前の命令の最後のサイクルにすでにアドレス関連の作業を開始します。これは前の命令の書き戻しとオーバーラップしています。
私たちが以前見た
MOV AX, 123h に続く ADD [AX+45h], 2 はまさにそのようなケースです。実行順序は以下の通り:
; MOV r,i 005 IMM PASS RNI 006 SIGMA -> DSTREG ; ADD m,i 039 EFLAGS -> FLAGSB FLGSBA RD 9 03A DLY ...
2 つ目のサイクルで、マイクロコードアドレス
006 にて、MOV 命令の結果 (0x123) が AX へ書き込まれます。同じサイクルは、次の ADD 命令のハードウェアによる早期開始ウィンドウでもあります。そのサイクル中に、アドレス経路は事前に先読み(peek)し、次の命令が AX+45h を必要とすることを見出し、直ちに関与した値をバイパス論理を使って効果アドレスを生成します:
EA = 0x123 + 0x45 = 0x158
ADD 命令が公式に 3 つ目のサイクルで開始するまでの間に、オペランド
[AX+45h] のためのメモリアドレス読み込みはすでに外部バスで進行しています。早期開始がなければ、ADD は AX を読み始めアドレスを計算するためにサイクル 3 まで待たねばなりませんでした。これらのハードウェア機能(早期開始)と MOV の最終サイクルを重ねることで、80386 は 1.5〜2 サイクルのアドレス生成レイテンシの多くを実質的に隠しており、マイクロコードがフェッチされたデータが到着するやいなや処理を開始することを可能にします。
Slager は同じ ICCD paper で、早期開始が全体のパフォーマンスを約 9% 向上させることを報告しています。その利益は、待機状態(wait states)のない一般的なメモリアドレス命令のタイミングで明確に現れます:
| 命令カテゴリ | 通常クロック数 |
|---|---|
| ストア (Store) | 2 |
| レジスタプッシュ (Push register) | 2 |
| ロード (Load) | 4 |
| ポップ (Pop) | 4 |
残念なことに、早期開始は実際の複雑性も導入しました。この複雑さは、少なくとも一つの量産版 80386 のバグ、すなわちPOPAD バグの背後にあるようです。このバグは Intel 製のすべての 80386DX ステッピングに存在します。
POPAD の終了時に、EAX の新しい値が IRF(レジスタファイルへの間接アクセス)と呼ばれるメカニズムによってコミットされます。もし次の命令がすぐに
[EAX+4] のような複雑なアドレッシングモードを使用する場合、転送論理はこのケースを正しく処理できません。言い換えれば、マシンの速度を向上させるための正確な最適化は、またしても「先読み」機械装置が誤った値を見ているように見えるコーナーケースを生み出します。これは、早期開始のような最適化がなぜ難しいのかという良い思い出です。それは、彼らがこれほど多くのコーナーケースを導入したから、まさにそのためです。
ページングの高速パス
ページングは、80386 が遅くになり得るもう一つの明らかな場所です。TLB がなければ、すべてのメモリアクセスは実作業が始まる前に追加のテーブル検索を必要とします。私が前回の保護メカニズム投稿でページリング機構そのもの(ハードウェアページウォーカーを含む)についてより詳しく扱ったので、ここではこのセクションを短くします。ここで主な点は、ページングもまた高速パスの一部だということです。TLB ヒットの場合、翻訳は同じオーバーラップしたメモリアドレスパイプラインに適合するほど安価で留まります。ミス(ヒットしない場合)の場合、ハードウェアページウォーカーが引き継ぎ、高価な作業を行ってもそれを大規模なマイクロコードルーチンに変えません。
バスインターフェースとキャッシュ
メモリアルパイプラインの議論を完了させるために、バスインターフェースユニットとキャッシュについて話す必要があります。80386 は 80286 に似ていますが、8086 とは異なり、多重化されていないアドレス/データバスを使用します。これは、多重化バスがアドレスフェーズとデータフェーズの間に方向転換するために必要なデッドタイムを回避します。システムメモリが追いつく限り、バスサイクルは 2 クロック(アドレスフェーズとデータフェーズ)しかありません。さらに、これによりアドレスパイプラインが可能になります:一つのバスサイクルが終了している間、次のサイクルのアドレスもすでに提示されています。実質的には、これはメモリシステムに即座にプロセッサを遅らせることなく応答するための追加サイクルを与えます。
実際には、その時代のシステム DRAM は通常この理想的な速度よりも遅かったです。典型的な DRAM レイテンシは約 80 ns から 130 ns で、すでに CPU サイクル 2 つ以上に対応します。したがって、2 クロックがベストケースのバスサイクルであり、それよりも遅いものはすべて待機状態として現れます(プロセッサが単に外部メモリスイステムを待機しています)。
別の重要な点は仲裁です。プリフェッチサイクルとデータサイクルは最終的に同じ外部バスを競合します。しかし、Intel の設計ではリアルなデータサイクルに優先順位を与え、プリフェッチが隙間を埋めるようにさせています。理想的なゼロウェイト状態の場合、これは争い(コンテンション)の多くを隠すことを意味します:データサイクルが先に行き、命令フェッチはそれらの間のスラックを使用します。それでも、基本的な制約は同じです:コードフェッチ、データアクセス、そしてページングはいずれも同一のメモリアルパスを共有しています。
ここでキャッシュが登場します。80386 はチップ上キャッシュ(オンチップキャッシュ)を持っていませんが、キャッシュを意識して設計された最初の x86 プロセッサです。Intel 82385 サブスタンプは、プロセッサ、SRAM キャッシュ、およびメインメモリの間に設置される専用キャッシュコントローラとして設計されています。キャッシュヒットの場合、CPU に待機状態のない 2 クロックバスサイクルを提供します。ミスの場合、それはアクセスをメインメモリに転送し、キャッシュラインを再充填します。典型的には 64KB から 128KB(ハイエンドシステムでは)の容量を持つこのキャッシュは非常に効果的です:386 にキャッシュが搭載されていると、キャシネがないものに比べて 30% から 40% スピード向上することが一般的です。
総まとめ
全体で見ると、メモリアルパイプラインはこのようになります:
- マイクロコードが
またはRD
を発令する。WR - 効果的アドレスハードウェアが作業を開始し(場合によっては早期開始で)。
- セグメンテーションが並列にリロケーションし制限チェックを行う。
- TLB が線形アドレスをヒット時に翻訳する。
- バスインターフェースがアクセスをスケジュールし、背景でプリフェッチが競合する。
- 結果はマイクロコードの
同期ポイントのためにタイムリーに返ってくる。DLY
80386 メモリアルパイプラインを通る RD メモリアドレス読み込みのサイクルごとの表示(早期開始ケースを示す)
FPGA 386 コアへのメモリアルパイプラインのマッピング
このシリーズでは主に歴史的な 386 に焦点を当ててきましたが、ここではそのメモリアルパイプラインモデルが私が設計している FPGA 用 386 コアにどのようにマッピングされるかについて議論を開始したいと思います。ao486 コアと同様に SDRAM を使用し、メモリアクセスのレイテンシを削減するためにキャッシュに依存しています。
歴史的な 80386 のメモリアルパイプラインを現代的な FPGA にマッピングする際に検討すべき点はいくつかあります。主に非同步(asynchronous)と同期(synchronous)論理およびメモリに関するものです。全体的な目標は、マイクロアーキテクチャを比較的忠実にマッピングしつつ、それでも高い
Fmax と低い CPI を達成することです。
- ラッチ対レジスタ。 80386(および 486)は主にラッチベースの設計です(詳細は Ken Shirriff の記事「Inside the Intel 386 processor die: the clock circuit for a die-level view of the 386 clocking scheme」参照)。ラッチはレベルトリガであり、有効信号が高い限り入力に従います。一方、現代のフリップフロップ(レジスタ)はエッジトリガであり、クロックエッジで入力のスナップショットを取得します。フリップフロップと比較して、ラッチはより少数のトランジスタを必要とし、「タイムボローニング」(少し遅いフェーズが近隣のスريعなフェーズから時間を借りる)を許可します。したがって、FPGA 設計では作業を均等に分けることがより重要です。私はどこにレジスタを挿入するかについてかなり実験を行いましたが、異なる決定は異なる
値をもたらしました。結果として、以前のセクションで示したパイプライン設計を採用することに落ち着き、それは十分に機能しています。Fmax - 2 つのクロックフェーズ。 80386 もまた、クロックサイクルあたり 2 つのクロックフェーズを持っています。これがアドレス翻訳レイテンシが 1.5 サイクルと引用される理由です。FPGA でこれをエミュレートする一つの方法は、クロック速度を倍にし、1 つの FPGA クロックサイクルをフェーズとして使用することです。私はそれをやらず、単にアドレス翻訳を 2 サイクルにするだけです。これは少しのレイテンシ増と CPI への影響を意味しますが、それが良い方法で検証できるものはありませんでした。
- キャッシュ設計。 FPGA でキャッシュを実装する一般的な課題の一つは、ブロック RAM が同期であり、値が 1 サイクル後に利用可能であることです。そのために、FPGA ベースのキャッシュは通常 2 サイクルを必要とし、1 つ目がタグ検索に、2 つ目がデータ取得に使われます。82385 スタイルのキャッシュを実装しゼロウェイト状態を実現するためには、アドレスパイプラインが基本的には必須です。なぜなら、それがキャッシュがデータを返すためにちょうど 2 サイクルを残すためです。私はまだアドレスパイプラインを実装していませんので、L2 キャッシュではここで待機状態が発生します。そのため L1 キャッシュを行うことにしました:16KB の命令キャッシュと 16KB のデータキャッシュは CPU 内に配置され、外部の 82385 キャッシュよりも速い 1 サイクルのヒットレイテンシを提供しますが、サイズは小さくなります。
結論
80386 メモリアルパイプラインは、マイクロコード、セグメンテーション、TLB、バスインターフェース、プリフェッチ、および外部キャッシュにわたるレイテンシ隠蔽技術の慎重な組み合わせです。結果として得られたプロセッサでは、保護された仮想メモリはアーキテクチャ図だけでは予想されるよりも物理メモりに非常に近いパフォーマンスを示し、遅延が生じるのは主に珍しいケースだけです。
ページリング自体だけでなく、それが PC オペレーションシステムの本格的な基盤となるようにした主な要素です。まだ扱うべき話題があります:命令プリフェッチとデコード、タスク切り替えと割り込みなど。コアがすでに DOS とゲームを動作させているので、次回からはそれらのトピック、そして実際の実装について話し始める予定です。
読んでいただきありがとうございます。更新情報については X (@nand2mario) でフォローするか、RSS を使用してください。
クレジット: この 80386 の分析は、reenigne、gloriouscow、smartest blob、および Ken Shirriff によるマイクロコードの非暗号化とシリコン逆エンジニアリング作業に引きながら構成されています。