512x512の行列の転置が、513x513の行列の転置よりもはるかに遅いのはなぜですか?


218

異なるサイズの正方行列でいくつかの実験を行った後、パターンが思い付きました。常に、サイズの行列の転置は、サイズの行列の2^n転置よりも遅くなります2^n+1。の値が小さい場合n、違いは重要ではありません。

ただし、値が512を超えると大きな違いが生じます。(少なくとも私にとっては)

免責事項:要素の二重交換のため、関数が実際に行列を転置しないことは知っていますが、違いはありません。

コードに従います:

#define SAMPLES 1000
#define MATSIZE 512

#include <time.h>
#include <iostream>
int mat[MATSIZE][MATSIZE];

void transpose()
{
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
   {
       int aux = mat[i][j];
       mat[i][j] = mat[j][i];
       mat[j][i] = aux;
   }
}

int main()
{
   //initialize matrix
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
       mat[i][j] = i+j;

   int t = clock();
   for ( int i = 0 ; i < SAMPLES ; i++ )
       transpose();
   int elapsed = clock() - t;

   std::cout << "Average for a matrix of " << MATSIZE << ": " << elapsed / SAMPLES;
}

変更MATSIZEすると、サイズを変更できます(そうです!)。私はideoneに2つのバージョンを投稿しました:

私の環境(MSVS 2010、完全な最適化)では、違いは似ています:

  • サイズ512-平均2.19ミリ秒
  • サイズ513-平均0.57ミリ秒

なぜこうなった?


9
あなたのコードは私には不親切なキャッシュに見えます。
CodesInChaos

7
これはかなりこの質問と同じ問題です:stackoverflow.com/questions/7905760/...
Mysticial

@CodesInChaos、詳しく説明しますか?(または他の誰か。)
corazza

@Bane受け入れられた回答を読んでみませんか?
CodesInChaos 2012

4
@nzomkxia最適化なしで何かを測定するのは、少し無意味です。最適化を無効にすると、生成されたコードは他のボトルネックを隠す不要なゴミで散らかされます。(メモリなど)
Mysticial

回答:


197

説明は、C ++でのソフトウェアの最適化におけるAgner Fogによるものです。からのものであり、データへのアクセス方法とキャッシュへの格納方法にされます。

用語と詳細情報については、キャッシングに関するWikiエントリを参照してください。ここで絞り込みます。

キャッシュは、セットラインで構成されています。一度に使用されるセットは1つだけで、その中のどのラインも使用できます。ラインがミラーリングできるメモリの数は、ラインの数にキャッシュサイズを与えます。

特定のメモリアドレスについて、次の式でミラーリングするセットを計算できます。

set = ( address / lineSize ) % numberOfsets

この種類の式は、各メモリアドレスが読み取られる可能性が高いため、理想的にはセット全体に均一な分布を与えます(理想的には)。

オーバーラップが発生する可能性があることは明らかです。キャッシュミスの場合、メモリはキャッシュで読み取られ、古い値が置き換えられます。各セットには多数の行があり、そのうち最も長く使用されていない行が新しく読み取られたメモリで上書きされることに注意してください。

Agnerの例に少し従うようにします。

各セットに4行があり、それぞれが64バイトを保持するとします。最初0x2710に、セットに入るアドレスを読み取ろうとします28。そして、我々はまた、アドレスを読み込もう0x2F000x37000x3F000x4700。これらはすべて同じセットに属しています。を読む前0x4700に、セット内のすべての行が占有されていたはずです。そのメモリを読み取ると、セット内の既存の行(最初は保持していた行)が削除され0x2710ます。問題は、(この例では)0x800離れているアドレスを読み取るという事実にあります。これは重要なストライドです(この例でも)。

クリティカルストライドも計算できます。

criticalStride = numberOfSets * lineSize

変数の間隔が空いている、criticalStrideまたは複数の変数が同じキャッシュラインで競合しています。

これは理論の部分です。次に、説明(また、アグナー、私は間違いを避けるためにそれを注意深くフォローしています):

8xキャッシュ、1セットあたり4行*行サイズ64バイトの64x64の行列(効果はキャッシュによって異なることに注意)を想定します。各行は、マトリックス(64ビットint)内の8つの要素を保持できます。

クリティカルストライドは2048バイトです。これは、マトリックスの4行に対応します(メモリ内で連続しています)。

行28を処理していると仮定します。この行の要素を取得して、列28の要素と交換しようとしています。行の最初の8つの要素はキャッシュラインを構成しますが、8つの要素に分かれます列28のキャッシュ行。クリティカルストライドは4行離れている(列の4つの連続する要素)ことに注意してください。

列の要素16に到達すると(1セットあたり4キャッシュライン&4行離れている=問題)、ex-0要素がキャッシュから削除されます。列の最後に到達すると、以前のすべてのキャッシュラインが失われ、次の要素へのアクセス時にリロードが必要になります(ライン全体が上書きされます)。

クリティカルストライドの倍数ではないサイズを使用すると、垂直方向のクリティカルストライドで分離されている要素を処理しなくなるため、キャッシュのリロードの数が大幅に削減されるため、災害のこの完璧なシナリオが台無しになります。

別の免責事項 -私は説明に頭を悩ませて、それを釘付けにしたいと思っていますが、私は間違っているかもしれません。とにかく、Mysticialからの返信(または確認)を待っています。:)


ああ、次回。ラウンジから直接pingしてください。SOで名前のすべてのインスタンスを見つけることはできません。:)私はこれを定期的な電子メール通知でのみ見ました。
ミスティシャル2012

私の友人の@Mysticial @Luchianグリゴール一つは、彼のことを私に語っIntel core i3たPCが実行するUbuntu 11.04 i386とほぼ同等の性能を示してGCC 4.6ので、私のコンピュータで同じである.ANDをIntel Core 2 Duo持つmingwのgcc4.4で実行されている、windows 7(32)ときに大きな違いを示してい.IT私はこのセグメントを、gcc 4.6を実行している少し古いPC intel centrinoでコンパイルしています。ubuntu 12.04 i386
Hongxu Chen 2012

また、アドレスが4096の倍数異なるメモリアクセスは、Intel SnBファミリCPUに誤って依存していることにも注意してください。(つまり、ページ内の同じオフセット)。これにより、一部の操作がストアである場合、特にスループットが低下する可能性があります。ロードとストアの混合。
Peter Cordes

which goes in set 24代わりに「セット28」という意味ですか?そして、あなたは32セットを想定していますか?
ルスラン

正解です。それは28です。:)リンクされた論文も再確認しました。元の説明では、9.2キャッシュ構成に移動できます
Luchian Grigore

78

Luchianがこの動作が発生する理由を説明しますが、この問題の1つの可能な解決策を示すと同時に、キャッシュの気付かないアルゴリズムについて少し説明するのは良い考えだと思いました。

あなたのアルゴリズムは基本的に:

for (int i = 0; i < N; i++) 
   for (int j = 0; j < N; j++) 
        A[j][i] = A[i][j];

これは最近のCPUにとっては恐ろしいことです。1つの解決策は、キャッシュシステムの詳細を把握し、アルゴリズムを調整してこれらの問題を回避することです。あなたがそれらの詳細を知っている限り、うまく動作します。特にポータブルではありません。

それ以上のことはできますか?はい、できます。この問題への一般的なアプローチは、名前が示すように特定のキャッシュサイズに依存しないようにするキャッシュ忘却アルゴリズムです[1]

ソリューションは次のようになります。

void recursiveTranspose(int i0, int i1, int j0, int j1) {
    int di = i1 - i0, dj = j1 - j0;
    const int LEAFSIZE = 32; // well ok caching still affects this one here
    if (di >= dj && di > LEAFSIZE) {
        int im = (i0 + i1) / 2;
        recursiveTranspose(i0, im, j0, j1);
        recursiveTranspose(im, i1, j0, j1);
    } else if (dj > LEAFSIZE) {
        int jm = (j0 + j1) / 2;
        recursiveTranspose(i0, i1, j0, jm);
        recursiveTranspose(i0, i1, jm, j1);
    } else {
    for (int i = i0; i < i1; i++ )
        for (int j = j0; j < j1; j++ )
            mat[j][i] = mat[i][j];
    }
}

少し複雑ですが、短いテストでは、VS2010 x64リリースの古代のe8400で非常に興味深い何かが示されています。 MATSIZE 8192

int main() {
    LARGE_INTEGER start, end, freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&start);
    recursiveTranspose(0, MATSIZE, 0, MATSIZE);
    QueryPerformanceCounter(&end);
    printf("recursive: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));

    QueryPerformanceCounter(&start);
    transpose();
    QueryPerformanceCounter(&end);
    printf("iterative: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
    return 0;
}

results: 
recursive: 480.58ms
iterative: 3678.46ms

編集:サイズの影響について:ある程度は顕著ですが、それほど顕著ではありません。これは、1に再帰する代わりに反復解をリーフノードとして使用しているためです(再帰アルゴリズムの通常の最適化)。LEAFSIZE = 1を設定した場合、キャッシュは私に影響を与えません[ 8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms-これはエラーのマージンの範囲内で、変動は100ミリ秒の領域にあります。この「ベンチマーク」は、完全に正確な値が必要な場合、私があまりに満足できるものではありません])

[1]この資料の出典:さて、もしあなたがLeisersonと共同でこれについて働いた誰かから講義を得ることができないなら..私は彼らの論文が良い出発点だと思います。これらのアルゴリズムについては、まだほとんど説明されていません。CLRにはそれらの脚注が1つあります。それでも、それは人々を驚かせる素晴らしい方法です。


編集(注:この回答を投稿したのは私ではありません。これを追加したいだけです):
上記のコードの完全なC ++バージョンは次のとおりです。

template<class InIt, class OutIt>
void transpose(InIt const input, OutIt const output,
    size_t const rows, size_t const columns,
    size_t const r1 = 0, size_t const c1 = 0,
    size_t r2 = ~(size_t) 0, size_t c2 = ~(size_t) 0,
    size_t const leaf = 0x20)
{
    if (!~c2) { c2 = columns - c1; }
    if (!~r2) { r2 = rows - r1; }
    size_t const di = r2 - r1, dj = c2 - c1;
    if (di >= dj && di > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, (r1 + r2) / 2, c2);
        transpose(input, output, rows, columns, (r1 + r2) / 2, c1, r2, c2);
    }
    else if (dj > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, r2, (c1 + c2) / 2);
        transpose(input, output, rows, columns, r1, (c1 + c2) / 2, r2, c2);
    }
    else
    {
        for (ptrdiff_t i1 = (ptrdiff_t) r1, i2 = (ptrdiff_t) (i1 * columns);
            i1 < (ptrdiff_t) r2; ++i1, i2 += (ptrdiff_t) columns)
        {
            for (ptrdiff_t j1 = (ptrdiff_t) c1, j2 = (ptrdiff_t) (j1 * rows);
                j1 < (ptrdiff_t) c2; ++j1, j2 += (ptrdiff_t) rows)
            {
                output[j2 + i1] = input[i2 + j1];
            }
        }
    }
}

2
これは、再帰的および反復的ではなく、異なるサイズの行列間の時間を比較する場合に関連します。指定されたサイズの行列で再帰的解法を試してください。
ルチアングリゴー

@Luchian なぜ彼がその振る舞いを見ているのすでに説明したので、この問題の一般的な解決策を1つ紹介するのは非常に興味深いと思いました。
Voo

なぜなら、より大きな行列がより高速なアルゴリズムを探すのではなく、なぜ処理にかかる時間がより短いのかと疑問に思っているからです...
Luchian Grigore

@Luchian 16383と16384の違いは、ここでは28ミリ秒対27ミリ秒、つまり約3.5%です-それほど重要ではありません。そして、それがあったとしたら私は驚かれることでしょう。
Voo

3
が何をするかを説明するのrecursiveTransposeは興味深いかもしれません。つまり、(次元の)小さなタイルを操作してもキャッシュがいっぱいにならないということLEAFSIZE x LEAFSIZEです。
Matthieu M.

60

Luchian Grigoreの回答の説明の例として、64x64および65x65マトリックスの2つのケースでのマトリックスキャッシュの存在を次に示します(数値の詳細については、上記のリンクを参照してください)。

以下のアニメーションの色は以下を意味します:

  • 白い –キャッシュにない、
  • 薄緑 –キャッシュ内
  • 明るい緑色 –キャッシュヒット
  • オレンジ – RAMから読み取るだけで、
  • 赤 –キャッシュミス。

64x64の場合:

64x64マトリックスのキャッシュプレゼンスアニメーション

新しい行へのほとんどすべてのアクセスがキャッシュミスになることに注意してください。そして、それが通常のケースである65x65マトリックスをどのように探すかです:

65x65マトリックスのキャッシュプレゼンスアニメーション

ここでは、最初のウォームアップ後のアクセスのほとんどがキャッシュヒットであることがわかります。これは、CPUキャッシュの一般的な動作方法です。


上記のアニメーションのフレームを生成したコードは、こちらにあります


垂直スキャンキャッシュヒットが最初のケースでは保存されないのに、2番目のケースでは保存されるのはなぜですか?どちらの例でも、ほとんどのブロックで特定のブロックに1回だけアクセスするようです。
Josiah Yoder

@LuchianGrigoreの回答から、列のすべての行が同じセットに属していることが原因であることがわかります。
Josiah Yoder

はい、素晴らしいイラストです。彼らは同じ速度であることがわかります。しかし、実際にはそうではありませんね。
ケララカ2018

@kelalakaはい、アニメーションFPSは同じです。スローダウンはシミュレートしませんでした。ここでは色だけが重要です。
ルスラン

異なるキャッシュセットを示す2つの静的イメージがあると興味深いでしょう。
Josiah Yoder
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.