web-dev-qa-db-ja.com

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

以下に示すように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]);
152
user576670

ここでは、std::copyがわずかな、ほとんど感知できないパフォーマンスの低下をもたらすという一般的な知恵に反するつもりです。ただテストを行ったところ、真実ではないことがわかりました。パフォーマンスの違いに気付きました。ただし、勝者はstd::copyでした。

C++ SHA-2実装を作成しました。私のテストでは、4つすべてのSHA-2バージョン(224、256、384、512)を使用して5つの文字列をハッシュし、300回ループします。 Boost.timerを使用して時間を測定します。その300ループカウンターは、結果を完全に安定させるのに十分です。 memcpyバージョンとstd::copyバージョンを交互に使用して、テストを5回実行しました。私のコードは、可能な限り大きなチャンクでデータを取得することを活用しています(他の多くの実装はchar/char *で動作しますが、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%

memcpyを超えるstd :: copyの速度の合計平均増加: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%

memcpy上のstd :: copyの速度の合計平均低下:0.11%

MD5実装のコード

これらの結果は、std::copyがMD5テストで使用できなかったstd :: copyがSHA-2テストで使用されたいくつかの最適化があることを示唆しています。 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%

memcpyを超えるstd :: copyの速度の合計平均増加:0.72%

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

結果の説明

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

まず、インライン化の最適化が有効になっている限り、どの実装でも遅くなるとは思わないでしょう。すべてのコンパイラーは積極的にインライン展開します。これは、他の多くの最適化を可能にするため、おそらく最も重要な最適化です。 std::copyは、引数が簡単にコピー可能であり、メモリが順番に配置されていることを検出できます(そして、私はすべての実世界の実装がそうだと思います)。これは、最悪の場合、memcpyが正当な場合、std::copyのパフォーマンスは低下しないことを意味します。 memcpyに従う遅延std::copyの自明な実装は、コンパイラの「速度またはサイズを最適化するときに常にインライン化する」という基準を満たす必要があります。

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

早すぎる最適化?

std::copyは興味深い位置にあります。 memcpyより遅くなることはなく、現代の最適化コンパイラーで高速になることもあります。さらに、memcpyにできることはすべて、std::copyにできます。 memcpyはバッファー内でのオーバーラップを許可しませんが、std::copyは一方向のオーバーラップをサポートします(オーバーラップの他の方向のstd::copy_backwardを使用)。 memcpyはポインターでのみ機能し、std::copyはイテレーター(std::mapstd::vectorstd::deque、または独自のカスタム型)で機能します。つまり、データのチャンクをコピーする必要がある場合は、std::copyを使用するだけです。

183
David Stone

私が知っているすべてのコンパイラーは、適切な場合は単純なstd::copymemcpyに置き換えますが、さらに良いのは、コピーをmemcpyよりも高速になるようにベクトル化することです。

いずれにせよ:プロファイルし、自分自身を見つけます。異なるコンパイラは異なることを行いますが、あなたが要求したことを正確に行わない可能性は十分にあります。

コンパイラの最適化に関するこのプレゼンテーション (pdf)を参照してください。

GCCが行うこと PODタイプの単純なstd::copyの場合です。

#include <algorithm>

struct foo
{
  int x, y;    
};

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

memmoveへの呼び出しを示す逆アセンブリ(-O最適化のみ)は次のとおりです。

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自体は非常にベクトル化されることに注意してください。

77
Peter Alexander

memcpyはCスタイルのPOD構造のみに制限されているため、常にstd::copyを使用します。ターゲットが実際にPODである場合、コンパイラはstd::copyへの呼び出しをmemcpyに置き換えます。

さらに、std::copyは、ポインターだけでなく、多くの反復子タイプで使用できます。 std::copyは、パフォーマンスを損なうことなくより柔軟であり、明確な勝者です。

23
Puppy

理論上、memcpyにはslightinceptceptibleinfinitesimal、パフォーマンスの利点。これは、std::copyと同じ要件がないためです。 memcpyのmanページから:

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

つまり、memcpyはデータの重複の可能性を無視できます。 (重複する配列をmemcpyに渡すことは未定義の動作です。)したがって、memcpyはこの状態を明示的にチェックする必要はありませんが、std::copyOutputIteratorパラメータがソース範囲内にありません。これはnotであり、ソース範囲と宛先範囲は重複できないことを言っているのと同じです。

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

もちろん、 PODs を使用していない場合は、とにかくmemcpyを使用することはできません。

17
Charles Salvia

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

11
UmmaGumma

ほんの少しの追加: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=1000memcpy()でも約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 */
2
Grumbel

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

メモリコピーを最適化するために実行できるlotがあります-複数のスレッド/コアを使用する場合はさらに多くなります。以下を参照してください。

このmemcpyの実装で不足している/最適でないものは何ですか?

質問と回答のいくつかは、実装または実装へのリンクを示唆しています。

2
einpoklum