2011年11月23日水曜日

CPUキャッシュによるプログラム高速化の例


CPUのキャッシュメモリを使うで、プログラムの実行性能をあげることができます。このエントリでは、キャッシュメモリを活用することでどの程度性能が向上するのか見てみます。

動機
コンピュータの記憶領域は、レジスタ、キャッシュメモリ、メインメモリなどからなる階層構造になっています。レジスタやキャッシュメモリはメインメモリより容量が小さいものの、非常に高速です。

CPUが直接触れるのはレジスタのみです。レジスタとメインメモリでは、データアクセスの速度に約100倍の差があり、その差を埋める目的で、CPU内部にキャッシュメモリがあります。キャッシュメモリを活用できれば、プログラムの実装性能を大きく高めることができます。

ただし、基本的に、CPUがどのようにキャッシュメモリを使うのかをプログラマが直接指示することはできません。なので、キャッシュメモリを活用するには、CPUがどのようにメモリ上のデータを使うのかを考慮したプログラムを書くことで間接的にキャッシュメモリを使うことになります。

※理化学研究所の京で使われているSPARC64 VIIIfxにはセクタキャッシュという機構があり、プログラマがキャッシュを制御できます。セクタキャッシュを使うと、キャッシュメモリを2つの領域に分割し、データごとにキャッシュに使う領域をプログラマが指定できます。

このエントリでは、キャッシュメモリを活用することで、実際にどの程度性能が向上するのか、一例を見てみます。


サンプルコード
1000×1000行列の積を求めるプログラムについて、キャッシュブロック化によって実行性能を最適化します。

以下が、最適化前のCのコードです。(code1.c)
#include <stdio.h>
#include <stdlib.h>
 
#define N 1000
 
int main ()
{
  double A[N][N], B[N][N], C[N][N];
  int i, j, k;
  
  // initialize the matrices
  for ( i = 0; i < N; i++ )
    for ( j = 0; j < N; j++ )
      {
        A[i][j] = 1.0;
        B[i][j] = 1.0;
        C[i][j] = 0.0;
      }
  
  // matrix multiplication
  for ( i = 0; i < N; i++ )
    for ( j = 0; j < N; j++ )
      for (k = 0; k < N; k++ )
        C[i][j] += A[i][k] * B[k][j];
  
  printf ( "%f\n", C[0][0] );
  
  return 0;
}

これを、キャッシュブロック化によって以下のコードに書き換えます(code2.c)。もちろん、計算の結果そのものは変わりません。
$ cat code2.c
#include <stdio.h>
#include <stdlib.h>
 
#define N 1000
 
int main ()
{
  double A[N][N], B[N][N], C[N][N];
  int i, j, k;
  
  // initialize the matrices
  for ( i = 0; i < N; i++ )
    for ( j = 0; j < N; j++ )
      {
        A[i][j] = 1.0;
        B[i][j] = 1.0;
        C[i][j] = 0.0;
      }
  
  // matrix multiplication
  int ibl = 100;
  int ib, jb, kb;
  for (ib=0; ib<N; ib+=ibl)
    for (jb=0; jb<N; jb+=ibl)
      for (kb=0; kb<N; kb+=ibl)
        for (i=ib; i<ib+ibl; i++)
          for (j=jb; j<jb+ibl; j++)
            for (k=kb; k<kb+ibl; k++)
              C[i][j] += A[i][k] * B[k][j];
  
  printf ( "%f\n", C[0][0] );
  
  return 0;
}


実行
最適化前、最適化後それぞれについて、実行にかかる時間は以下のようになりました。
$ gcc -Wl,-stack_size,1000000000 -O2 -o code1 code1.c
$ time ./code1 > /dev/null

real    0m7.595s
user    0m7.494s
sys     0m0.035s

$ gcc -Wl,-stack_size,1000000000 -O2 -o code2 code2.c
$ time ./code2 > /dev/null

real    0m2.509s
user    0m2.462s
sys     0m0.030s
※大きな配列をスタック上に確保するために、ldにオプションを渡してスタックサイズを大きくしています

グラフにすると以下のようになります。



キャッシュブロック化によって、約3倍の性能向上が得られました。


ブロックサイズによる性能比較
次に、ブロック化のサイズによって性能がどのように変わるかを調べてみます。ブロック化のサイズを100、200、250、500に変えて、実行時間を計ると、以下のようになりました。
100 : 2.311s
200 : 2.390s
250 : 2.509s
500 : 6.588s
※ブロック化のサイズの単位は、配列の要素数です。バイト数に直すと、double型の配列なので8を掛けたものになります

グラフにすると、以下のようになります。


ブロック化のサイズが500になると、急激に性能が悪化しています。このことから、ブロック化のサイズが大きくなりキャッシュにデータが収まらなくなった様子が分かります。

500×500のブロックのデータ量が2MBなので、このプログラム以外が動いていることも勘案すると、使っている環境のCPUの持つキャッシュメモリサイズと一致した結果といえます。


--

0 件のコメント: