キャッシュメモリの秘密、または10チームに1000クロックサイクルを費やす方法

Habréを含む、プロセッサキャッシュについては多くのことが書かれていますが、一般的な用語ではますます多くなっています。 キャッシュが実際にどのように機能するかの具体例をご紹介します。



例として、単一レベルのキャッシュを備え、MMU(ARM Cortex-Rのようなもの)のない32ビットハーバードRISCプロセッサに基づくチップ上のシンプルなシステムを取り上げます。 プロセッサは、プロセッサ周波数で動作する32ビットAMBA AHBバスを介して外部メモリコントローラに接続されます。







コマンドキャッシュとデータキャッシュは個別の直接マッピングで、サイズはそれぞれ16キロバイトです。 キャッシュラインの長さは32バイトです。つまり、各キャッシュには512ラインが含まれています。 オフセットによってキャッシュライン内のバイトのシリアル番号が決まり、インデックスによってキャッシュ内のキャッシュラインの番号が決まることを思い出してください。 キャッシュは直接マッピングであるため、タグが異なっていても、同じインデックスを持つ複数の行を同時に含めることはできません。







これがすべてどのように機能するかを理解する最も簡単な方法は、電源投入直後にプロセッサが何をするかを把握することです。 キャッシュがデフォルトでオン(無効)になっている(つまり空である)とすると、外部メモリに何らかの方法で2つの数値を追加するプログラムがあったとします。



         #割り込みベクトルのテーブルは、アドレス0x0にあります
         _reset_vector:
 0x0000:ジャンプ_start
         _interrupt1:
 0x0004:ジャンプ_handler1
         _interrupt2:
 0x0008:_handler2にジャンプ
 ...

         #メインプログラムコードはアドレス0x1234にあります
         _start:

         #メモリからレジスタR10に第1オペランドをロード
 0x1234:moveh R0、0x1000#R0に0x1000をロード[31:16]
 0x1238:R10、[R0]をロード#0x10000000からR10に値をロード

         #メモリからレジスタR11に第2オペランドをロード
 0x123C:moveh R1、0x1001#0x1001をR1にロード[31:16]
 0x1240:R11、[R1]をロード#0x10010000からR11に値をロード

         #オペランドを追加し、結果をメモリに書き込む
 0x1244:R2、R0、R1を追加
 0x1248:moveh R3、0x2000#0x2000をR3にロード[31:16]
 0x124C:store R2、[R3]#R2をR3からのアドレスに保存する

         #プロセッサを停止
 0x1250:停止 




電源をオンにした後、プロセッサは、割り込みベクタテーブルのアドレスに対応するアドレスからコマンドの実行を開始します。 このアドレスは通常、プロセッサに配線されています。 一般的な場合、これは何でもかまいませんが、簡単にするためにゼロに等しいと仮定します。

したがって、プログラムの最初のコマンド「jump _start」は0x0にあり、プロセッサはそのアドレスを事前に認識しています。



ステージ1。プロセッサは、アドレス0x0のコマンドを含むコマンドキャッシュ内の行を探します。 キャッシュが空なので、プロセッサ内のキャッシュコントローラはミスを通知し、プロセッサを即座に停止すると同時に、いわゆる「読み取りバースト」、つまり外部メモリからのデータパケットの読み取り要求を開始します。 データがワード単位でキャッシュにロードされることはありません。パケット単位でのみロードされます。それ以外の場合、キャッシュにはまったく意味がありません。 データパケットのサイズは、キャッシュラインの長さと同じです。 したがって、このシステムではAHBバスは1クロックあたり32データビットしか送信できないため、リクエストには8クロックサイクルかかります。 理想的には、リクエストがメモリコントローラーに到達するのに1クロックサイクルが必要です(バスが別のリクエストの処理でビジーである場合、より多くのクロックサイクルが必要です)。 外部メモリのタイプに応じて、コントローラは、リクエストが到着した直後(メモリがSSRAMの場合)、または数十クロックサイクル後(SDRAMの場合)に読み取りデータの送信を開始できます-この時間はメモリレイテンシと呼ばれます。 読み取りデータは、メモリコントローラーからプロセッサに戻るためにもう1クロック必要です。 以下は、4クロックサイクルのレイテンシで外部メモリからキャッシュラインを読み取るタイミング図です。







ステージ2。キャッシュコントローラーは、メモリからのワードをラインバッファー(「ラインバッファー」)に書き込みます。そのサイズはキャッシュラインの長さに等しくなります。 ラインバッファがいっぱいになると、その内容はコマンドキャッシュのゼロラインに格納されます(アドレス0x0のインデックスは0であるため)。 ただし、プロセッサはバッファからコマンドを直接実行できるため、ミスの原因となったコマンドがバッファに書き込まれるとすぐに、キャッシュコントローラがプロセッサを直ちにロック解除します。 プロセッサはパイプライン化されているため、最初の命令がデコードされる前にメモリから2番目の命令を選択する必要があります。 これを実現するために、2番目のコマンドのアドレスが予測されます。その結果、プロセッサはアドレス0x4からコマンドを選択し続ける可能性があります。 この時点で2番目のコマンド(「jump _handler1」)もバッファーにあるため、ミスはなく、プロセッサーは3番目のコマンド(「jump _handler2」)の選択を開始します。 これは、予測に誤りがあり、2番目のコマンドを0x1234で選択する必要があることをプロセッサが理解するまで続きます。



ステージ3.プロセッサがクリアになると(たとえば、7ステージの命令パイプラインを備えたプロセッサでは約7クロックサイクルかかります)、すぐに別のキャッシュミスが発生します。今回は、0x1234のコマンドを含むキャッシュ内の行の不足が原因です(「移動R0、0x10000000))、新しい「読み取りバースト」を送信し、戻り時にキャッシュの145行目に格納されます(アドレス0x1220のインデックスは0x91 = 145であるため)。 実装に応じて、プロセッサは、行の先頭(つまり、最初に0x1220、次に0x1224、0x1228などの0x123Cまでのワード)から、またはミスを引き起こしたワード(いわゆるモード)からキャッシュへのデータのロードを開始できます「クリティカルワードが最初」-最初にワードがアドレス0x1234で読み取られ、次に0x1238、0x123C、0x1220、0x1224など)。いずれの場合も、文字列はいずれの場合でも完全にロードされますが、最初の場合、プロセッサは5クロックサイクル余分に待機します。 多くのマイクロコントローラはまだ「クリティカルワードファースト」をサポートしていないため、幸運なことにプログラマはパフォーマンスの大幅な低下を達成できます。







ステージ4。 「クリティカルワードファースト」がサポートされているかどうかに関係なく、3つの正しいコマンドがすぐにキャッシュにロードされます(「R1、0x10010000を含む」まで)。 キューが「load R10、[R0]」コマンドに達すると、プロセッサはデータキャッシュミスを検出し(結局、データキャッシュは空です-コマンドキャッシュで以前の操作がすべて発生しました)、ゼロラインに書き込まれる別の読み取り読み取りを送信します(アドレス0x10000000のインデックスは0であるため)。 プロセッサが十分に賢い(そして、学生のクラフトよりもやや複雑なほとんどのプロセッサがこれに対して十分に賢い)場合、「load R10、[R0]」に続くコマンドは独立しており、同時に実行できることを理解します。データをキャッシュにコピーします。 その後、プロセッサは0x1240のコマンドを選択しますが、データキャッシュからの以前の要求でバスがまだ占有されているため、ハングする別の「読み取りバースト」を取得します。 もちろん、遅かれ早かれコマンド「load R11、[R1]」は命令キャッシュで終了し、プロセッサはすぐにデータキャッシュの新しいミスを検出します。さらに、キャッシュからタグ0x04000の以前にロードされた行を消去します(アドレス0x10000000と0x10010000のみが異なるためタグ付き-それぞれ0x04000および0x04800で、両方のインデックスはゼロです)。



ステップ5。最後に、プロセッサは「store R2、[R3]」コマンドを実行します。これにより、データキャッシュがさらに失われ、その結果、「読み取りバースト」が発生します。さらに、キャッシュからタグ0x04800の行を消去し、タグ0x08000の行。 さらに、このコマンドはキャッシュにのみ書き込まれるため、外部メモリには何も書き込まないことに注意してください! プログラマーが更新されたキャッシュラインを明示的にメモリに「マージ」する(つまり、キャッシュラインをフラッシュする)まで、またはラインが別のラインによってキャッシュから排出されるまで、「書き込みバースト」はありません。 たとえば、「halt」コマンドの代わりに「load r4、[0x8000]」コマンドがある場合、読み取りプロセスは次のようになります。







最後に言いたいのは、キャッシュの誤った使用に関連するエラーを数か月間検索できることです。 ノイマン同志の教訓に反して、揮発性とメモリをキャッシュするコンパイラーは、それを目撃しています。 キャッシュの「透明性」は、バキュームPHPを使用する球状のプログラマーにのみ適している神話です。 したがって、プロセッサにキャッシュがある場合は、それを無効にしてください!



All Articles