あなたが見ているのは、コンピュータのメモリ階層における参照の局所性の影響だと思います。
通常、コンピュータのメモリは、パフォーマンス特性が異なるさまざまなタイプに分類されます(これはメモリ階層と呼ばれることがよくあります)。最速のメモリはプロセッサのレジスタにあり、(通常は)単一のクロックサイクルでアクセスおよび読み取りが可能です。ただし、通常、これらのレジスタはほんの一握りです(通常は1KB以下)。一方、コンピュータのメインメモリは巨大ですが(たとえば8GB)、アクセスがはるかに遅くなります。パフォーマンスを向上させるために、コンピューターは通常、いくつかのレベルのキャッシュを持つように物理的に構築されていますプロセッサとメインメモリの間。これらのキャッシュはレジスタよりも低速ですが、メインメモリよりもはるかに高速です。したがって、キャッシュ内で何かを検索するメモリアクセスを実行すると、メインメモリに移動する必要がある場合よりもはるかに高速になる傾向があります(通常、5〜25倍)。もっと早く)。メモリにアクセスするとき、プロセッサは最初にメモリキャッシュでその値をチェックしてから、メインメモリに戻って値を読み込みます。キャッシュ内の値に一貫してアクセスすると、スキップする場合よりもパフォーマンスが大幅に向上します。メモリ、ランダムに値にアクセスします。
ほとんどのプログラムは、メモリ内の1バイトがメモリに読み込まれると、後でそのメモリ領域の周囲から複数の異なる値を読み取るように記述されています。したがって、これらのキャッシュは通常、メモリから単一の値を読み取るときに、その単一の値の周囲の値のメモリブロック(通常は1KBから1MBの間)もキャッシュに取り込まれるように設計されています。そうすれば、プログラムが近くの値を読み取った場合、それらはすでにキャッシュにあり、メインメモリに移動する必要はありません。
最後にもう1つ、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つのキャッシュミスが発生する可能性があります。
しかし、この2番目のバージョンを検討してください。
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];
ここで、j
反復ごとに増加しているので、最も内側のステートメントで発生する可能性のあるキャッシュミスの数について考えてみましょう。値は行優先の順序であるため、の値はc[i][j]
キャッシュ内にある可能性がありc[i][j]
ます。これは、前の反復からの値もキャッシュされ、読み取る準備ができているためです。同様に、b[k][j]
はおそらくキャッシュされ、変更されi
てk
いないため、チャンスもa[i][k]
キャッシュされます。これは、内部ループの各反復で、キャッシュミスが発生しない可能性が高いことを意味します。
全体として、これは、コードの2番目のバージョンでは、ループの各反復でキャッシュミスが発生する可能性が低いことを意味しますが、最初のバージョンではほぼ確実にキャッシュミスが発生します。その結果、これまで見てきたように、2番目のループは最初のループよりも高速になる可能性があります。
興味深いことに、多くのコンパイラは、コードの2番目のバージョンが最初のバージョンよりも高速であることを検出するためのプロトタイプサポートを開始しています。並列処理を最大化するためにコードを自動的に書き直そうとする人もいます。Purple Dragon Bookのコピーをお持ちの場合は、第11章でこれらのコンパイラの動作について説明します。
さらに、より複雑なループを使用して、このループのパフォーマンスをさらに最適化できます。たとえば、ブロッキングと呼ばれる手法を使用すると、配列をキャッシュに保持できるサブ領域に分割し、これらのブロックで複数の操作を使用して全体的な結果を計算することで、パフォーマンスを大幅に向上させることができます。
お役に立てれば!