gem5のサイクル精度モデルについて理解したいので、O3CPUのドキュメントを読んでみることにする。
次に、メモリサブシステムについてドキュメントを読み進めていく。
M5の新しいメモリーシステム(最初の2.0ベータリリースで導入)は、以下の目標を持って設計された:
- タイミング・モードにおけるタイミング・アクセスとファンクショナル・アクセスを統一する。旧メモリシステムでは、タイミング・アクセスはデータを持たず、操作にかかる時間だけを示していた。その後、別の機能アクセスによって、実際に操作がシステムから見えるようになった。この方法は混乱を招き、シミュレートされたコンポーネントが誤って不正を行うことを許し、メモリシステムがタイミングに依存した値を返すことを妨げていた。
- メモリシステムのコードを単純化する - 膨大な量のテンプレートと重複するコードを削除する。
- 特に、共有バス以外のメモリー相互接続を可能にする。
2.0b4で(キャッシュモデルの大幅な書き換えとともに)導入された新しいコヒーレンス・プロトコルの詳細については、コヒーレンス・プロトコルを参照のこと。
MemObject
メモリ・システムに接続するすべてのオブジェクトは MemObject
を継承する。このクラスには、与えられた名前とインデックスに対応するポートを返す純粋仮想関数getMasterPort(const std::string &name, PortID idx)
とgetSlavePort(const std::string &name, PortID idx)
が追加されている。このインターフェースは、MemObject同士を構造的に接続するために使用される。
ポート
メモリ・システムの次の大きな部分は、ポートという考え方である。ポートは、メモリー・オブジェクト同士のインターフェースに使われる。MasterPortとSlavePortのペアで、もう一方のポート・オブジェクトをピアと呼ぶ。これらは、設計をよりモジュール化するために使用される。ポートを使えば、あらゆるタイプのオブジェクト間の特定のインターフェースを作る必要はない。すべてのメモリ・オブジェクトは、少なくとも1つのポートを持たなければ役に立たない。CPUなどのマスター・モジュールは、1つ以上のMasterPortインスタンスを持っている。メモリコントローラなどのスレーブモジュールは、1つ以上のSlavePortsを持つ。キャッシュ、ブリッジ、バスなどのインターコネクトコンポーネントは、MasterPortとSlavePortの両方のインスタンスを持つ。
ポート・オブジェクトには2つの関数グループがある。send*
関数は、そのポートを所有するオブジェクトからポート上で呼び出される。例えば、メモリシステムでパケットを送信する場合、CPUはmyPort->sendTimingReq(pkt)
を呼び出してパケットを送信する。各送信関数には対応するrecv
関数があり、ポートのピアで呼び出される。つまり、上記のsendTimingReq()
呼び出しの実装は、単にスレーブ・ポートのpeer->recvTimingReq(pkt)
となる。この方法を使うと、仮想関数呼び出しのペナルティは1つだけだが、どんなメモリー・システム・オブジェクトでも接続できる汎用ポートを維持できる。
マスター・ポートはリクエストを送信してレスポンスを受信し、スレーブ・ポートはリクエストを受信してレスポンスを送信する。コヒーレンス・プロトコルにより、スレーブ・ポートはスヌープ・リクエストを送信し、スヌープ・レスポンスを受信することもできる。
コネクション
Pythonでは、ポートはシミュレーション・オブジェクトのファーストクラス属性で、Paramsとよく似ている。2つのオブジェクトは、代入演算子を使ってポートの接続を指定することができる。通常の変数やパラメータの代入とは異なり、ポートの接続は対称的である: A.port1 = B.port2
は、B.port2 = A.port1
と同じ意味になる。マスター・ポートとスレーブ・ポートという概念はPythonのオブジェクトにも存在し、ポート同士が接続されたときにチェックが行われる。
ポートの数が無制限になる可能性があるバスのようなオブジェクトは、「ベクター・ポート」を使用する。ベクターポートへの割り当ては、以前の接続を上書きするのではなく、接続のリストに相手を追加する。
C++では、すべてのオブジェクトがインスタンス化された後、pythonコードによってメモリポートが接続される。
リクエスト
リクエストオブジェクトは、CPUやI/Oデバイスから発行されたオリジナルのリクエストをカプセル化する。このリクエストのパラメータはトランザクショ ン中永続的であるため、リクエストオブジェクトのフィールドは、与えられたリクエストに 対して最大一度だけ書き込まれることを意図している。オブジェクトのフィールドのサブセットを異なる時に書き込む(あるいはまったく 書き込まない)ことを可能にする、一握りのコンストラクタと更新メソッドがある。すべてのリクエストフィールドへの読み出しアクセスは、読み出される フィールドのデータが有効であることを検証するアクセサメソッドを 通じて提供される。
リクエストオブジェクトのフィールドは、通常、実システムの デバイスでは利用できないので、通常、統計やデバッグのためにのみ使用すべきであ り、アーキテクチャ値として使用すべきではない。
リクエストオブジェクトのフィールドは以下の通りである:
- 仮想アドレス。リクエストが物理アドレス上で直接発行された場合、このフィー ルドは無効かもしれない(例えば、DMA I/Oデバイスによって)
- 物理アドレス
- データサイズ
- リクエストが作成された時間
- このリクエストを発生させたCPU/スレッドのID。リクエストがCPUによって発行されたものでない場合(デバイスアクセスやキャッシュライトバックなど)、無効である可能性がある
- このリクエストを発生させたPC。また、リクエストがCPUによって発行されたものでない場合は無効かもしれない
パケット
Packetは、メモリシステム内の2つのオブジェクト(例えば、L1キャッシュとL2キャッ シュ)間の転送をカプセル化するために使われる。これはRequestとは対照的で、1つのRequestが要求元から最終的な宛先ま でを往復し、場合によっては途中で複数の異なるPacketによって伝達される。
多くのパケットフィールドへの読み出しアクセスは、読み出 されるフィールドのデータが有効であることを検証するアクセサメソッド を介して提供される。
パケットには以下のものが含まれ、これらはすべて、データが有効であることを確認するためにアクセッサによってアクセスされる:
- アドレス。これは、パケットをターゲットにルーティングし(宛先が明示的に設定されていない場合)、ターゲットでパケットを処理するために使用されるアドレスである。これは通常、リクエストオブジェクトの物理アドレスから導かれるが、 状況によっては(例えば、アドレス変換が実行される前に完全仮想キャッシュに アクセスする場合など)仮想アドレスから導かれることもある。例えば、キャッシュミスの場合、パケットアドレスはリクエ ストアドレスではなく、フェッチするブロックのアドレスになるかもしれない。
- サイズ。この場合も、キャッシュミスのシナリオのように、このサイズは元のリク エストのサイズと同じではないかもしれない。
- 操作されるデータへのポインタ。
dataStatic()
,dataDynamic()
,dataDynamicArray()
で設定され、パケットに関連付けられたデータを、パケットが解放されるとき、されないとき、delete
のとき、delete[]
のときに、それぞれ制御する。- 上記のメソッド
allocate()
で設定されていない場合はallocatedとなり、パケットが破棄されたときにデータが解放される。(常に安全に呼び出せる)。 - ポインタは
getPtr()
を呼び出すことで取得できる。 get()
とset()
は、パケット内のデータを操作するために使用できる。get()メソッドはゲストからホストへのエンディアン変換を行い、 setメソッドはホストからゲストへのエンディアン変換を行う。
- Success, BadAddress, Not Acknowleged, Unknownを示すステータス。
- パケットに関連付けられたコマンド属性のリスト。
- 注意: ステータスフィールドとコマンド属性のデータには重複がある。これは主に、ナックされたときにパケットを簡単に再初期化したり、アトミックアクセスやファンクショナルアクセスで簡単に再利用したりするためである。
SenderState
ポインターは、パケットに関連するが送信デバイス(例えばMSHR)固有のステー トを保持するために使用される、仮想ベース不透明構造体である。このステートへのポインタはパケットのレスポンスで返され、送信側はそれを処理するために必要なステートをすぐに調べることができる。特定の送信デバイスに固有の状態を保持するために、このサブクラスが派生する。CoherenceState
ポインターは、コヒーレンス関連のステートを保持するために使用される仮想ベース不透明構造体である。特定のコヒーレンス・プロトコルに固有の状態を保持するために、このポインタから特定のサブクラスが派生する。- リクエストへのポインタ。
アクセス・タイプ
ポートがサポートするアクセスは3種類ある。
- タイミング
- アトミック
- ファンクショナルアクセス
- ファンクショナルアクセスはアトミックアクセスと同様に瞬時に発生するが、アトミックアクセスとは異なり、メモリシステム内でアトミックアクセスやタイミングアクセスと共存することができる。ファンクショナル・アクセスは、バイナリのロード、シミュレートされたシステム内の変数の検査/変更、シミュレーターへのリモート・デバッガーの接続などに使用される。重要な注意点は、機能アクセスがデバイスによって受信されたとき、そのデバイスにパケットのキューが含まれていれば、すべてのパケットを検索して、機能アクセスが影響を及ぼしているリクエストやレスポンスを探し、適切に更新しなければならないことである。
Packet::intersect()
メソッドとfixPacket()
メソッドがこれを助けることができる。
- ファンクショナルアクセスはアトミックアクセスと同様に瞬時に発生するが、アトミックアクセスとは異なり、メモリシステム内でアトミックアクセスやタイミングアクセスと共存することができる。ファンクショナル・アクセスは、バイナリのロード、シミュレートされたシステム内の変数の検査/変更、シミュレーターへのリモート・デバッガーの接続などに使用される。重要な注意点は、機能アクセスがデバイスによって受信されたとき、そのデバイスにパケットのキューが含まれていれば、すべてのパケットを検索して、機能アクセスが影響を及ぼしているリクエストやレスポンスを探し、適切に更新しなければならないことである。
パケット割り当てプロトコル
Packetオブジェクトの割り当てと割り当て解除のプロトコルは、アクセスタイプによって異なる。(ここでは、低レベルのC++のnew
/delete
の問題を話しているのであって、コヒーレンス・プロトコルとは関係ない)
- アトミックおよびファンクショナル:Packetオブジェクトは要求元が所有する。responderはリクエストパケットをレスポンスで上書きしなければならない(通常は
Packet::makeResponse()
メソッドを使用する)。一つのリクエストに対して複数のレスポンダを持つことはできない。レスポンスは常にsendAtomic()
またはsendFunctional()
が戻る前に生成されるので、requesterはPacketオブジェクトを静的またはスタック上に割り当てることができる。 - タイミング: タイミングトランザクションは、リクエストとレスポンスの2つの一方向メッセージで構成される。どちらの場合も、Packetオブジェクトは送信者によって動的に割り当てられなければならない。割り当て解除は受信側(ブロードキャスト・コヒーレンス・パケットの場合はターゲット・デバイス、通常はメモリ)の責任である。リクエストのレシーバーがレスポンスを生成する場合、リクエスト・パケットをそのレスポンスに再利用することで、
delete
とnew
を呼び出すオーバーヘッドを節約することができる(そしてmakeResponse()
を使用する利便性を得ることができる)。しかしながら、この最適化はオプションであり、 リクエスターはリクエストに対する応答で同じPacketオブジェクトを受け取る ことに頼ってはならない。(キャッシュ間転送(cache-to-cache transfer)のように)レスポンダがターゲッ トデバイスでないとき、ターゲットデバイスは依然としてリクエストパケットを 削除するので、応答するキャッシュはその応答のために新しいPacketオブジェクトを 割り当てなければならないことに注意すること。また、ターゲットデバイスはリクエストパケットを配送時に即座に削除するかもしれないので、パケットが配送された時点を過ぎてブロードキャストパケットを参照したい他のメモリデバイスは、配送されたパケットへのポインタが有効であり続けることを信頼できないので、そのパケットのコピーを作成しなければならない。
タイミングフロー制御
タイミングリクエストは実際のメモリシステムをシミュレートしているので、ファンクショナルアクセスやアトミックアクセスとは異なり、その応答は瞬時ではない。タイミングリクエストは瞬時ではないので、フロー制御が必要である。タイミング・パケットがsendTiming()
で送信されると、そのパケットは受け入れられるかどうか、trueまたはfalseを返すことで通知される。falseが返された場合、オブジェクトはrecvRetry()
コールを受信するまで、それ以上パケットの送信を試みてはならない。この時点で、オブジェクトは再びsendTiming()
を呼ぼうとするはずである。注:元のパケットを再送する必要はない。代わりに、より優先順位の高いパケットを送ることができる。sendTiming()
がtrueを返した後でも、パケットは宛先に到達できないかもしれない。応答を必要とする(すなわち、pkt->needsResponse()
がtrueである)パケットについては、どのメモリオブジェクトでも、その結果をNackedに変更して送信元に送り返すことで、パケットの承認を拒否することができる。しかし、それが応答パケットである場合、これはできない。true/falseリターンはローカルなフロー制御に使われることを意図しており、一方nackingはグローバルなフロー制御に使われることを意図している。どちらの場合も、応答をナックすることはできない。
レスポンスとスヌープの範囲
メモリシステム内の範囲は、アドレス範囲に敏感なデバイスに、スレーブポートオブジェクト内のgetAddrRanges
の実装を提供させることによって処理される。このメソッドは、応答するアドレスのAddrRangeList
を返す。これらの範囲が変更されたとき(例えば、PCIコンフィギュレーションが行われたとき)、デバイスはそのスレーブ・ポートでsendRangeChange()
をコールし、新しい範囲が階層全体に伝搬されるようにする必要がある。すべてのメモリ・オブジェクトがsendRangeChange()
を呼び出し、システム内のすべてのバスにすべての人の範囲が伝搬されるまで、慌ただしく範囲が更新される。