GF100のキャッシュの性能
Motivation
GTX480に代表されるGF100およびGTX460などのGF104にはL1, L2キャッシュが搭載され、メモリアクセスに関する制限が緩和されました。何も考えずに書くとGT200系では性能が出なかった場合でも、十分な性能が発揮されることがあります。
そこで、定数がテーブルに入っていて、それを参照して計算するというコードを例に、キャッシュの性能を測定します。
普通に書く
__device__ float f(float const x) { return 2 * x * x; } __global__ void kernel(float2 const* table, float* result) { result += blockIdx.x * 256 + threadIdx.x; float sum = 0; for (int loop = 0; loop < 256; ++loop) { for (int i = 0; i < 32; ++i) { sum += f(table[i].x) * table[i].y; } } *result = sum; }
まずは普通にCPUの様に書きます。
GT200系では、table[i]のアクセスがキャッシュされないため、メモリアクセスが毎ループ発生してしまいます。一方で、GF100系ではキャッシュによりメモリアクセスの回数軽減が見込まれる書き方です。
shared memory
__global__ void kernel(float2 const* table, float* result) { float2 __shared__ s[32]; result += blockIdx.x * 256 + threadIdx.x; if (threadIdx.x < 32) { s[threadIdx.x] = table[threadIdx.x]; } __syncthreads(); float sum = 0; for (int loop = 0; loop < 256; ++loop) { for (int i = 0; i < 32; ++i) { sum += f(s[i].x) * s[i].y; } } *result = sum; }
次はテーブルを一旦shared memoryに読み込み、それを使う書き方です。メモリアクセスは最適化されますが、本質的でない処理を記述せねばならず、開発コストは上昇してしまいます。
constant memory
float2 __constant__ table[32]; __global__ void kernel(float* result) { result += blockIdx.x * 256 + threadIdx.x; float sum = 0; for (int loop = 0; loop < 256; ++loop) { for (int i = 0; i < 32; ++i) { sum += f(table[i].x) * table[i].y; } } *result = sum; }
次にconstant memoryを使った書き方です。参照しかしないのでこういう書き方もできます。
texture cache
texture<float2, 1, cudaReadModeElementType> tex; __global__ void kernel(float* result) { result += blockIdx.x * 256 + threadIdx.x; float sum = 0; for (int loop = 0; loop < 256; ++loop) { for (int i = 0; i < 32; ++i) { float2 tmp = tex1Dfetch(tex, i); sum += f(tmp.x) * tmp.y; } } *result = sum; }
最後にキャッシュ繋りでtexture cacheを使う書き方です。
性能比較
GTX480とGTX285での実行時間の差を示します。単位はmsです。ブロック数は32768、スレッド数は256で統一しています。
GTX480 | GTX285 | |
cached | 461.71 | 8704.82 |
shared | 426.79 | 1327.15 |
constant | 426.32 | 1519.05 |
texture | 1635.48 | 2534.30 |
という結果になりました。2つのボードでは性能差があるので、sharedの結果を1とした相対値を以下に示します。
GTX480 | GTX285 | |
cached | 1.08 | 6.56 |
shared | 1.00 | 1.00 |
constant | 1.00 | 1.14 |
texture | 3.83 | 1.91 |
まず、texture cacheはこういう用途に使うのは適さないようです。そして、どちらのボードでもshared memoryを使うのが最速であり、またGF100のキャッシュを頼ってもそこそこの速度が出るようです。GT200からGF100でconstant memoryの参照速度も向上しているようです。
予想できる局所性がある場合はshared memoryを使うべきでしょうが、予測が困難な場合はL1キャッシュを頼るのも手であると考えられます。とりあえずL1を使う書き方をしておいて、あとから最適化でshared memoryを使うようにする戦略もありそうです。