キャッシュを実現するためのネットワーク全体での最も詳細な連携シミュレーション

序文: この記事には多くの内容 (1w ワード) があり、理論的な知識だけでなく、包括的な実践も含まれています。この記事では、最初の 3 章の理論的な内容について大まかに説明します。ステーション b に移動して、ハルビン工業大学と王道燕のオペレーティング システムの仮想ストレージに関連する章を視聴することをお勧めします。設計と実装が簡単です。

ブロガーは主に CacheSim シミュレータを実装するためにこの記事を書きました。その目的は、CPU シミュレータを強化し、CPU のアウトオブオーダー実行メカニズムとロールバック メカニズムによって引き起こされるメルトダウンの脆弱性を研究することです。

I. 概要

1.1 はじめに

コンピュータ システムにおいて、CPU キャッシュ (英語: CPU Cache、この記事ではキャッシュと呼びます) は、プロセッサがメモリにアクセスするのに必要な平均時間を短縮するために使用されるコンポーネントです。ピラミッド ストレージ システムでは、CPU レジスタに次ぐ、上から下への 2 番目のレベルに位置します。その容量はメモリよりもはるかに小さいですが、その速度はプロセッサの周波数に近づく可能性があります。プロセッサがメモリ アクセス要求を発行すると、まず要求されたデータがキャッシュ内に存在するかどうかを確認します。存在する場合 (ヒット)、データはメモリにアクセスせずに直接返されますが、存在しない場合 (失敗)、メモリ内の対応するデータをまずキャッシュにロードしてからプロセッサに返す必要があります。キャッシュが有効な理由は主に、プログラム実行時のメモリアクセスの局所性(Locality)特性によるものです。この局所性には、空間的局所性と時間的局所性の両方が含まれる。この局所性を効果的に利用すると、キャッシュは非常に高いヒット率を達成できます。プロセッサの観点から見ると、キャッシュは透過的なコンポーネントです。したがって、プログラマは通常、キャッシュの操作に直接介入することはできません。ただし、キャッシュをより有効に活用するために、キャッシュの特性に基づいてプログラム コードに特定の最適化を行うことができることは事実です。

1.2 キャッシュとメインメモリの関係

ここに画像の説明を挿入

ブロックには 16 バイトが含まれており、メモリの構成単位はバイト、ブロック内のアドレスは 4 ビット、残りの部分はストレージ ブロックのシリアル番号です。

1. キャッシュ アドレスを計算する必要がないのはなぜですか?

メインメモリブロックとキャッシュブロックのサイズが同じであるため、ブロック内のアドレスビット数は全く同じであり、あるブロックがメモリとキャッシュ間で転送される場合、全体として行われます。ブロック内に変化はなく、2つのブロックのアドレス部分、値は全く同じです。

2. キャッシュ上のマークは何ですか?
メインメモリブロックとキャッシュブロックの対応関係がマークされています。メインメモリ上のブロックをキャッシュに転送する場合、このタグにメインメモリのブロック番号を書き込むことができ、CPUから与えられたメモリアクセスアドレスを受け取ると、キャッシュ上にあるかどうかを判断し、そのアドレスを取得することができます。ブロック番号とタグを比較するだけで、それらが等しく、キャッシュブロックが有効であれば、キャッシュから直接情報を取得できます。

3. ヒット率とブロック長の関係
ブロックが小さすぎると局所性原理が十分に活用されず、ブロックが大きすぎるとブロック数が少なくなります。ブロック内の情報の一部は
CPU にとって有用ですが、他の情報は使用されず、ヒット率にも影響します。

1.3 キャッシュの基本構造

ここに画像の説明を挿入
1. アドレス マッピング (ストレージ: ロード):
マップ、メイン メモリ内のブロック、キャッシュに配置される場合、キャッシュのどのブロックに配置できるか、これがマッピング ルールです。
2. 構造の変換 (take: search):
メインメモリのブロック番号をキャッシュの対応するブロック番号に変換するか、メインメモリのアドレスをキャッシュのアドレスに変換し、対応するメインメモリブロックを検索します。アクセス用のキャッシュ。
3. 置換アルゴリズム

1.4 キャッシュ読み取り操作

ここに画像の説明を挿入

1.5 キャッシュ書き込み操作

キャッシュとメインメモリ間の一貫性の問題:

  • ダイレクトライト方式:書き込み動作
    中にメインメモリとキャッシュの両方にデータが書き込まれている
    時間がメインメモリにアクセスする時間と
    なり、キャッシュブロックが存在する場合はメインメモリへの書き込み動作は不要更新戦略は実装が比較的簡単ですが、
    繰り返しのメモリ アクセスが必要な累積および合計プログラムの場合は、
  • ライトバック方式 書き込み
    時はキャッシュのみに書き込み、メインメモリには書き込まない
    キャッシュデータを置き換えるとメインメモリに書き戻されるが
    、整合性は保てない(並列計算機システム)

1.6 キャッシュの改善

(1) 段数の増加、
オンチップキャッシュ、
オフチップキャッシュ
(2) 統合キャッシュと分離
キャッシュ、命令キャッシュ
、データキャッシュ
(命令実行の制御方式:パイプラインか否かに関係)

2. キャッシュ - メインメモリのアドレスマッピング

2.1、ダイレクトマッピング

ここに画像の説明を挿入
定義: メイン メモリ内の指定されたブロックは、指定されたキャッシュ ブロックにのみマップまたはロードでき、領域の最初のブロックはキャッシュ バンクのブロック 0 にのみ配置できます。
ここに画像の説明を挿入

CPU がアドレスを与える場合、それを 3 つの部分に分割します: エリア コー​​ド | ブロック番号 | ブロック オフセット アドレス
エリア コー​​ド: メイン メモリのワード ブロック マーク (「マーク」と同じ t 桁)
ブロック番号: キャッシュ ワード ブロック アドレス
オフセット シフト アドレス:ブロック内のアドレス
ここに画像の説明を挿入

キャッシュのブロック 0 (0 番目のブロック) はメイン メモリの任意の領域の最初のブロックをロードする可能性があるため、0 番目のブロックはどの領域にあるでしょうか? キャッシュストレージ本体の「タグ」に市外局番を記述する必要があります。つまり、メインメモリのエリアコードを記録する「マーク」です。
キャッシュワードブロックのアドレスにより、キャッシュからブロック番号が分かります。このブロックが指定領域内の指定ブロックであるかどうかは、を見つけるには、住所で指定された市外局番とキャッシュ (マーク) 内の現在の市外局番を照合して比較する必要があります。

短所: メインメモリ 2 の c 乗の内容を再度使用する必要がある場合、直接マッピングの規則により、内容はキャッシュの 0 番目のブロックとそれに続くブロックで置き換えられる必要があります。特定の領域の最初のブロックは、キャッシュの 0 番目のブロックにのみ配置できます。

各キャッシュ ブロック i は複数のメイン メモリ ブロックに対応できます
が、各メイン メモリ ブロック j は 1 つのキャッシュ ブロックにのみ対応します。

2.2、完全連想マッピング

ここに画像の説明を挿入
メイン メモリ内の任意のブロックをキャッシュ内の任意のブロックに配置できます。キャッシュに空きブロックがある限り、メイン メモリ ブロックを転送できるため、キャッシュの使用率が向上します。ただし、メイン メモリ内のブロックがキャッシュに転送される場合、そのブロックはキャッシュのどのブロックにある可能性があるため、メイン メモリ アドレスのメイン メモリ ブロック番号タグとキャッシュ内のすべてのブロックのタグを比較する必要があります。 (直接マッピングは直接マッピングです。キャッシュからブロックを見つけて、必要な比較は 1 回だけです (つまり、メイン メモリのブロック番号マークとキャッシュ内のブロックのマークを比較する))。コンパレータも長くなります (t+c)。

ここに画像の説明を挿入

2.3、グループ連想マッピング

妥協案: ランダムな配置でも指定された配置でもなく、指定された場所にランダムに配置します。
ここに画像の説明を挿入

1. まずキャッシュをブロックに分割し、次にこれらのブロックをいくつかのグループに分割します。
各セットには 2 個のピース​​が含まれています。

2. 次に、メインメモリをブロックに分割し、これらのブロックを領域に分割します。各領域のサイズは、キャッシュ内のグループの数と同じです (各領域のブロック数 = グループの数)
メイン メモリの 0 番目のブロックは、キャッシュの 0 番目のグループのどこにでも配置でき (完全関連付けの特性)、エリア コー​​ドによって対応するマークが指定されます (ダイレクト マッピングの特性)。

これにより、対応するグループ内に位置がある限り、同じ領域内のブロックを転送できます (フルアソシアティブ機能)。

キャッシュにすでに存在するかどうかを確認する必要がある場合は、エリア番号とエリア内のブロック番号を指定するだけでよく、ブロック ラベル (j) を指定した後、指定されたグループ (マップ Q) を見つけることができます。 i) グループ内の複数のブロックのマークをエリア コー​​ドと比較することができ、各キャッシュ ブロックと比較する必要はありません (ダイレクト マッピング機能)。

ここに画像の説明を挿入
ここに画像の説明を挿入

3. 置換アルゴリズム

ここに画像の説明を挿入

4. デザイン

4.1 設計プロセス

  • 1コマンド読み取り

メインメモリアクセストレースはファイル形式で提供されるため、トレースファイルのダウンロードではファイルからメモリアクセストレースを読み取る必要があります。トレースの形式は次のとおりです。

s 0x1fffff50 1

各行の最初の文字は命令のタイプで、s は書き込み (ストア)、r は読み取り (読み取り) を表します。中央の 16 進数はメモリ アドレスで、最後の整数はメモリ アクセス命令間の間隔命令の数を示します。たとえば、5 番目の命令と 10 番目の命令がメモリ アクセス命令で、間に他のメモリ アクセス命令がない場合、インターバル命令の数は 4 になります。私たちが作成したシミュレーターでは、最後のパラメーターの使用は考慮されていません。

  • 2 命令を解析する

トレースファイルから命令を読み込んだ後、命令を解析し、アドレスを変換し、そのアドレスのデータがキャッシュにあるかどうかを確認し、ヒットしたかどうかの結果を取得します。

  • 3 ヒットを確認する

アドレスを操作してヒットを確認する

  • 4 空き回線を探す

読み取りヒットの場合は、タイムスタンプを更新するだけで済みます。書き込み操作の場合は、ダーティ ビットをマークします。以前にヒットしたことがない場合は、現在無効な状態にある行を優先して、現在のキャッシュから使用できるブロックを見つける必要があります。そうでない場合は、置換アルゴリズムを実行して、置換する必要があるブロックを見つけます。同時に、置き換えられるブロック内の元のデータがダーティである場合は、メモリに書き戻されます。

  • 5 データをキャッシュラインにロードします

ミスした場合は、1.4 を通じてデータを配置できるキャッシュ ラインを見つけます。次のステップでは、メイン メモリからキャッシュ ラインにデータをロードします。

4.2 文書作成

4.2.1 Cache_line クラス

Cache_Line は別のクラスであり、タグは符号なし 32 ビットを使用します

class Cache_Line {
    
    
public:
    _u32 tag;
    /**计数,FIFO里记录最一开始的访问时间,LRU里记录上一次访问的时间*/
    union {
    
    
        _u32 count;
        _u32 lru_count;
        _u32 fifo_count;
    };
    _u8 flag;
    _u8 *buf;
};

カウントはアクセス時間を記録するために使用され、ユニオンデータ構造を採用しています。後でカウントを更新する場合、異なる置換アルゴリズムに特別な代入を実行するだけでよく、すべてのカウントを使用するときに思考の混乱を招くことはありません。時間。この値の変更については、置換アルゴリズムで後述します。buf の本来の目的は、メモリにデータを保存することです。前の章の構造の説明のバンクを参照してください。Flag は、いくつかのフラグ ビットのストレージです。

桁数 7~3 2 1 0
効果 予約 ロッキングビット ダーティビット 有効ビット

4.2.2 変数

変数 意義
キャッシュサイズ 合計キャッシュ サイズ、単位バイト
キャッシュラインサイズ キャッシュラインのサイズ、単位バイト
キャッシュライン番号 キャッシュには合計何行ありますか?
キャッシュマッピングウェイ グループ連想
キャッシュセットサイズ キャッシュ全体に含まれるグループ (セット) の数
キャッシュセットシフト メモリアドレス部のビット数 log2(cache_set_size)
キャッシュラインシフト メモリアドレス部のビット数 log2(cache_line_num)
キャッシュ 実キャッシュ配列
ティックカウント 命令カウンタ
キャッシュバッファ キャッシュバンク、メモリデータが保存される場所
キャッシュフリー番号 現在の空きキャッシュラインの数
キャッシュ_r_カウント、キャッシュ_w_カウント キャッシュによって読み書きされたデータの合計量
キャッシュヒット数、キャッシュミス数 当たり外れのカウント
スワップスタイル 置換アルゴリズム

4.2.3 機能

関数 関数
チェックキャッシュヒット ヒットを確認する
get_cache_free_line 現在の空きキャッシュ ラインを取得し、空きキャッシュ ラインがない場合は置換アルゴリズムを実行し、置換可能なブロックを返します。
set_cache_line get_cache_free_line で取得したキャッシュラインにメモリ上のデータをロードします。
do_cache_op 命令を分析する
ロードトレース トレースファイルをロードして分析を開始します
set_swap_style 今回のキャッシュ置換アルゴリズムを設定します

4.2.4 定数

const unsigned char CACHE_FLAG_VAILD = 0x01;	//有效位
const unsigned char CACHE_FLAG_DIRTY = 0x02; //脏位
const unsigned char CACHE_FLAG_LOCK = 0x04; //Lock位
const unsigned char CACHE_FLAG_MASK = 0xff;	//初始化

この記事の最初の表に対応して、ロック ビットは後のキャッシュ ロックのために予約されています。後で、この機能を実現するためにプロジェクトが変更されます。CACHE_FLAG_MASK は主に、キャッシュ ラインへの書き込み時にフラグを初期化するために使用されます。他のものは、キャッシュ ライン内のフラグを指定した & 操作を使用して、キャッシュ ラインが有効かダーティかを判断します。

五、詳細な実施

5.1 プログラムの入力

main.cpp は主にテスト ファイル用で、さまざまなキャッシュ ライン サイズ、さまざまなグループ関連付け方法、およびさまざまな置換戦略で構成されています。デフォルトではライトバック方式が使用されます。デフォルトのキャッシュ サイズは 32KB (0x8000 バイト) です。各サイクルでは、最初にキャッシュ構成を初期化し、次に置換戦略を設定し、最後にトレース ファイルを読み取り、メモリの読み取りおよび書き込みプロセスのシミュレーションを開始します。

5.2 初期化

CacheSim クラスのコンストラクターでは、主に前章のいくつかの変数の意味に従って初期化されますが、主なことはシフトの 2 つの変数に注目し、ライブラリ関数 log2() を使用して数値を計算することです。数字の。キャッシュ ラインは実際にはキャッシュ変数に格納され、memset には 0 が設定されるため、キャッシュ ラインのフラグもデフォルトで 0 が設定されます。デストラクター内のキャッシュとcache_bufの解放に注意してください。

5.3 load_trace トレースファイルロード関数

関数を通じてfgetsファイルを buf 文字配列に読み取り、sscanfフォーマットして buf から命令行を読み取ります。元のファイルのフォーマットがフォーマットであることに注意してください。s 0x1fffff50 1最後のパラメータは考慮しないため、命令タイプのみが考慮されます。 (ストアまたは読み取り) は読み取り、メモリアドレスです。を判断して読み書きするかどうかを決定しstyle、読み込まれる (実行される) たびに、こちらの値tick_countは ++ になり、読み取られたか書き込まれたかを記録するためのカウンタもそれに応じて ++ になります。rcount/wcount読み取りおよび書き込み命令の数をカウントするものですcache_r_count/cache_w_countがメモリとキャッシュによって実行されるデータの読み取りおよび書き込みの数をカウントするものです。キャッシュミスにより代替ラインが見つかった場合、見つかったラインにダーティデータが含まれていた場合はメモリに書き戻す必要があり、その際にデータ通信が発生し、メモリからメモリにデータをロードする際にもデータ通信が行われます。交換ライン。トレース ファイルが処理された後、結果が出力されます。現在、次の 3 つの主な重点分野があります。

  • 命令数
  • ミス率/ヒット率
  • 読み書きデータ通信

5.4 check_cache_hit キャッシュがヒットしたかどうかを確認します

do_cache_op命令を分析する

受信パラメータは、アドレスと読み取りまたは書き込み用のフラグ (s または r) です。ロード命令とストア命令には、ヒットまたはミスの 2 つのケースしかありません。そして、真ん中には主に 3 つの部分があります。ヒットしたかどうかの確認、ヒット後の操作、そしてミス後の操作です。

int CacheSim::check_cache_hit(_u32 set_base, _u32 addr) {
    
    
    /**循环查找当前set的所有way(line),通过tag匹配,查看当前地址是否在cache中*/
    _u32 i;
    for (i = 0; i < cache_mapping_ways; ++i) {
    
    
        if ((caches[set_base + i].flag & CACHE_FLAG_VAILD) && (caches[set_base + i].tag == ((addr >> (cache_set_shifts + cache_line_shifts))))) {
    
    
            return set_base + i;
        }
    }
    return -1;
}
int CacheSim::check_cache_hit(_u32 set_base, _u32 addr) 

ヒットをチェックするために渡されるパラメータは 2 つあり、1 つはセットのベース アドレス、もう 1 つは現在のトレース内のメモリ アドレスです。

set = (addr >>cache_line_shifts) % cache_set_size;
set_base = set * cache_mapping_ways;

グループアソシアティビティにおけるアドレスの領域分割に応じて、現在のアドレスがキャッシュのどのセットにマッピングされているかを知ることができます。(行が占める桁数はブロック内のオフセット アドレスであり、セットが占める桁数はグループ インデックスであるため、行が占める桁を削除し、残りの上位桁、つまりタグを取得します+set、直接剰余を取る、または上位ビットの後の係数を実行するタグによって占有されている桁を削除する可能性があります)

シミュレータではキャッシュは 1 次元配列として扱われるため、セットの先頭アドレスを取得することも必要です。(最初のグループがわかったら、最初のアドレス = グループの数 * グループのサイズ)

ヒットするかどうかの決定はタグの一致に基づいています。現在のアドレス addr のタグがキャッシュに設定されたマッピング内の特定の行タグと同じであり、この行が有効な場合は、これを(caches[set_base + i].tag == ((addr >> (cache_set_shifts + cache_line_shifts))))返し(caches[set_base + i].flag & CACHE_FLAG_VAILD)ますこの 1 次元配列キャッシュ内のラインindex(つまり、どのキャッシュ ライン/ブロック番号)。現在のセットで見つからない場合は、この addr 内のデータがキャッシュにロードされていないことを意味し、 を返します-1

5.5 ヒット後のアクション

if (index >= 0) {
    
    
    cache_hit_count++;
    //只有在LRU的时候才更新时间戳,第一次设置时间戳是在被放入数据的时候。所以符合FIFO
    if (CACHE_SWAP_LRU == swap_style)
        caches[index].lru_count = tick_count;
    //直接默认配置为写回法,即要替换或者数据脏了的时候才写回。
    //命中了,如果是改数据,不直接写回,而是等下次,即没有命中,但是恰好匹配到了当前line的时候,这时的标记就起作用了,将数据写回内存
    if (!is_read)
        caches[index].flag |= CACHE_FLAG_DIRTY;
}

ヒットしたら全員ハッピー、ヒット数++、置換アルゴリズムがLRUの場合はタイムスタンプを更新します(タイムスタンプに関することは後ほど詳しく説明します)。メモリに書き込む操作の場合、ヒット キャッシュ ライン フラグをダーティに設定する必要があります。これは、この命令がこのメモリ アドレスにデータを書き込むことを意味し、デフォルトのキャッシュ書き込みメソッドによりキャッシュ内にデータのコピーが存在するためです。はライトバック方式(ライトバック方式、ライトバック、つまりキャッシュに書き込むときはメインメモリに書き込まず、キャッシュデータを置き換えるときはメインメモリに書き戻す)なので、ここではは最初にダーティ データとしてマークされるだけであり、そのラインは置換時にそのラインのデータをメモリに書き戻します。

5.6 ミス後のアクション

index = get_cache_free_line(set_base);
set_cache_line((_u32)index, addr);
if (is_read) {
    
    
    cache_r_count++;
} else {
    
    
    cache_w_count++;
}
cache_miss_count++;

ミスした場合は、まず利用可能なライン (5.7) を取得し (おそらく現在のセットがいっぱいではなく空きがあるか、置換アルゴリズムによって取得された置換可能なラインである可能性があります)、次にそのメモリ アドレスにデータを書き込みます。 addr をキャッシュ ラインに追加する場合、対応する読み取りおよび書き込み通信の数も ++ である必要があります。cache_r_count は 5.3 のload_trace 関数セクションに導入されました。

5.7 get_cache_free_line は現在利用可能なライン関数を取得します。

_u32 CacheSim::get_cache_free_line(_u32 set_base)
もちろん、使用可能な行は現在マップされているセットから検索され、「1 次元配列」キャッシュ内のセットにマップされているインデックスが渡されます。

for (i = 0; i < cache_mapping_ways; ++i) {
    
    
    if (!(caches[set_base + i].flag & CACHE_FLAG_VAILD)) {
    
    
        if (cache_free_num > 0)
            cache_free_num--;
        return set_base + i;
    }
}

現在のセットを 1 回ループし、フラグ ビットの有効ビットを判定し、無効なキャッシュ ラインがある場合、そのラインは「アイドル」であることを意味します。インデックスを直接返すだけです。ループの最後に何も見つからず、現在のキャッシュ セットがいっぱいの場合は、置換アルゴリズムが実行されます。

free_index = 0;
if(CACHE_SWAP_RAND == swap_style){
    
    
    free_index = rand() % cache_mapping_ways;
}else{
    
    			//FIFO
    min_count = caches[set_base].count;
    for (j = 1; j < cache_mapping_ways; ++j) {
    
    
        if(caches[set_base + j].count < min_count ){
    
    
            min_count = caches[set_base + j].count;
            free_index = j;
        }
    }
}
if (CACHE_SWAP_LRU == swap_style)
    caches[index].lru_count = tick_count;

ランダム置換アルゴリズムのシードはCacheSimコンストラクターで初期化されます。FIFO (先入れ先出し) 先入れ先出し置換アルゴリズムの場合、キャッシュ ラインのタイムスタンプは、ラインがデータで満たされた時刻、つまり、キャッシュ ラインが「使用キュー」に入った時刻を記録します。 , したがって、FIFO はこのキューの最初のエントリの行を見つけ、それを置き換えます。LRU (最も最近使用されていない) は、最も最近使用されていない置換アルゴリズムです。ラインがアクセスされると、そのタイムスタンプが更新される必要があります。そのため、関数内では、キャッシュがヒットしたときに、そのタイムスタンプを更新する必要がありますdo_cache_op

FIFO で取得される min_count は、最初にキューに入ったラインですが、LRU では、このキュー内で長時間アクセスされていないラインです。

置換アルゴリズムによって取得されたこのラインの元のデータがダーティ データである場合、そのダーティ ビットをマークします。

if (caches[free_index].flag & CACHE_FLAG_DIRTY) {
    
    
    caches[free_index].flag &= ~CACHE_FLAG_DIRTY;
    cache_w_count++;
}

5.8 set_cache_line は、キャッシュ ラインのデータをメモリ関数に書き込みます。

void CacheSim::set_cache_line(_u32 index, _u32 addr) {
    
    
    Cache_Line *line = caches + index;
    // 这里每个line的buf和整个cache类的buf是重复的而且并没有填充内容。
    line->buf = cache_buf + cache_line_size * index;
    // 更新这个line的tag位
    line->tag = addr >> (cache_set_shifts + cache_line_shifts );
    line->flag = (_u8)~CACHE_FLAG_MASK;
    line->flag |= CACHE_FLAG_VAILD;
    line->count = tick_count;
}

入力は、「1 次元配列」キャッシュに書き戻されるラインのインデックスと、そのラインに書き込まれるメモリ アドレスです。ここでの buf は何の効果もありません。メモリ内のデータが現在の行に書き込まれることを意味するだけですが、キャッシュ シミュレーターには実際のデータ フローはありません。タグの更新は、第 1 章のアドレス分割の内容に従って、addr をシフト演算するだけです。最初にフラグがクリアされ、CACHE_FLAG_MASK = 0xff の場合、~CACHE_FLAG_MASK = 0x00 の場合、最後の有効ビットが有効になり、タイムスタンプが更新されます。

完了後、コードはコメント領域に配置されます

おすすめ

転載: blog.csdn.net/Strive_LiJiaLe/article/details/128814699