web-dev-qa-db-ja.com

memcpy()の速度が4KBごとに劇的に低下するのはなぜですか?

I * 4KBで速度が劇的に低下することに気づいたmemcpy()の速度をテストしました。結果は次のとおりです。Y軸は速度(MB /秒)で、X軸はmemcpy()のバッファーのサイズで、1KBから2MBに増加します。サブ図2およびサブ図3は、1KB-150KBおよび1KB-32KBの部分の詳細を示しています。

環境:

CPU:Intel(R)Xeon(R)CPU E5620 @ 2.40GHz

OS:2.6.35-22-generic#33-Ubuntu

GCCコンパイラフラグ:-O3 -msse4 -DINTEL_SSE4 -Wall -std = c99

Graphs of memcpy speed showing troughs every 4k

キャッシュに関連しているに違いないと思いますが、次のキャッシュにやさしいケースから理由を見つけることはできません。

これらの2つのケースのパフォーマンスの低下は、分散したバイトをキャッシュに読み込んでキャッシュラインの残りのスペースを無駄にする不親切なループによって引き起こされるためです。

ここに私のコードがあります:

void memcpy_speed(unsigned long buf_size, unsigned long iters){
    struct timeval start,  end;
    unsigned char * pbuff_1;
    unsigned char * pbuff_2;

    pbuff_1 = malloc(buf_size);
    pbuff_2 = malloc(buf_size);

    gettimeofday(&start, NULL);
    for(int i = 0; i < iters; ++i){
        memcpy(pbuff_2, pbuff_1, buf_size);
    }   
    gettimeofday(&end, NULL);
    printf("%5.3f\n", ((buf_size*iters)/(1.024*1.024))/((end.tv_sec - \
    start.tv_sec)*1000*1000+(end.tv_usec - start.tv_usec)));
    free(pbuff_1);
    free(pbuff_2);
}

更新

@ usr、@ ChrisW、および@Leeorからの提案を考慮して、テストをより正確に再編集し、下のグラフに結果を示します。バッファサイズは26KBから38KBで、64B(26KB、26KB + 64B、26KB + 128B、......、38KB)おきにテストしました。各テストは、約0.15秒で100,000回ループします。興味深いのは、ドロップが4KB境界で正確に発生するだけでなく、4 * i + 2 KBでも発生することで、振幅の低下ははるかに小さくなります。

More graphs showing performance drops

PS

@Leeorは、ドロップを埋める方法を提供し、pbuff_1pbuff_2の間に2KBのダミーバッファーを追加しました。それは機能しますが、私は、リーアの説明についてはわかりません。

enter image description here

55
foool

メモリは通常4kページで構成されます(ただし、より大きなサイズもサポートされています)。プログラムが認識する仮想アドレス空間は連続している場合がありますが、物理メモリでは必ずしもそうではありません。 (ページマップで)仮想アドレスから物理アドレスへのマッピングを維持するOSは通常、物理ページも一緒に保持しようとしますが、それは常に可能とは限らず、破損する可能性があります(特に、頻繁にスワップされる可能性がある長時間の使用の場合) )。

メモリストリームが4kのページ境界を超えると、CPUは停止して新しい翻訳を取得する必要があります-すでにページが表示されている場合、TLBにキャッシュされ、アクセスが最速になるように最適化されますが、最初のアクセスである(または保持するTLBのページが多すぎる場合)、CPUはメモリアクセスをストールし、ページマップエントリを介してページウォークを開始する必要があります-各レベルは実際には比較的長いですメモリ自体が読み取ります(仮想マシンでは、各レベルでホスト上で完全なページウォークが必要になるため、さらに長くなります)。

Memcpy関数には別の問題がある可能性があります-最初にメモリを割り当てると、OSはページをページマップに構築するだけですが、内部最適化によりアクセスされておらず、変更されていないとしてマークします。最初のアクセスはページウォークを呼び出すだけでなく、OSにページが使用されること(およびターゲットバッファーページに格納されること)をOSに伝えることもできます。

このノイズを除去するには、バッファを1回割り当て、コピーを数回繰り返し、償却時間を計算します。一方、それは「ウォーム」パフォーマンスを提供します(つまり、キャッシュをウォームアップした後)ので、グラフにキャッシュサイズが反映されます。ページングレイテンシに悩まされずに「コールド」効​​果を取得したい場合は、反復間でキャッシュをフラッシュすることをお勧めします(時間をとらないように注意してください)

編集

質問を読み直せば、あなたは正しい測定をしているようです。私の説明の問題は、4k*iの後に徐々に増加するはずだということです。そのようなドロップごとにペナルティを再度支払うが、その後4kまでフリーライドを楽しむ必要があるからです。なぜそのような「スパイク」があるのか​​は説明されておらず、その後は速度が通常に戻ります。

あなたはあなたの質問にリンクされているクリティカルストライドの問題と同様の問題に直面していると思います-バッファサイズがニースラウンド4kの場合、両方のバッファが整列しますキャッシュ内の同じセットに移動し、互いにスラッシュします。 L1は32kなので、最初は問題に見えませんが、データL1に8つの方法があると仮定すると、実際には同じセットへの4kのラップアラウンドであり、2 * 4kブロックとまったく同じアライメントです(割り当てが連続して行われたと仮定)、それらは同じセットでオーバーラップします。 LRUが期待どおりに機能せず、競合が発生し続けるだけで十分です。

これを確認するには、pbuff_1とpbuff_2の間にダミーバッファーをmallocして、2kに大きくして、アライメントが崩れることを期待します。

EDIT2:

わかりました。これでうまくいくので、少し詳しく説明します。 0x1000-0x1fff0x2000-0x2fffの範囲で2つの4k配列を割り当てたとします。 L1のセット0には0x1000と0x2000の行が含まれ、セット1には0x1040と0x2040の行が含まれます。これらのサイズでは、スラッシングの問題はまだありません。キャッシュの結合性をオーバーフローさせることなく、すべて共存できます。ただし、反復を実行するたびに、同じセットにアクセスするロードとストアがあります。これにより、ハードウェアで競合が発生する可能性があります。さらに悪いことに、1行をコピーするには複数の反復が必要になります。つまり、8つのロードと8つのストアの輻輳が発生します(ベクトル化してもまだたくさんあります)。そこに隠れている衝突の束があることを確認してください。

また、 Intel最適化ガイド にはそれについて具体的に言うべきことがあります(3.6.8.2を参照):

4 KBのメモリエイリアシングは、コードが2つの異なるメモリ位置にアクセスし、その間に4 KBのオフセットがある場合に発生します。 4 KBのエイリアシングの状況は、コピー元バッファーとコピー先バッファーのアドレスが一定のオフセットを維持し、一定のオフセットが1回の反復から次のバイト増分の倍数になるメモリコピールーチンで明らかになります。

...

ロードを続行するには、ストアが廃止されるまで待つ必要があります。たとえば、オフセット16では、次の反復の負荷は現在の反復ストアをエイリアス化した4Kバイトであるため、ループはストア操作が完了するまで待機し、ループ全体をシリアル化する必要があります。待機に必要な時間は、96のオフセットが問題を解決するまでオフセットが大きくなると減少します(同じアドレスのロード時まで保留中のストアがないため)。

32
Leeor

理由は次のとおりです。

  • ブロックサイズが4KB倍の場合、mallocはO/Sから新しいページを割り当てます。
  • ブロックサイズが4KB倍数でない場合、mallocは(すでに割り当てられている)ヒープから範囲を割り当てます。
  • O/Sからページが割り当てられると、ページは「コールド」になります。初めてページに触れるのは非常に高価です。

私の推測では、最初のmemcpyの前に単一のgettimeofdayを実行すると、割り当てられたメモリが「ウォーム」され、この問題は発生しません。最初のmemcpyを実行する代わりに、割り当てられた各4KBページに1バイトを書き込むだけでも、ページを事前にウォームアップするのに十分かもしれません。

通常、あなたのようなパフォーマンステストが必要な場合は、次のようにコーディングします。

// Run in once to pre-warm the cache
runTest();
// Repeat 
startTimer();
for (int i = count; i; --i)
  runTest();
stopTimer();

// use a larger count if the duration is less than a few seconds
// repeat test 3 times to ensure that results are consistent
2
ChrisW

あなたは何度もループしているので、マップされていないページに関する議論は無関係だと思います。私の意見では、ハードウェアプリフェッチャーが(潜在的に不必要な)ページフォールトを引き起こさないためにページ境界を越えようとしないという影響です。

0
virco