キャッシュの局所性:読み出す順序で何が変わるか

8 × 8のint型の行列が、メモリ上に行ごとに並んでいます。プロセッサとの間にあるのは、16バイトのラインを8本持つキャッシュ。行ごと、列ごとに行列を足し合わせながら、読み出しを追ってみましょう。各マスの数字は、そのアドレスに割り当てられたキャッシュラインです。ミスは黄、ヒットは緑、ミスの際に一緒に読み込まれた隣の要素は淡い色で示します。

インタラクティブシミュレーションを読み込んでいます...

レッスン

理論 — キャッシュの局所性:読み出す順序で何が変わるか

キャッシュは、主記憶のデータのコピーを保持する、小容量で高速なメモリです。データはライン単位で保持し、各ラインにはLバイト境界にそろえたLバイトのブロックが一つ入ります。読み出すブロックがすでにラインにあればヒット、なければミスとなり、ブロック全体を読み込みます。S本のラインを持つダイレクトマップ方式のキャッシュでは、アドレスaを含むブロックの格納先は、⌊a / L⌋ mod Sで求まるライン一つに決まっています。隣り合うアドレスを順に読むループでは、一度読み込んだブロックから続けていくつも要素を読めます。この性質を空間的局所性といいます。

各記号の意味

L
ラインのバイト数。ここでは16。一般的なデスクトップやサーバーのプロセッサでは64
S
ラインの本数。ここでは8
前提
最初は空のダイレクトマップ方式のキャッシュ、4バイトのint型、Cと同じく行ごとに格納された行列を仮定します。Fortranでは列ごとに格納するため、速い読み出し順が逆になります。数えるのは行列の読み出しだけです。実際の1次データキャッシュはもっと大きく、セットアソシアティブ方式で、一般には8ウェイです。ブロックは所属するセット内の8本のラインのどこにでも置けるため、ここで見られる競合の多くは解消されますが、容量不足は解消されません。要求される前に次のラインを読み込むハードウェアプリフェッチも、このモデルには含めていません。

同じ64回の加算でも、ヒットは48回にも0回にもなる 🖖

8 × 8を行ごとに、次に8 × 8を列ごとにを押してみましょう。どちらも同じ64個のアドレスから、同じ64個のint型の要素を読み出します。変わるのは順序だけです。行ごとに読むと、16バイトの各ラインで最初の読み出しがミスになり、4個の要素がまとめて読み込まれます。続く3個はすでにキャッシュにあるので、計48回のヒット。列ごとでは、次に読む要素は1行先、32バイト離れた別のラインにあります。ここでは1行がちょうどライン2本分なので、第0列の割り当て先はライン0、2、4、6に限られ、a[4][0]を読むとa[0][0]が読み込んだブロックが追い出されます。ループがa[0][1]に戻るころには、そのブロックはもうありません。64回すべてがミスになります。Ulrich Drepperは2007年、実機でこの現象を測定しました。Intel Core 2上で1000 × 1000の行列積を計算する際、列ごとに読んでいた行列をあらかじめ転置し、両方の行列を行ごとに読むようにすると、必要なクロックサイクル数は元の23.4%になりました。

行列がキャッシュに収まる間は、順序による差はない 🖖

大きさを5にして、読み出し順を切り替えてみましょう。どちらも25回中18回のヒットです。行列は100バイトで、キャッシュは128バイト。行列の7個のブロックはそれぞれ異なる7本のラインに割り当てられ、一度読み込まれたブロックが追い出されることはありません。どの順序でも、各ブロックの最初の読み出しだけがミスになります。6 × 6では行列が144バイトになり、2個のブロックが同じラインを使わなければならず、結果に差が出ます。行ごとは27回、列ごとは25回のヒットです。余白を加えなければ、ここから16 × 16まで、どの大きさでも列ごとのヒット数が少なくなります。

各行に未使用のint要素を4個加えると、ヒットが0回から48回に 🖖

行末に余白を追加を押してみましょう。同じ8 × 8の行列を列ごとに読みますが、各行の後ろに、読み出されないint型の要素が4個ずつ入っています。そのため1行は48バイトとなり、ライン2本分から3本分に延びます。3と8は互いに素なので、第0列の割り当て先は8本すべてのラインに広がります。1列分の8個のブロックが同時にキャッシュに収まり、次の3列ではそのまま使えるため、64回中48回のヒット。行ごとと同じです。その代わり、読み出さない領域に128バイトを使います。この余白が解消するのは、ほかのラインが空いているのに複数のブロックが同じラインを奪い合う、競合ミスです。キャッシュの容量そのものを増やすことはできません。最後の例題では、この容量が問題になります。

全プロセスの詳細解説

  1. 読み出し順によって、メモリからの転送量はどれだけ増えるか 4 ステップ

    8 × 8を列ごとにを押し、ヒットが0回になる理由を説明してください。次に、4バイトのint型からなる1024 × 1024の行列と、ラインサイズ64バイト、容量32 KiBのダイレクトマップ方式のキャッシュを考えます。行ごとに足し合わせる場合と列ごとに足し合わせる場合、メモリから何バイト転送されるでしょうか。各行の末尾にライン1本分の余白を加えると、改善するでしょうか。

    この問題をツールで開く

    1. このページのモデルでは、1行は8 × 4 = 32バイトで、ライン2本分です。したがって行iの先頭はライン2i mod 8に割り当てられ、第0列が使うのはライン0、2、4、6だけ。a[4][0]がa[0][0]のあったラインを使うため、ループがa[0][1]に戻るときには必要なブロックがなく、64回中0回のヒットになります。行ごとなら、16バイトの各ブロックでミスは一度ずつ、計16回なので、ヒットは48回。ヒット率は1 − 4/16 = 75%です。

    2. 大きい方のキャッシュには、32,768 / 64 = 512本のラインがあります。行ごとに読むと、64バイトの各ラインにint型の要素が16個入り、最初の読み出しだけがミスになります。ミスは1,048,576 / 16 = 65,536回、ヒット率は1 − 4/64 = 93.75%。転送量は65,536 × 64 = 4,194,304バイトで、行列全体をちょうど一度読み込む量です。

    3. 列ごとでは、1行が4,096バイト、ライン64本分なので、行iの先頭はライン64i mod 512に割り当てられます。各列が使うラインは512本中8本だけです。a[8][0]がa[0][0]のブロックを追い出し、ループがa[0][1]に戻るよりずっと前に、必要なブロックは失われています。1,048,576回の読み出しがすべてミスとなり、転送量は67,108,864バイト。行ごとの16倍です。

    4. 各行にint型の要素16個分の余白を加えると、1行は4,160バイト、ライン65本分になります。65と512は互いに素なので、各列の割り当て先は512本すべてのラインに広がります。それでもヒットはありません。1列で読み出すブロックは1,024個、キャッシュに収まるのは512個なので、次の列で必要になる前に、どのブロックも追い出されてしまいます。

    解答

    行ごとは4 MiB、列ごとは64 MiB。余白によって列の割り当て先はキャッシュ全体に広がりますが、ヒットは増えません。競合は解消できても、この行列では各列で読み出すブロックだけでキャッシュ全体の容量を超えるからです。

学習の道すじ

コンピュータはどうやってプログラムを動かすのか

参考文献 (1)