パフォーマンスの観点からstd :: memcpy()またはstd :: copy()を使用する方が良いですか?


163

memcpy以下に示すように使用する方が良いstd::copy()ですか、それともパフォーマンスの観点から使用する方が良いですか?どうして?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);

char実装に応じて、署名付きまたは署名なしの場合があることに注意してください。バイト数が128以上の場合unsigned char、バイト配列に使用します。((int *)キャスト(unsigned int *)もとして安全です。)
Dan Breslau

13
なぜ使ってないのstd::vector<char>?またはあなたが言うbitsので、std::bitset
GManNickG 2011年

2
実際、何を説明していただけ(int*) copyMe->bits[0]ますか?
user3728501 2015

4
重要なコンテキストがほとんど提供されていないような混乱の原因が+81であった理由はわかりませんが、ちょっと。@ user3728501私の推測では、バッファーの先頭にintそのサイズが決まりますが、これは他の多くのものと同様に、実装定義の災害のレシピのように見えます。
underscore_d

2
実際、その(int *)キャストは純粋に未定義の動作であり、実装定義ではありません。キャストを介して型パンニングを行おうとすると、厳密なエイリアシングルールに違反するため、規格ではまったく定義されていません。(また、C ++ではCではありませんが、unionどちらでも型打ちできません。)唯一の例外は、のバリアントに変換する場合ですchar*が、アローワンスは対称的ではありません。
underscore_d

回答:


207

ここでstd::copyは、わずかな、ほとんど知覚できないパフォーマンスの低下をもたらす一般的な知識に反対するつもりです。私はテストを行ったところ、それが正しくないことがわかりました。パフォーマンスの違いに気づきました。しかし、勝者はでしたstd::copy

C ++ SHA-2実装を作成しました。私のテストでは、4つのSHA-2バージョン(224、256、384、512)すべてを使用して5つの文字列をハッシュし、300回ループします。Boost.timerを使用して時間を測定します。その300ループカウンターは、結果を完全に安定させるのに十分です。テストは、memcpyバージョンとバージョンを交互に5回ずつ実行しましたstd::copy。私のコードは、(他の多くの実装がで動作可能な限りチャンクの大きいようにつかむデータを利用するchar/ char *Iは、で動作するのに対し、T/ T *(ここでT正しいオーバーフロー挙動を有するユーザの実装で最大の種類がある)、上のように高速メモリアクセスを私ができる最大のタイプは私のアルゴリズムのパフォーマンスの中心です。これらは私の結果です:

SHA-2テストの実行を完了するまでの時間(秒)

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

std :: copyのmemcpyに対する平均平均速度増加:2.99%

私のコンパイラはFedora 16 x86_64上のgcc 4.6.3です。私の最適化フラグは-Ofast -march=native -funsafe-loop-optimizationsです。

SHA-2実装のコード。

MD5実装でもテストを実行することにしました。結果ははるかに安定していなかったので、10回実行することにしました。しかし、最初の数回の試行の後、実行ごとに大きく異なる結果が得られたため、何らかのOSアクティビティが発生していると思います。最初からやり直すことにしました。

同じコンパイラ設定とフラグ。MD5のバージョンは1つしかなく、SHA-2より高速であるため、同様の5つのテスト文字列のセットで3000ループを実行しました。

これらは私の最後の10件の結果です。

MD5テストの実行を完了するまでの時間(秒)

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

std :: copyのmemcpy全体の平均速度低下:0.11%

MD5実装のコード

これらの結果は、私のSHA-2テストでstd :: copyを使用してstd::copyいて、MD5テストでは使用できないいくつかの最適化があることを示唆しています。SHA-2テストでは、両方の配列がstd::copy/ を呼び出した同じ関数で作成されましたmemcpy。MD5テストでは、配列の1つが関数パラメーターとして関数に渡されました。

もう少しstd::copy速くするために何ができるかを確認するために、もう少しテストを行いました。答えは簡単であることがわかりました。リンク時の最適化を有効にします。LTOをオンにした結果(gccのオプション-flto):

-fltoを使用してMD5テストの実行を完了するまでの時間(秒)

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

std :: copyのmemcpyに対する平均平均速度増加率:0.72%

要約すると、を使用してもパフォーマンスが低下することはないようstd::copyです。実際、パフォーマンスが向上しているようです。

結果の説明

では、なぜstd::copyパフォーマンスが向上するのでしょうか?

まず、インライン化の最適化がオンになっている限り、どのような実装でもそれが遅くなることはないと思います。すべてのコンパイラーは積極的にインライン化します。それは他の多くの最適化を可能にするので、それはおそらく最も重要な最適化です。std::copy引数が簡単にコピー可能であり、メモリが順番に配置されていることを検出できます(実際の実装ではすべてそうなります)。これは、最悪の場合、memcpy合法である場合、std::copyパフォーマンスが低下しないことを意味します。些細な実装std::copyするために、その延期memcpy「速度や大きさのために最適化する際、常にこれをインライン」のコンパイラの基準を満たす必要があります。

ただし、std::copyその情報も多く保持されます。を呼び出すstd::copyと、関数は型をそのまま保持します。memcpyはで動作しvoid *、ほとんどすべての有用な情報を破棄します。たとえば、の配列を渡した場合std::uint64_t、コンパイラまたはライブラリの実装者はで64ビットアライメントを利用できるかもしれませんstd::copyが、でそれを行うのはより難しい場合がありますmemcpy。このようなアルゴリズムの多くの実装では、最初に範囲の最初の位置合わせされていない部分、次に位置合わせされた部分、最後に位置合わせされていない部分を処理します。すべてが整列していることが保証されている場合、コードはよりシンプルで高速になり、プロセッサーのブランチ予測子が正しく取得しやすくなります。

時期尚早の最適化?

std::copy面白い位置にいます。memcpy最新の最適化コンパイラよりも遅くなることはなく、時には速くなることを期待しています。さらに、できることなら何でもmemcpyできstd::copyます。memcpyバッファーのオーバーラップは許可されませんが、std::copyサポートは一方向のオーバーラップをサポートします(オーバーラップstd::copy_backwardのもう一方の方向で)。memcpy、ポインタ上で動作だけでstd::copy任意のイテレータ(上で動作しstd::mapstd::vectorstd::deque、または独自のカスタムタイプ)。つまり、std::copyデータのチャンクをコピーする必要がある場合にのみ使用する必要があります。


35
これは、std::copyが2.99%、0.72%、-0.11%速いという意味ではなくmemcpy、これらの時間はプログラム全体が実行されるためのものです。しかし、私は一般的に、実際のコードのベンチマークは、偽のコードのベンチマークよりも有用であると感じています。私のプログラム全体で、実行速度にその変化がありました。2つのコピースキームだけの実際の効果は、ここで示したものを単独で見ると、より大きな違いがありますが、これは、実際のコードに測定可能な違いがあることを示しています。
David Stone、

2
あなたの調査結果に同意しませんが、結果は結果です:/。ただし、1つの質問(私はそれがずっと前だったことを知っていて、研究を覚えていないので、考えた方法でコメントしてください)、おそらくアセンブリコードを調べませんでした。
ST3 2015年

2
私の意見memcpystd::copyは、実装が異なるため、コンパイラは、周囲のコードと実際のメモリコピーコードを1つの統合されたコードとして最適化する場合があります。つまり、ある方がより優れている場合があり、言い換えると、どちらを使用するかを決定するのは時期尚早または愚かな最適化です。なぜなら、あらゆる状況で新しい研究を行う必要があり、さらに、プログラムは通常開発されているためです。他の機能に対するいくつかの小さな変更の利点が失われる可能性があります。
ST3 2015年

3
@ ST3:私は最悪の場合、それが合法なときにstd::copyだけ呼び出す単純なインライン関数だと想像しmemcpyます。基本的なインライン化により、パフォーマンスのマイナスの影響を排除できます。std :: copyの方が高速な理由を少し説明して投稿を更新します。
David Stone

7
非常に有益な分析。再STDの速度の合計の平均減少は:: memcpyを上書きコピー:0.11%を、番号が正しいことながら、結果は統計的に有意ではありません。平均の差の95%信頼区間は(-0.013s、0.025)で、これにはゼロが含まれます。他のソースやデータとの違いがあったことを指摘したように、パフォーマンスは同じだと思います。参考までに、他の2つの結果は統計的に有意です-この極端な時間の違いが偶然に見られる可能性は、約1億分の1(最初)と20,000分の1(最後)です。
TooTone 2016年

78

私が知っているすべてのコンパイラは単純なものstd::copymemcpy、それは適切な、あるいは優れているとき、それはさらに速くよりになるように、コピーをベクトル化しますmemcpy

いずれにせよ:プロファイルを作成し、自分自身を見つけます。コンパイラが異なれば動作も異なり、要求したとおりに実行されない可能性もあります。

見る コンパイラーの最適化に関するこのプレゼンテーション(pdf)を。

これGCCがstd::copy PODタイプのシンプルに対して行うことです。

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

これが逆アセンブリ(-O最適化のみ)で、次の呼び出しを示していますmemmove

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

関数のシグネチャを次のように変更した場合

void bar(foo* __restrict a, foo* __restrict b, size_t n)

次に、memmovememcpyパフォーマンスがわずかに向上します。それmemcpy自体が大幅にベクトル化されることに注意してください。


1
プロファイリングを行うにはどうすればよいですか。使用するツール(WindowsおよびLinux)
user576670 2011年

5
@Konrad、あなたは正しいです。ただしmemmove、2つのデータ範囲が重複する可能性を考慮に入れなければならないため、速くなるべきではありません。むしろ遅くなるはずです。私が考えるstd::copy許可がデータを重複して、それが呼び出す必要がありますのでmemmove
Charles Salvia

2
@Konrad:memmoveがmemcpyよりも常に速い場合、memcpyはmemmoveを呼び出します。std :: copyが実際にディスパッチする可能性があるものは(もしあれば)実装定義であるため、実装について言及せずに詳細について言及することは役に立ちません。
Fred Nurk、2011年

1
ただし、この動作を再現する単純なプログラムで、GCCの下で-O3を指定してコンパイルすると、が表示されますmemcpy。GCCがメモリのオーバーラップがないかどうかを確認すると思います。
jweyrich、2011年

1
@Konrad:標準でstd::copyは、一方向のオーバーラップが許可されていますが、他の方向には許可されていません。出力の先頭を入力範囲内にすることはできませんが、入力の先頭を出力範囲内にすることはできます。割り当ての順序が定義されているため、これは少し奇妙です。その順序での割り当ての効果が定義されていても、呼び出しはUBになる可能性があります。しかし、制限によりベクトル化の最適化が可能になると思います。
Steve Jessop、2011年

24

CスタイルのPOD構造のみに制限されているstd::copyため、常に使用します。memcpyコンパイラはstd::copymemcpy。ターゲットが実際にPOD場合、。

さらに、std::copyポインタだけでなく、多くのタイプのイテレータで使用できます。std::copyパフォーマンスの低下がないため、より柔軟であり、明らかに勝者です。


なぜイテレータをコピーしたいのですか?
11

3
イテレータをコピーするのではなく、2つのイテレータによって定義された範囲をコピーします。たとえば、std::copy(container.begin(), container.end(), destination);containerbeginとの間のすべてend)の内容をで示されるバッファにコピーしdestinationます。またはのstd::copyようなシェニガンは必要ありません。&*container.begin()&container.back() + 1
David Stone、

16

理論的には、memcpy持っているかもしれないわずかな知覚できない微小それは同じ要件を持っていないという理由だけで、パフォーマンス上の利点をstd::copy。のマニュアルページからmemcpy

オーバーフローを回避するために、宛先パラメーターとソースパラメーターの両方が指す配列のサイズは少なくともnumバイトでなければならず、オーバーラップしてなりません(メモリーブロックがオーバーラップしている場合は、memmoveがより安全です)。

つまり、memcpyデータが重複する可能性を無視できます。(重複する配列を渡すことmemcpyは未定義の動作です。)したがってmemcpy、この条件を明示的に確認する必要はありませんがstd::copyOutputIteratorパラメーターがソース範囲にない限り使用できます。これは違います、ソース範囲と宛先範囲を重複させることができないということと同じでことに。

したがって、std::copy要件が多少異なるので、理論的には、C配列の重複をチェックするか、またはC配列のコピーを委任するため、理論的には少し(極端に少し重視)遅くなるはずです。memmoveを実行する必要があり、小切手。しかし実際には、あなた(そしてほとんどのプロファイラー)はおそらく違いさえ検出しません。

もちろん、あなたが作業していない場合のPOD、あなたがすることはできません使用memcpyとにかく。


7
これはにも当てはまりますstd::copy<char>。ただしstd::copy<int>、その入力はint-alignedであると想定できます。それはすべての要素に影響するため、はるかに大きな違いが生じます。オーバーラップは1回限りのチェックです。
MSalters 2011年

2
@MSalters、本当ですが、memcpy私が見たほとんどの実装は整列をチェックし、バイト単位ではなく単語をコピーしようとします。
Charles Salvia、

1
std :: copy()も重複するメモリを無視できます。重複するメモリをサポートする場合は、適切な状況でstd :: reverse_copy()を呼び出すロジックを自分で作成する必要があります。
Cygon

2
反対の議論をすることができます:memcpyインターフェイスを通過するとき、それは整列情報を失います。したがって、memcpyは整列されていない開始と終了を処理するために、実行時に整列チェックを行う必要があります。これらの小切手は安いかもしれませんが無料ではありません。一方std::copy、これらのチェックを回避してベクトル化できます。また、コンパイラーは、ユーザーがとを選択しなくても、ソース配列と宛先配列がオーバーラップせず、再びベクトル化することを証明する場合がmemcpyありmemmoveます。
Maxim Egorushkin 2016年

11

私のルールは簡単です。C ++を使用している場合は、CではなくC ++ライブラリを使用してください。


40
C ++はCライブラリを使用できるように明示的に設計されました。これは偶然ではありませんでした。多くの場合、C ++ではmemcpyよりもstd :: copyを使用する方が適切ですが、これはCがどれであるかとは関係がなく、そのような引数は通常間違ったアプローチです。
Fred Nurk、2011年

2
@FredNurk通常、C ++がより安全な代替手段を提供するCの弱い領域を避けたいと思います。
Phil1970

@ Phil1970この場合、C ++の方がはるかに安全かどうかはわかりません。オーバーランしない有効なイテレータを渡す必要があります。代わりに、より安全に使用できると思いますか?そしておそらくもっと重要なことは、より明確です。そして、それがこの特定のケースで私が強調するポイントです。イテレータのタイプが後で変更され、より明確な構文などにつながる場合、より慣用的で、より保守可能ですstd::end(c_arr)c_arr + i_hope_this_is_the_right_number_of elementsstd::copy()
underscore_d

1
@underscore_d std::copyは、PODタイプでない場合に渡されたデータを正しくコピーするため、より安全です。オブジェクトを1バイトずつ新しい表現にmemcpy喜んでコピーstd::stringします。
Jens

3

ほんの小さな追加:との速度の違いはmemcpy()std::copy()最適化が有効か無効かによってかなり異なります。g ++ 6.2.0を使用し、最適化を行わなかった場合、memcpy()明らかにメリットがあります。

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

最適化を有効にすると(-O3)、すべてがほぼ同じに見えます。

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

配列が大きいほど、効果は目立ちN=1000 memcpy()ませんが、最適化が有効になっていない場合でも、速度は約2倍になります。

ソースコード(Googleベンチマークが必要):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */

18
最適化を無効にしてパフォーマンスを測定することは...まあ...ほぼ無意味です...パフォーマンスに関心がある場合は、最適化なしではコンパイルできません。
ボロフ

3
@bolov常にではありません。デバッグ中の比較的高速なプログラムは、場合によっては重要です。
ドングリ

2

本当に最大のコピーパフォーマンスが必要な場合(そうでない場合もあります)、どちらも使用しないでください。

メモリのコピーを最適化するためにできることはたくさんあります。複数のスレッド/コアを使用する場合でもなおさらです。たとえば、次を参照してください。

このmemcpy実装に欠けている/最適ではないものは何ですか?

質問といくつかの回答の両方で、実装または実装へのリンクが提案されています。


4
ペダントモード:通常、「どちらも使用しない」という警告は、実装によって提供される標準関数のいずれも十分に高速ではない、非常に具体的な状況/要件があることを証明した場合を意味します。それ以外の場合、私の通常の懸念は、プログラムの通常より有用な部分ではなく、コードのコピーを時期尚早に最適化することを回避することを証明していない人々です。
underscore_d

-2

プロファイリングはそのステートメントを示しています:std::copy()常に同じくらい高速ですmemcpy()偽であるか速いです。

私のシステム:

HP-Compaq-dx7500-Microtower 3.13.0-24-generic#47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux。

gcc(Ubuntu 4.8.2-19ubuntu1)4.8.2

コード(言語:c ++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

memcpy()プロファイル:main:21:now:1422969084:04859経過:2650 us
std :: copy()プロファイル:main:27:now:1422969084:04862lapsed:2745 us
memcpy()経過44 s std :: copy( )経過45秒

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy()プロファイル:main:21:現在:1422969601:04939経過:2385 us
std :: copy()プロファイル:main:28:現在:1422969601:04941経過:2690 us
memcpy()経過27 s std :: copy( )経過43秒

Red Alertは、コードが配列から配列へのmemcpyおよび配列からベクトルへのstd :: copyを使用することを指摘しました。そのcoudはより速いmemcpyの理由です。

あるので

v.reserve(sizeof(arr1));

ベクトルまたは配列へのコピーに違いがあってはなりません。

コードは両方の場合に配列を使用するように修正されています。memcpyはさらに高速です:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 

1
間違っています。プロファイリングは、配列へのコピーがベクトルへのコピーよりも速いことを示しています。オフトピック。
2015

私は間違っている可能性がありますが、修正された例では、memcpyを使用してarr2をarr1にコピーしていないのに対し、std :: copyを使用してarr1をarr2にコピーしていますか?...実験(一度memcpyのバッチ、一度std :: copyのバッチ、次にmemcopyなどで複数回戻る)。次に、time()の代わりにclock()を使用します。なぜなら、そのプログラムに加えて、あなたのPCが何ができるのか誰が知っているからです。でも私の2セントだけは... :-)
paercebal

7
それで、std::copyベクトルから配列に切り替えると、どういうわけmemcpyか2倍近くの時間がかかりましたか?このデータは非常に疑わしいものです。私は-O3でgccを使用してコードをコンパイルしましたが、生成されたアセンブリは両方のループで同じです。したがって、マシンで観察する時間の違いは偶発的なものです。
レッドアラート
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.