コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB
メモリ階層(レジスタ→キャッシュ→RAM→ディスク)
速いメモリを 8GB 積めば済む話ではない
CPU が 1 回の足し算にかける時間は 1 ナノ秒を切ります。ところが、その足し算に使う数値を RAM から取り寄せると 100 ナノ秒ほどかかります。CPU から見ると、数値が届くまでに 100 回分の仕事ができてしまう計算です。
では RAM をもっと速い素材で作ればよいかというと、そうはいきません。速い SRAM は 1 ビットあたりのトランジスタ数が多く、密度が低くて高価です。8GB 分を SRAM で作ると、値段も発熱も現実的な範囲を超えます。安くて大容量の DRAM は遅い。ここが動かせない前提です。
全部を速くできないので、近くに置く
そこで現代のコンピューターは、速くて小さい記憶と、遅くて大きい記憶を積み重ねています。CPU のすぐ内側から順に、下へ行くほど桁違いに大きく、桁違いに遅くなります。
| 場所 | 容量の目安 | 取り寄せにかかる時間 |
|---|---|---|
| レジスタ | 数十バイト | 1 ナノ秒未満 |
| L1 キャッシュ | 数十 KB | 約 1 ナノ秒 |
| L3 キャッシュ | 数 MB | 約 10 ナノ秒 |
| RAM | 数 GB | 約 100 ナノ秒 |
| SSD | 数百 GB | 約 100,000 ナノ秒 |
レジスタと SSD の間には 10 万倍以上の開きがあります。CPU はまず一番近いところを見て、無ければ 1 つ下へ問い合わせます。この「無かった」が キャッシュミス です。
なぜ、たったこれだけで間に合うのか
L1 が数十 KB しかないのに実用になるのは、プログラムのアクセスに偏りがあるからです。同じ変数がループの中で何度も読まれ、配列は端から順になめられます。前者を時間的局所性、後者を空間的局所性と呼びます。どちらも証明された法則ではなく、実測から得られた経験則です。それでも十分に当たるので、普通に書かれたプログラムなら L1 のヒット率は 90% を超えます。
キャッシュはこの偏りに賭けています。だから、偏りを壊す書き方をすると賭けが外れます。
Python
import numpy as np
a = np.zeros((4096, 4096))
for i in range(4096):
a[i, :] += 1 # 行に沿って進む。次に触るのは隣の番地
for j in range(4096):
a[:, j] += 1 # 列に沿って進む。1 要素ごとに 32KB 先へ飛ぶ更新する要素数は同じですが、下の書き方は毎回離れた番地へ飛びます。CPU は 1 バイト欲しいときでも 64 バイトまとめて持ってくるので、下では持ってきた 64 バイトのうち 8 バイトだけ使って捨てることになります。上の書き方が速いのは、持ってきた 64 バイトを最後まで使い切るからです。データの並び順と、なめる順番をそろえる。それだけで速くなることがあります。
現場の話 「メモリを増やしたのに速くならない」という相談はよくあります。容量が増えても CPU からの距離は変わりません。増設で速くなるのは、遅い層への追い出しが起きていた場合だけです。