gem5のサイクル精度モデルについて理解したいので、O3CPUのドキュメントを読んでみることにする。
- パイプラインステージ
- Execute-in-executeモデル
- テンプレートポリシ
- ISA独立性
- ThreadContextとの相互作用
O3CPUは、v2.0リリース用の新しい詳細モデルである。これは、Alpha 21264を大まかにベースとしたアウトオブオーダーCPUモデルである。このページでは、O3CPUモデル、パイプライン・ステージ、パイプライン・リソースの概要について説明する。O3CPUの各部分がどのように動作するかについての正確な詳細については、コードを参照してほしい。
パイプライン・ステージ
- フェッチ
- 各サイクルで命令をフェッチし、選択されたポリシーに基づいてどのスレッドからフェッチするかを選択する。この段階でDynInstが最初に作成される。分岐予測も行う。
- デコード
- サイクルごとに命令をデコードする。PC相対無条件分岐の早期解決も行う。
- リネーム
- 発行/実行/ライトバック
- 我々のシミュレータ・モデルは、
execute()
関数が命令上で呼び出されたときに、executeとwritebackの両方を処理するため、これら3つのステージを1つのステージにまとめた。このステージ(IEW)は、命令キューへの命令のディスパッチ、命令キューへの命令の発行指示、命令の実行とライトバックを処理する。
- 我々のシミュレータ・モデルは、
- コミット
- 各サイクルの命令をコミットし、命令が引き起こしたフォールトを処理する。また、分岐予測ミスの場合にフロントエンドをリダイレクトする処理も行う。
Execute-in-Executeモデル
O3CPUでは、高いタイミング精度を実現するために工夫を凝らした。そのために、パイプラインの実行段階で実際に命令を実行するモデルを使用している。ほとんどのシミュレータモデルは、パイプラインの先頭か末尾で命令を実行する。SimpleScalarと私たちの古い詳細CPUモデルは、どちらもパイプラインの先頭で命令を実行し、それをタイミングバックエンドに渡す。第一に、タイミングバックエンドにエラーが発生する可能性があり、それがプログラムの結果に現れない可能性がある。第二に、パイプラインの先頭で実行することにより、すべての命令が順番に実行され、アウトオブオーダーのロード相互作用が失われる。我々のモデルはこれらの欠点を回避し、正確なタイミングモデルを提供することができる。
テンプレート・ポリシー
O3CPUは、仮想関数を使うことなくポリモーフィズムのレベルを得るために、テンプレート・ポリシーを多用している。O3CPU内で使用されるほとんどすべてのクラスに「Impl」を渡すために、テンプレート・ポリシーを使用する。このImplは、特定のFetchクラス、Decodeクラス、特定のDynInst型、CPUクラスなど、パイプラインの重要なクラスをすべて定義している。このImplをテンプレート・パラメーターとして使用するクラスは、Impl内で定義されたクラスの完全な型情報を取得することができる。完全な型情報を取得することで、ポリモーフィズムを提供するために通常使用される従来の仮想関数や基底クラスは不要になる。主な欠点は、CPUがコンパイル時に完全に定義されていなければならないことと、テンプレート化されたクラスが手動でインスタンス化を必要とすることである。Implクラスの例はsrc/cpu/o3/impl.hh
とsrc/cpu/o3/cpu_policy.hh
を参照のこと。
ISA独立性
O3CPUは、ISAに依存するコードとISAに依存しないコードを分離するように設計されている。パイプライン・ステージとリソースは、下層のCPUコードと同様に、すべて主にISA非依存である。ISA依存のコードは、ISA固有の機能を実装している。例えば、AlphaO3CPUは、エラー割り込みからのハードウェア復帰(hwrei())や割り込みフラグの読み取りなど、Alpha固有の機能を実装している。下位CPUであるFullO3CPUは、すべてのパイプラインステージをオーケストレーションし、他のISAに依存しない動作を処理する。このように分離することで、将来的なISAの実装が容易になり、高レベルのクラスだけを再定義すればよくなることを期待している。
ThreadContextとの相互作用
ThreadContextは、外部オブジェクトがCPU内のスレッド状態にアクセスするためのインターフェースを提供する。しかし、これはO3CPUがアウトオブオーダーCPUであるという事実によって少し複雑になっている。任意のサイクルでアーキテクチャ状態がどうなっているかはよく定義されているが、そのアーキテクチャ状態が変更された場合にどうなるかはよく定義されていない。そのため、ThreadContextへの読み込みはそれほど労力をかけずに実行可能だが、ThreadContextへの書き込みやレジスタ状態の変更は、CPUがパイプライン全体をフラッシュする必要がある。これは、変更されたレジスタに依存するインフライト命令が存在する可能性があり、それらがレジスタの更新を見るべきか見るべきでないかが不明だからである。そのため、ThreadContextへのアクセスは、CPUシミュレーションの速度低下を引き起こす可能性がある。
バックエンドパイプライン
算術演算命令
算術演算命令は、LSQによる相互作用が発生するメモリアクセス命令よりも単純である。以下は、算術円座命令を実行した際に呼ばれる主要な関数とその関数毎の説明である。
Rename::tick()->Rename::RenameInsts() IEW::tick()->IEW::dispatchInsts() IEW::tick()->InstructionQueue::scheduleReadyInsts() IEW::tick()->IEW::executeInsts() IEW::tick()->IEW::writebackInsts() Commit::tick()->Commit::commitInsts()->Commit::commitHead()
- リネーム (
Rename::renameInsts()
)- 名前の通り、レジスタ命令リネームされ、命令がIEWステージに送られる。IQ/LSQが新しい命令を保持することができるかをチェックする
- ディスパッチ (
IEW::dispatchInsts()
)- この関数はリネームした命令をIQとLSQに挿入する
- スケジュール (
InstructionQueue::scheduleReadyInsts()
)- IQは発行準備完了の命令(オペランドがReady状態)の命令を管理し、使用可能なFUに発行するためのスケジュールを行う。FUのレイテンシはここで設定され、FUが完了すると、命令がFUに対して送られる。
- Execute (
IEW::executeInsts()
)execute()
関数が実行され、コミットに送られる。execute()
は結果を書き込みレジスタに書き込む。
- Writeback(
IEW::writebackInsts()
)- ここではInstructionQueue::wakeDependents()が実行される。依存している命令はReadyリストに追加され、スケジュールされる。
- Commit (
Commit::commitInsts()
)- 命令がROBの先頭に到達すると、命令はコミットされROBから消滅する。
ロード命令
ロード命令は、実行までは算術演算命令と同じパスを共有する。
IEW::tick()->IEW::executeInsts() ->LSQUnit::executeLoad() ->StaticInst::initiateAcc() ->LSQ::pushRequest ->LSQUnit::read() ->LSQRequest::buildPacket() ->LSQRequest::sendPacketToCache() ->LSQUnit::checkViolation() DCachePort::recivTimingResp()->LSQRequest::recivTimingResp() ->LSQUnit::completeDataAccess() ->LSQUnit::writeback() ->StaticInst::completeAcc() ->IEW::instToCommit() IEW::tick()->IEW::writebackInsts()
LSQUnit::executeLoad()
- 命令の
initiateAcc()
関数を呼び出してアクセスを開始する。実行コンテキストインターフェースを介して、initiateAcc()
はinitiateMemRead()
を呼び出し、最終的にLSQ::pushRequest()
に誘導される。
- 命令の
LSQ::pushRequest()
- すべての状態を追跡するために
LSQRequest
を割り当て、translationを開始する。変換が完了すると、仮想アドレスを記録し、LSQUnit::read()
を呼び出す。
- すべての状態を追跡するために
LSQUnit::read()
LSQUnit::writeback()
StaticInst::completeAcc()
を呼び出す。この命令は、コミット・キューにプッシュされる。その後、IEW::writebackInsts()
は、それを完了とし、その依存関係をウェイクアップする。ここからは、計算命令と同じパスを共有する。
ストア命令
IEW::tick()->IEW::executeInsts() ->LSQUnit::executeStore() ->StaticInst::initiateAcc() ->LSQ::pushRequest() ->LSQUnit::write() ->LSQUnit::checkViolation() Commit::tick()->Commit::commitInsts()->Commit::commitHead() IEW::tick()->LSQUnit::commitStores() IEW::tick()->LSQUnit::writebackStores() ->LSQRequest::buildPackets() ->LSQRequest::sendPacketToCache() ->LSQUnit::storePostSend() DcachePort::recvTimingResp()->LSQRequest::recvTimingResp() ->LSQUnit::completeDataAccess() ->LSQUnit::completeStore()
LSQUnit::read()
とは異なり、LSQUnit::write()
はストアデータをコピーするだけで、ストアがまだコミットされていないため、パケットをキャッシュに送信しない。- ストアがコミットされた後、
LSQUnit::commitStores()
は SQ エントリをcanWB
としてマークするので、LSQUnit::writebackStores()
はストア要求をキャッシュに送信する。 - 最後に、レスポンスが戻ってくると、
LSQUnit::completeStore()
は SQ エントリを解放する。
メモリ順序の推測ミス
InstructionQueueには、メモリ順序依存性を追跡するためのMemDepUnitがある。IQは、MemDepUnitが依存性があると判断した場合、命令をスケジューリングしない。
LSQUnit::read()
では、LSQは可能であればエイリアシングストアとフォワードを検索する。そうでない場合、ロードはブロックされ、MemDepUnit
に通知することで、ブロックされているストアが完了したときに再スケジュールされる。
LSQUnit::executeLoad/Store()
の両方は、LSQUnit::checkViolation()
を呼び出して、LQにミススペックの可能性がないか検索する。もし見つかれば、LSQUnit::memDepViolator
を設定し、IEW::executeInsts()
は後で開始され、ミススペキュレーションされた命令をつぶす。