あなたが見ているのは、locality of referenceがコンピュータのメモリ階層に与える影響です。
典型的には、コンピュータのメモリは、異なる性能特性を有する異なるタイプに分離される(これはしばしばmemory hierarchyと呼ばれます)。最も速いメモリはプロセッサのレジスタにあり、通常は1クロックサイクルでアクセスして読み取ることができます。しかし、通常これらのレジスタはほんの一握りです(通常は1KB以下)。一方、コンピュータのメインメモリは、巨大(例えば8GB)ですが、アクセスするのがはるかに遅いです。パフォーマンスを向上させるために、コンピュータは通常、プロセッサとメインメモリの間にseveral levels of cachesを持つように物理的に構築されています。これらのキャッシュはレジスタよりも低速ですがメインメモリよりもはるかに高速です。したがって、キャッシュ内に何か見えるメモリアクセスを行うと、メインメモリに移動する必要がある場合よりも高速になる傾向があります(通常、5〜もっと早く)。メモリにアクセスするとき、プロセッサは最初にその値のメモリキャッシュをチェックしてからメインメモリに戻り、値を読み込みます。キャッシュ内の値に一貫してアクセスすると、スキップした場合よりもパフォーマンスが向上しますランダムに値にアクセスします。
ほとんどのプログラムは、メモリの1バイトがメモリに読み込まれると、後でプログラムはそのメモリ領域の周りから複数の異なる値を読み込みます。その結果、これらのキャッシュは、通常、メモリから単一の値を読み取るときに、その単一の値を中心とする値のブロック(通常は1〜1MBの間)がキャッシュに引き込まれるように設計されています。そうすれば、あなたのプログラムが近くの値を読み込んでも、すでにキャッシュに入っているので、メインメモリに行く必要はありません。
最後に、C/C++では配列が行優先順序で格納されています。つまり、行列の1行の値はすべて隣り合って格納されます。したがって、メモリ内の配列は最初の行、次に2番目の行、3番目の行などのように見えます。
これを考えると、コードを見てみましょう。最初のバージョンは次のようになります。
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
c[i][j] = c[i][j] + a[i][k]*b[k][j];
次に、その最も内側のコード行を見てみましょう。各反復において、kの値は増加するように変化している。つまり、最も内側のループを実行すると、ループの各反復では、b[k][j]
の値をロードするときにキャッシュミスが発生する可能性があります。その理由は、行列が行優先順序で格納されているため、kをインクリメントするたびに、行列の行全体をスキップしていて、キャッシュに入れた値をはるかに超えている可能性があります。ただし、c[i][j]
(i
とj
が同じであるため)を参照すると、値が行優先であるため、a[i][k]
の値が前の値からキャッシュされるため、おそらくa[i][k]
が失われることはありませんこの反復で読み取られるa[i][k]
の値は、隣接するメモリ位置からのものです。その結果、最も内側のループの繰り返しごとに、1つのキャッシュミスが発生する可能性が高くなります。
しかし、この第二のバージョンを検討:あなたは、各反復でj
が増加していることから、今
for (int i = 0; i < n; i++)
for (int k = 0; k < n; k++)
for (int j = 0; j < n; j++)
c[i][j] = c[i][j] + a[i][k]*b[k][j];
を、あなたはおそらく、最も内側の文にありますミスどのように多くのキャッシュについて考えてみましょう。値は行優先順序であるため、c[i][j]
の値はキャッシュ内にある可能性があります。これは、前回の反復でのc[i][j]
の値も同様にキャッシュされ、読み込み可能なためです。同様に、b[k][j]
がおそらくキャッシュされており、i
とk
は変更されていないため、a[i][k]
もキャッシュされます。これは、内部ループの各繰り返しで、キャッシュミスを起こさない可能性が高いことを意味します。
全体的に言えば、これは、コードの第2バージョンがループの各繰り返しでキャッシュミスを起こす可能性は低いことを意味しますが、最初のバージョンはほぼ確実です。その結果、2番目のループは最初に見たより高速になりそうです。
興味深いことに、多くのコンパイラでは、コードの2番目のバージョンが最初のバージョンより高速であることを検出するためのプロトタイプサポートが開始されています。並列処理を最大限にするためにコードを自動的に書き直そうとする人もいます。 Purple Dragon Bookのコピーがある場合、第11章ではこれらのコンパイラの動作について説明します。
さらに、複雑なループを使用してこのループのパフォーマンスをさらに最適化できます。たとえば、blockingという手法を使用して、アレイをキャッシュに長く保持できるサブ領域に分割し、次にこれらのブロックで複数の演算を使用して全体の結果を計算することでパフォーマンスを大幅に向上させることができます。
希望すると便利です。
+1本当に素晴らしい説明です!キャッシュデバッグに関する@Kerrek SBの提案には、技術的な詳細が追加されています。 – rbaleksandar