memmpyよりmemmoveが速いのはなぜですか?


89

memmove(3)で時間の50%を費やすアプリケーションのパフォーマンスホットスポットを調査しています。アプリケーションは、何百万もの4バイト整数をソートされた配列に挿入し、memmoveを使用してデータを「右に」シフトし、挿入された値のためのスペースを作ります。

メモリのコピーは非常に高速であると私は予想しており、memoveに多くの時間が費やされていることに驚きました。しかし、それからmemmoveは、メモリの大きなページをコピーするのではなく、タイトなループで実装する必要がある重複領域を移動するため、遅いと考えました。memcpyとmemmoveの間にパフォーマンスの違いがあるかどうかを確認するために、小さなマイクロベンチマークを書きました。memcpyが勝つことを期待しています。

私は2つのマシン(コアi5、コアi7)でベンチマークを実行し、memmpyがmemcpyよりも実際に高速であることを確認しました。古いコアi7では、2倍近くも高速です!今、私は説明を探しています。

これが私のベンチマークです。memcpyで100 mbをコピーし、memmoveで約100 mb移動します。ソースと宛先が重複しています。発信元と宛先のさまざまな「距離」が試されます。各テストは10回実行され、平均時間が印刷されます。

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

Core i5(Linux 3.5.0-54-generic#81〜precise1-Ubuntu SMP x86_64 GNU / Linux、gccは4.6.3(Ubuntu / Linaro 4.6.3-1ubuntu5)での結果です。角かっこ内の数値はソースと宛先の間の距離(ギャップサイズ):

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

MemmoveはSSEに最適化されたアセンブラーコードとして実装され、後ろから前へコピーします。ハードウェアプリフェッチを使用してデータをキャッシュにロードし、128バイトをXMMレジスタにコピーして、宛先に格納します。

memcpy-ssse3-back.S、1650行以降)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

memmpyがmemcpyより速いのはなぜですか?memcpyがメモリページをコピーすることを期待します。これはループよりもはるかに高速です。最悪の場合、memcpyはmemmoveと同じくらい高速になると予想します。

PS:私のコードではmemmoveをmemcpyに置き換えることができないことを知っています。コードサンプルにはCとC ++が混在しています。この質問は、実際には学術目的のためだけのものです。

アップデート1

さまざまな答えに基づいて、テストのバリエーションをいくつか実行しました。

  1. memcpyを2回実行すると、2回目の実行は最初の実行よりも高速になります。
  2. memcpy(memset(b2, 0, BUFFERSIZE...))の宛先バッファに「触れる」と、memcpy の最初の実行も高速になります。
  3. memcpyはmemmoveよりも少し遅いです。

結果は次のとおりです。

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

私の結論:@Oliver Charlesworthからのコメントに基づいて、memcpy宛先バッファーが初めてアクセスされるとすぐに、オペレーティングシステムは物理メモリをコミットする必要があります(誰かがこれを「証明」する方法を知っている場合は、答えを追加してください! )。さらに、@ Mats Peterssonが言ったように、memmoveはmemcpyよりもキャッシュフレンドリです。

すばらしい回答とコメントをありがとう!


1
memmoveコードを確認しましたが、memcpyコードも確認しましたか?
Oliver Charlesworth、

8
私の期待は、メモリのコピーが非常に高速であることでした-メモリがL1キャッシュにある場合のみ。データがキャッシュに収まらない場合、コピーのパフォーマンスが低下します。
Maxim Egorushkin、2015

1
ところで、コピーしたブランチは1つだけですmemmove。このブランチは、ソースが宛先と重なり、宛先がより低いアドレスにある場合、移動を処理できません。
Maxim Egorushkin、2015

2
Linuxマシンにアクセスする時間がなかったので、この理論をまだテストすることはできません。しかし、もう1つの考えられる説明は、過大評価です。あなたのmemcpyループはの内容は今回が初めてでb2アクセスされ、これOSは、それが行くようにそれのために物理メモリをコミットする必要があります。
オリバーチャールズワース

2
PS:これがボトルネックである場合、私はアプローチを再考します。値をリストまたはツリー構造(バイナリツリーなど)に入れて、最後に配列に読み込む方法はどうでしょうか。このようなアプローチのノードは、プール割り当ての優れた候補になります。まとめてリリースされる最後まで追加されます。最初に必要な数がわかっている場合は特にそうです。ブーストライブラリにはプールアロケーターがあります。
Persixty

回答:


56

あなたのmemmoveごながらの通話は、2 128バイトで一緒にメモリをシャッフルしているmemcpy送信元と送信先が完全に異なっています。どういうわけか、パフォーマンスの違いを占めていますこと:あなたが同じ場所にコピーした場合、あなたが表示されますmemcpy可能性がsmidge速い端をアップし、例えば上ideone.com

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

その中にはほとんど何もかかわらず-すでにメモリページにフォルトに戻って書き込みが持っているという証拠あまり影響を、我々は確かに時間の半分を見ていない...しかし、それはなって何も間違ってありますことを示していmemcpyたリンゴを比較すると、不必要に遅くします-りんご。


私のバッファはキャッシュよりもはるかに大きいので、CPUキャッシュが違いを引き起こしていないと思いました。
cruppstahl、2015

2
しかし、それぞれに必要なメインメモリアクセスの総数は同じですよね?(つまり、100MBの読み取りと100MBの書き込み)。キャッシュパターンはそれを回避しません。したがって、一方が他方よりも遅くなる唯一の方法は、いくつかのデータを複数回メモリから読み書きする必要がある場合です。
Oliver Charlesworth、

2
@Tony D-私の結論は、私より頭のいい人に聞くことでした;)
cruppstahl

1
また、同じ場所にコピーするとどうなりますmemcpyか?
オリバーチャールズワース

1
@OliverCharlesworth:最初のテスト実行では常に大きなヒットが発生しますが、2つのmemcpyテストを実行します:memcpy 0.0688002 0.0583162 | MEMMOVE 0.0577443 0.05862 0.0601029 ...参照ideone.com/8EEAcA
トニー・デルロイ

24

を使用memcpyしている場合、書き込みはキャッシュに移動する必要があります。memmove小さなステップをコピーするときに使用する場合、コピーするメモリは既にキャッシュにあります(2、4、16、または128バイト「読み取られた」ため)。memmove宛先が数メガバイト(> 4 *キャッシュサイズ)である場所で試してみてください。同様の結果が得られると思います(ただし、テストする必要はありません)。

大規模なメモリ操作を行う場合、ALLはキャッシュのメンテナンスに関するものです。


+1あなたが言及した理由から、後方ループmemmoveはmemcpyよりもキャッシュに優しいと思います。ただし、me​​mcpyテストを2回実行すると、2回目の実行がmemmoveと同じ速さであることがわかりました。どうして?バッファーが非常に大きいため、memcpyの2回目の実行は、最初の実行と同じくらい(キャッシュの点で)非効率的である必要があります。したがって、パフォーマンスの低下を引き起こす追加の要素があるようです。
cruppstahl 2015

3
memcpyTLBが事前に入力されているため、適切な状況では、1秒は著しく速くなります。また、2番目memcpyは、「取り除く」必要があるかもしれないもののキャッシュを空にする必要はありません(ダーティなキャッシュラインは、多くの点でパフォーマンスにとって「悪い」ものです。しかし、確かに言うと、 「perf」のようなものを実行し、キャッシュミス、TLBミスなどのサンプルを実行します
Mats Petersson

15

歴史的に、memmoveとmemcopyは同じ機能です。彼らは同じように働き、同じ実装をしました。その後、重複した領域を特定の方法で処理するためにmemcopyを定義する必要がない(そして、頻繁に定義されなかった)ことがわかりました。

最終的に、memmoveは、パフォーマンスに影響を与える場合でも、重複する領域を特定の方法で処理するように定義されました。Memcopyは、オーバーラップしていない領域に使用できる最適なアルゴリズムを使用することになっています。通常、実装はほとんど同じです。

あなたが遭遇した問題は、x86ハードウェアの非常に多くのバリエーションがあり、メモリをシフトするどの方法が最速であるかを見分けることが不可能であることです。また、ある状況で結果が得られたと考えている場合でも、メモリレイアウトに異なる「ストライド」があるような単純なものは、キャッシュパフォーマンスが大幅に異なる可能性があります。

実際に行っていることをベンチマークするか、問題を無視して、Cライブラリに対して行われたベンチマークに依存することができます。

編集:ああ、最後にもう1つ。大量のメモリコンテンツのシフトは非常に遅いです。整数を処理する単純なBツリー実装のようなものを使用すると、アプリケーションがより高速に実行されると思います。(ああ、大丈夫)

Edit2:コメントで私の拡張を要約すると:マイクロベンチマークはここでの問題であり、それがあなたが何であるかを測定するものではありません。memcpyとmemmoveに与えられるタスクは、互いに大きく異なります。memcpyに与えられたタスクがmemmoveまたはmemcpyで数回繰り返される場合、領域が重複しない限り、最終的な結果は使用するメモリシフト機能に依存しません。


しかし、それはそれが何であるかです-私は私が実際に何をしているのかをベンチマークしています。この質問は、あなたが主張しているものと矛盾するベンチマークの結果を解釈することについてです-memcpyは重複しない領域に対してより高速です。
cruppstahl 2015

私のアプリケーション Bツリーです!整数がリーフノードに挿入されるたびに、スペースを空けるためにmemmoveが呼び出されます。データベースエンジンに取り組んでいます。
cruppstahl 2015

1
マイクロベンチマークを使用していて、memcopyとmemmoveで同じデータをシフトすることさえしていません。対処しているデータが存在するメモリ内の正確な場所によって、キャッシングと、CPUがメモリへのラウンドトリップを何回行う必要があるかが異なります。
user3710044 2015

この答えは正しいですが、この場合はなぜ遅くなるのは実際には説明されていません。基本的には「場合によっては遅くなる可能性があるため遅くなる」と述べています。
Oliver Charlesworth、

同じ状況では、ベンチマークをコピー/移動するための同じメモリレイアウトを含め、実装は同じであるため、同じになると言います。問題はマイクロベンチマークにあります。
user3710044

2

「memcpyはmemmoveよりも効率的です。」あなたのケースでは、2つの関数を実行している間、あなたはおそらくまったく同じことをしていません。

一般に、必要な場合にのみmemmoveを使用してください。ソースと宛先の領域が重複している可能性が非常に高い場合に使用します。

参照:https ://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain、(Stanford Intro Systems Lecture-7)時間:36:00

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.