シェアードメモリとキャッシュメモリの構造の違い
シェアードメモリの場合は、デバイスメモリからデータをコピーしてくると、それは単なるデータ(値)であり、元のデバイスメモリのアドレスとは関係が切れてしまう。したがって、シェアードメモリのデータは、シェアードメモリに付けられたアドレスを指定してアクセスすることになる。
一方、キャッシュの場合は、値だけではなく、元のデバイスメモリのアドレス、そして、値がGPUコアによる書き込みで変更されているかなどの属性を示すタグが付けられている。そして、キャッシュの内容をアクセスするには、キャッシュメモリアレイの中の位置ではなく、元のデバイスメモリのアドレスを使う。
そのため、図3-23に示すように、シェアードメモリはシェアードメモリのアドレスを使って単純にデータメモリのアレイをアクセスすれば良いが、キャッシュの場合は、まず、タグアレイを読み出し、それとアクセスするデバイスメモリアドレスのタグ部を比較して、一致していれば正しいアクセスでHitである。この場合は、データメモリアレイから読み出されたデータは指定したデバイスメモリアドレスのデータである。一方、タグが異なっていれば、グローバルアドレスのインデックス部は一致しているが、タグ部は異なる別アドレスのデータであるのでMissであり、読み出されたデータは意味が無い。
図3-23のキャッシュはタグとデータメモリアレイが1組のダイレクトマップ方式のキャッシュであるが、実際には複数のウェイ(Way)を持つセットアソシアティブキャッシュが用いられる。なお、NVIDIAはL1Dキャッシュの内部構成を発表していないので、筆者の推定であるが、図3-24に示すように、複数セットのタグマッチ回路を持ち、アクセスに際しては、まず、タグを引いて、ヒットしたway番号を求める。そして、そのway番号をデータメモリをアクセスする下位のアドレスとして正しいデータを読み出すと考えられる。このようにすれば、シェアードメモリとのメモリアレイの共用がやり易いと思われる。なお、ここでは、各wayのヒット情報をエンコードしてバイナリのアドレスビットを出力する絵になっているが、メモリアレイのデコーダを細工して、One-Hotのヒット信号でそれぞれのwayを選択することができるようになっている可能性もある。
どのタグマッチもヒットしなかった場合は、キャッシュ全体としてMissであり、目指すデータはキャッシュには入っていないので、L2キャッシュをアクセスすることになる。なお、この図では書き込み側の制御は省略している。
そして、GPUのキャッシュはWarpやWavefrontと呼ばれるスレッドグループのアクセスを処理する必要があるが、図3-20のシェアードメモリのように、スレッドごとに独立にアドレスを指定できるようにするのはハードが膨大になってしまう。従って、図3-25に示すように、一時には横1行のアクセスに限定され、シェアードメモリのようにバンクごとに異なる段をアクセスすることはできない。
そのため、32スレッドがアクセスするアドレスを調べて、横1行に含まれるアクセスをまとめるコアレス(Coalesce)ユニットが必要となる。すべてのスレッドのアクセスが横1行の128バイトに含まれる場合は、1回のメモリアクセスで済むが、アドレスが複数の行にまたがっている場合は、コアレスユニットは32スレッドすべてのアドレスがカバーされるまで複数回のアクセスを行うことになる。最悪の場合は、32スレッドのアクセスがすべて異なる行になっていると32回のアクセスが必要となり、メモリアクセスに32倍の時間が掛かってしまう。
このようにシェアードメモリと1次データキャッシュはどちらもコア内の小規模メモリであることから、NVIDIAのFermi GPUやKepler GPUでは、1つの32バンクの64KBのメモリアレイを使い、一部をシェアードメモリ、残りの部分をキャッシュとして使うようになっている。シェアードメモリの量は、以前のGPUに装備されていた16KBに加えて、新たに32KBと48KBの指定が可能になっている。
16KBを指定すれば以前のGPUと同じ容量であるので、以前のGPU用に書いたプログラムがそのまま動くが、32KBや48KBを指定して、増加したシェアードメモリを有効に使って性能を向上させようとする場合は、プログラムを修正する必要がある。
一方、残りのキャッシュ部分は、自動的に、グローバルメモリやローカルメモリのアクセスされたアドレスのデータを保持して、以降のアクセスを高速化するので、プログラムの修正なしに自動的に利用することができる。