まず第一に、メインメモリへのアクセスは非常に高価です。現在、2 GHz の CPU (最も遅いもの) は 1 秒あたり 2G ティック (サイクル) です。CPU (最近の仮想コア) は、ティックごとに 1 回、レジスタから値を取得できます。仮想コアは複数の処理ユニット (ALU - 算術論理演算ユニット、FPU など) で構成されているため、可能であれば特定の命令を実際に並列処理できます。
メインメモリのアクセスは70ns~100ns程度(DDR4の方が若干速い)。今回は基本的に、L1、L2、および L3 キャッシュを検索し、メモリにヒットし (コマンドをメモリ コントローラに送信し、メモリ コントローラがそれをメモリ バンクに送信します)、応答を待って完了します。
100ns は約 200 ティックを意味します。したがって、基本的に、プログラムが各メモリにアクセスするキャッシュを常に見逃す場合、CPU は (メモリを読み取るだけの場合) 時間の約 99.5% をアイドル状態でメモリを待機することに費やします。
物事をスピードアップするために、L1、L2、L3 キャッシュがあります。それらは、チップに直接配置されたメモリを使用し、特定のビットを格納するために異なる種類のトランジスタ回路を使用します。CPU は通常、より高度なテクノロジを使用して製造され、L1、L2、L3 メモリの製造上の失敗は CPU を無価値 (欠陥) にする可能性があるため、これにはメインメモリよりも多くのスペース、エネルギー、およびコストがかかります。大規模な L1、L2、L3 キャッシュはエラー率を増加させ、歩留まりを低下させ、ROI を直接低下させます。そのため、利用可能なキャッシュ サイズに関しては大きなトレードオフがあります。
(現在、特定の部分を非アクティブ化して、実際の製品の欠陥がキャッシュ メモリ領域であり、全体として CPU の欠陥をレンダリングする可能性を減らすために、より多くの L1、L2、L3 キャッシュを作成します)。
タイミングのアイデアを提供するには(出典:キャッシュとメモリにアクセスするためのコスト)
- L1 キャッシュ: 1ns ~ 2ns (2 ~ 4 サイクル)
- L2 キャッシュ: 3ns ~ 5ns (6 ~ 10 サイクル)
- L3 キャッシュ: 12ns ~ 20ns (24 ~ 40 サイクル)
- RAM: 60ns (120 サイクル)
さまざまな CPU タイプを混在させているため、これらは単なる推定値ですが、メモリ値がフェッチされ、特定のキャッシュ レイヤーでヒットまたはミスが発生する可能性がある場合に実際に何が起こっているかを示す良いアイデアを提供します。
したがって、キャッシュは基本的にメモリ アクセスを大幅に高速化します (60ns 対 1ns)。
値をフェッチし、再読み取りの機会のためにキャッシュに保存することは、頻繁にアクセスされる変数には適していますが、メモリコピー操作の場合、値を読み取り、値をどこかに書き込み、値を読み取ることはないため、それでも遅くなります。繰り返しますが...キャッシュヒットはなく、非常に遅いです(実行順序が間違っているため、これは並行して発生する可能性があります)。
このメモリ コピーは非常に重要であるため、速度を上げるためのさまざまな手段があります。初期の頃、メモリは CPU の外部にメモリをコピーできることがよくありました。これはメモリ コントローラによって直接処理されたため、メモリ コピー操作によってキャッシュが汚染されることはありませんでした。
しかし、プレーン メモリ コピーのほかに、メモリのシリアル アクセスは非常に一般的でした。一例として、一連の情報の分析があります。整数の配列を持ち、合計、平均、平均を計算したり、特定の値を見つけたり (フィルター/検索) することは、汎用 CPU で毎回実行される非常に重要なアルゴリズムのクラスです。
そのため、メモリ アクセス パターンを分析すると、データが非常に頻繁にシーケンシャルに読み取られることが明らかになりました。プログラムがインデックス i の値を読み取る場合、プログラムは値 i+1 も読み取る可能性が高くなりました。この確率は、同じプログラムが値 i+2 などを読み取る確率よりもわずかに高くなります。
そのため、メモリアドレスが与えられた場合、先読みして追加の値をフェッチすることは良い考えでした (そして今でもそうです)。これがブーストモードがある理由です。
ブーストモードでのメモリアクセスとは、アドレスが送信され、複数の値が連続して送信されることを意味します。追加の値を送信するたびに、追加で約 10ns (またはそれ以下) しかかかりません。
もう1つの問題はアドレスでした。アドレスの送信には時間がかかります。メモリの大部分をアドレス指定するには、大きなアドレスを送信する必要があります。初期の頃は、アドレス バスが 1 サイクル (ティック) でアドレスを送信するのに十分な大きさではなく、アドレスを送信するのに複数のサイクルが必要であり、さらに遅延が追加されていました。
たとえば、64 バイトのキャッシュ ラインは、メモリがサイズが 64 バイトの別個の (重複しない) ブロックに分割されていることを意味します。64 バイトとは、各ブロックの開始アドレスの下位 6 アドレス ビットが常にゼロであることを意味します。したがって、これらの 6 つのゼロ ビットを毎回送信する必要はありません。アドレス バス幅の数に関係なく、アドレス空間を 64 倍に増やす必要はありません (歓迎効果)。
キャッシュ ラインが解決するもう 1 つの問題 (先読みとアドレス バス上の 6 ビットの保存/解放に加えて) は、キャッシュの編成方法にあります。たとえば、キャッシュが 8 バイト (64 ビット) のブロック (セル) に分割される場合、メモリ セルのアドレスを格納する必要があります。このキャッシュ セルは、値を保持します。アドレスも 64 ビットの場合、キャッシュ サイズの半分がアドレスによって消費され、100% のオーバーヘッドが発生します。
キャッシュ ラインは 64 バイトであり、CPU は 64 ビットを使用する可能性があるため、6 ビット = 58 ビット (ゼロ ビットを適切に格納する必要はありません) は、58 ビット (11% のオーバーヘッド) のオーバーヘッドで 64 バイトまたは 512 ビットをキャッシュできることを意味します。実際には、格納されるアドレスはこれよりもさらに小さいですが、ステータス情報があります (キャッシュ ラインが有効で正確であるか、ダーティで RAM に書き戻す必要があるかなど)。
もう 1 つの側面は、セットアソシアティブ キャッシュがあることです。すべてのキャッシュ セルが特定のアドレスを格納できるわけではなく、それらのサブセットのみを格納できます。これにより、必要な格納アドレス ビットがさらに小さくなり、キャッシュの並列アクセスが可能になります (各サブセットは 1 回だけアクセスできますが、他のサブセットとは独立しています)。
異なる仮想コア、コアごとの独立した複数の処理ユニット、および最終的に 1 つのメインボード上の複数のプロセッサ (48 個以上のプロセッサを収容するボードがある) 間のキャッシュ/メモリ アクセスの同期に関しては、特に重要です。
これが基本的に、キャッシュ ラインを使用する現在の考え方です。先読みの利点は非常に高く、確率が非常に低いため、キャッシュ ラインから 1 バイトを読み取って残りを再度読み取らないという最悪のケースは非常にわずかです。
キャッシュ ラインのサイズ (64) は、より大きなキャッシュ ライン間の賢明な選択のトレードオフであり、近い将来、キャッシュ ラインの最後のバイトが読み取られる可能性が低くなり、完全なキャッシュ ラインを取得するのにかかる時間になります。メモリから (およびそれを書き戻すため)、キャッシュ構成のオーバーヘッド、およびキャッシュとメモリ アクセスの並列化も必要です。