今回は、CPUキャッシュとメモリの話です。若干マニアックかもしれない。
記憶装置

コンピュータを構成する五大要素の一つに「記憶装置」というものがあります。記憶装置には「主記憶装置」やら「補助記憶装置」やらの種類がありますが、ヒエラルキーがあります(別にヒエラルキーじゃないんですけどね)。
このヒエラルキーは、CPUがどれくらいの速度でアクセスする事ができるかというものです。ヒエラルキーが高いほど高速(=短い時間でCPUがアクセス可能)ですが、原則容量が小さくなります。
このヒエラルキーの頂点である記憶装置であるレジスタはもはやCPUそのものに内蔵されていますし、実質的に最も遠い記憶装置となるネットワークストレージは、もはや別のシステムです。

最もCPUに近い記憶装置である「レジスタ」は、CPUが処理を行うのに必要とする各種の情報を保持しているものです。容量は数bit~数十bit程度。
基本的にCPUがなにかの処理を行うときは、レジスタがメインメモリからデータや命令を取り出し、実際に演算を行うALU(加算器)や命令デコーダがレジスタからデータや命令を取り出すという流れになります。結果の書き込みも、レジスタを経由してメインメモリに書き込まれます。
この処理を見ると、ALUや命令デコーダが、直接メインメモリを参照すればいいのではないかと思うかもしれませんが、メインメモリは遅いのです。なので、一旦レジスタに値をおいておくことで効率的に処理を行うことができます。
実際にこのあたりの話は、CPUがどのように処理を行っているのかや、パイプライン方式の処理実行方式を理解しないと分かりづらいので、今度お話しようと思います。
この記事を読むにあたっては「CPUの処理のためにメインメモリからレジスタにデータが取り出される」「メインメモリは遅い」ということを理解しておいていただければと思います。
クロックサイクル

基本的にCPUは、クロック信号毎に行動を行います。
クロック信号とは、CPU内部の発振器によって発せられる信号です。1秒間に1回クロック信号が発せられると1 Hzの動作周波数のCPUとなります。つまり、現代のPC向けCPUの場合、1 GHz(10億 Hz)以上はあるので、1秒間に10億回以上クロック信号が発せられていることになります。つまり、CPUは1秒間に10億回以上行動ができるということになりますね。
クロック信号はあくまでONとOFFなので、1回発せられたら(ONになったら)再度OFFになる必要があります。つまり、クロックはONとOFFのサイクルが超高速で連続しているということになります。このサイクルを「クロックサイクル」といいます。1回のクロックサイクルを以下「1-cycle」のように表現します。
さて、CPUはこのクロックサイクルごとに行動すると言いましたが、1-cycleでこなせる行動もあれば、数-cyleにわたらないと完了できないものもあります。
CPUによっては、図のようにONになったときにのみ動作する場合と、ONになったときとOFFになったときに動作する場合があります。
メインメモリは遅い

で、なぜクロックサイクルの話をしたかというと「メインメモリは遅い」をもう少し深堀りするためです。
CPUがななにかの処理を実行するとき、流れとして上の図の通りの流れになります。
前述の通り、CPUの演算実行自体はそれほどクロックサイクルを消費しません。というか、メインメモリからデータや命令を取り出す以外の処理は実は数-cycleで終わります。
一方、メインメモリへの読み書きというのは非常に時間がかかります。どれくらいかというと大体100~300-cycle程度。他の処理が10-cycle未満で処理できるのに対して、メインメモリからのデータの取り出しだけがその数十倍の時間がかかるのです。
メインメモリアクセスに時間がかかる理由はいくつかあり、メインメモリはCPUの外部のチップであること、メインメモリに使用されるDRAMの構造上(後述)、そもそも遅いことなどが挙げられます。そもそも、CPUはあまりにも高速であるため、データがCPU外に関係するとき、データバス(通り道)がネックとなることが多いのです。
キャッシュの役割

メインメモリへのアクセスには数百-cycleかかることをお伝えしました。これを解決する方法があります。「キャッシュ」です。
CPUキャッシュメモリは、CPUに搭載されている記憶装置で、記憶装置の中では、主記憶装置(メインメモリ)よりもCPUに近く、レジスタよりも遠い立ち位置の記憶装置です。
キャッシュメモリは階層化されており、多くのCPUの場合、2階層~3階層のキャッシュを採用しています。CPUから近い順に、1次(L1)キャッシュ、2次(L2)キャッシュ...となっており、これもまたL1キャッシュに行けば行くほど容量は小さく、アクセスにかかるサイクルは少なくなっています。
例として、Intelの「Golden Cove」*1では、L1キャッシュが5-cycle、L2キャッシュが15-cycleでアクセスすることが可能です。メインメモリが100~300-cycle程度かかると考えると、数十倍の速度でアクセスできるキャッシュメモリは非常に有用です。
CPUのデータのアクセス手順

CPUはデータを必要としたとき、そのデータを取り出すためにCPUから近い順に記憶装置を辿っていきます。
まず、CPUはキャッシュメモリからのデータ取り出しを試みます。このとき、キャッシュメモリにデータがあればキャッシュヒットとなり、メインメモリにアクセスするのに比べて大幅に性能が向上します。ちなみに、キャッシュメモリからデータを取り出すときは、L1キャッシュから順に見ていくので、L2キャッシュでキャッシュヒットするよりも、L1キャッシュでキャッシュヒットする方が処理速度が向上します。
もし、キャッシュメモリにデータがなかった場合、キャッシュミスとなりメインメモリからのデータの取り出しを試みます。この場合、通常のメインメモリアクセスに加えて、キャッシュメモリからデータの取り出しを試みたサイクル分ものしかかるので、ただでさえ遅いメインメモリアクセス(~300-cycle)に、追加ペナルティ(~100-cycle)がのしかかるので大幅に速度が低下します。
さらに、メモリスワップ等によってメインメモリにすらデータなかった場合、補助記憶装置からのデータ取り出しを試みます。こうなれば、数千~数万-cycle消費することになり、非常に速度が低下します。
キャッシュメモリにデータが貯められるタイミング
次に、キャッシュメモリにデータが書き込まれるタイミングについてです。当たり前ですが、キャッシュメモリの容量はメインメモリを上回ることは基本的にありません。メインメモリの内容をキャッシュにすべて書き込めれば嬉しいですが、残念ながらできません。
そのためキャッシュにはできる限り「CPUがアクセスする可能性が高いデータや命令」を保存することが好ましいです。では、どのようにキャッシュにメモリのデータが書き込まれるのでしょうか。
主に2つパターンがあります。見ていきましょう。

1つ目のパターンは、一度CPUが参照したデータは再度アクセスされる可能性が高いものとして、メインメモリからデータが取り出されるとき、同時にキャッシュにも書き込むパターン。1回目のアクセスではキャッシュミスが発生しますが、2回目以降はキャッシュから消えていない限りキャッシュヒットとなり処理速度が向上します。このキャッシュのされ方を「デマンドフェッチ」(Demand fetch)あるいは「要求時フェッチ」をといいます。

2つ目のパターン。参照される可能性のあるデータをあらかじめ予測してキャッシュメモリに移動する方法です。これを「プリフェッチ」(Pre fetch)といいます。
この予測に関しては、投機実行としてOut-of-Order(OOO)とか色々ありますが、ここでは割愛します。
デマンドフェッチとプリフェッチは、CPUがデータを取り出す前にキャッシュされる方法でした。では、CPUが処理を終えたあと、メモリに結果を書き込むときはどうでしょうか。
メインメモリからデータを取り出すときと同様、書き込むときも数百-cycleを消費します。加えて、処理されたデータというのは再度参照される可能性が高いのでキャッシュメモリにあったほうが便利です。
このことから書き込み時もキャッシュメモリが活用されます。
これも主に2パターンあるのでご紹介します。

1つ目は、キャッシュメモリに書き込まれたタイミングで、同時にメモリにも書き込むパターン。これを「ライトスルー方式」(Write-through)といいます。この方式は、メインメモリとキャッシュメモリの一貫性が保たれるものの、書き込みの度にメモリアクセスが発生するため、遅いです。

2つ目は、演算結果をキャッシュメモリには書き込むが、メインメモリには即座に書き込まないというものです。メインメモリに書き込むタイミングは、「キャッシュメモリから追い出されたとき」、あるいは「明示的にキャッシュメモリの内容をメインメモリに書き込んだとき」となります。これを「ライトバック方式」(Write-back)といいます。この方式は、メモリアクセスが発生する機会が少ないので、メモリ帯域がそれほど消費しませんが、メインメモリとキャッシュメモリの一貫性を保てなくなったりします。
ちなみに、キャッシュメモリから追い出されるタイミングというのは、新しいデータがキャッシュメモリに保存され、容量が少なくなったタイミングで、最も古いデータから追い出されていきます。
まとめ
では、まとめましょう。
- CPUはメインメモリからデータを取り出す
- ただしメインメモリの読み書きは非常に遅い(普通の処理の数十倍~数百倍)。
- その遅さを埋めるためにキャッシュを使用する
となります。
以下、記事は続きますが、補足ともう少し深堀りした内容です。
【補足】メインメモリとキャッシュメモリの構造
メインメモリは、DRAM(Dynamic Random Access Memory)を使用しています。DRAMは、揮発性メモリでデータの維持に電源が必要です。
さらに、周期的にデータの再書き込み(リフレッシュ)をしないとデータを消失してしまうという欠点も備えています。このため、リフレッシュとメモリアクセスが衝突してしまった場合、リフレッシュを待たなければならないので、メモリアクセスがより遅くなります。
一方で、低コストで大容量化することができるという利点があり、大容量のメインメモリに好適なのです。
キャッシュメモリは、フリップフロップ回路を採用したSRAM(Static Random Access Memory)を使用しています。SRAMも揮発性メモリでデータの維持に電源が必要ですが、リフレッシュは不要です。さらに高速です。
しかし、高価で大容量化しにくいという欠点が存在しており、キャッシュメモリのような使用方法が好適です。
メインメモリがデータを渡せる量
ついで、メインメモリがCPUにデータを渡せる量について。
記事では「メモリからデータを取り出す」という表現をしましたが、これはCPU目線の話であり、実際に「メモリが1サイクル(メモリのクロックサイクル)で渡せるデータの量」というのが存在します。これが「メモリバス幅」です。
メモリバス幅は、1メモリクロックサイクルでCPUに渡せるデータ量を示します。ただしDDRメモリでは立ち上がりと立ち下がりの両方でデータを送信するため、実効転送量はクロック1サイクルあたり2回分のデータになります。
基本的に、メモリバス幅はメモリチャネル数によって決まります。通常、メモリチャネルあたり64-bitのメモリバス幅となり、一般的向けCPUの場合、2チャネルなので128-bitである事が多いです。
まれに、AMD Ryzen AI MaxやApple Siliconなど、256-bitやそれ以上のメモリバス幅を有している場合もありますし、サーバー向けCPUなどでは6チャネルや8チャネルで512-bitや多くて1024-bitのメモリバス幅を有している場合もあります。
また、GPUなどではメモリ帯域が重要なため、CPUよりも大きなメモリバス幅を有しています。
これを基に、1秒間にメモリがデータを渡せる量を示したのが「メモリ帯域幅」です。
メモリ帯域幅は、データレート ✕ メモリバス幅 で計算することができます。
例として、「Ryzen 9 9950X」(Zen 5)を基にメモリ帯域幅を計算してみましょう。Ryzen 9 9950Xは2チャネルなので128-bit(=16-Byte)のメモリバス幅を有しています。また、メモリは使用上、データレート 5,600 MT/sのDDR5-5600に対応しています。
16-Byte ✕ 5,600 MT/s = 89,600 MB/s = 89.6 GB/s
よって89.6 GB/sの帯域幅を有していることになりますね。
おわり。ほなまた。
この記事を書いた人
西田(総合情報学部 情報学科 2021年入学)
通信研究会OB。当ホームページの保守運用を支援しています。組み込み系のソフトウェアエンジニア。応用情報技術者・修習技術者。
*1:第12世代CoreプロセッサであるAlder LakeのPコアや、第5世代Xeon SPである「Sapphire Rapids」に採用されているCPUマイクロアーキテクチャ

