以下に示すように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]);
ここでは、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
です。
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%
これらの結果は、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::map
、std::vector
、std::deque
、または独自のカスタム型)で機能します。つまり、データのチャンクをコピーする必要がある場合は、std::copy
を使用するだけです。
私が知っているすべてのコンパイラーは、適切な場合は単純なstd::copy
をmemcpy
に置き換えますが、さらに良いのは、コピーを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)
memmove
はmemcpy
になり、パフォーマンスがわずかに向上します。 memcpy
自体は非常にベクトル化されることに注意してください。
memcpy
はCスタイルのPOD構造のみに制限されているため、常にstd::copy
を使用します。ターゲットが実際にPODである場合、コンパイラはstd::copy
への呼び出しをmemcpy
に置き換えます。
さらに、std::copy
は、ポインターだけでなく、多くの反復子タイプで使用できます。 std::copy
は、パフォーマンスを損なうことなくより柔軟であり、明確な勝者です。
理論上、memcpy
にはslight、inceptceptible、infinitesimal、パフォーマンスの利点。これは、std::copy
と同じ要件がないためです。 memcpy
のmanページから:
オーバーフローを回避するために、宛先パラメーターとソースパラメーターの両方が指す配列のサイズは、少なくともnumバイトでなければならず、オーバーラップしてはなりません(オーバーラップの場合)メモリブロック、memmoveはより安全なアプローチです)。
つまり、memcpy
はデータの重複の可能性を無視できます。 (重複する配列をmemcpy
に渡すことは未定義の動作です。)したがって、memcpy
はこの状態を明示的にチェックする必要はありませんが、std::copy
はOutputIterator
パラメータがソース範囲内にありません。これはnotであり、ソース範囲と宛先範囲は重複できないことを言っているのと同じです。
したがって、std::copy
の要件は多少異なるため、理論的にはslightly(slightlyに極端に重点を置いて)おそらく、重複するC-arrayをチェックするか、チェックを実行する必要があるmemmove
にC-arrayのコピーを委任するためです。しかし、実際には、あなた(およびほとんどのプロファイラー)はおそらく違いさえ検出しません。
もちろん、 PODs を使用していない場合は、とにかくmemcpy
を使用することはできません。
私のルールは簡単です。 C++を使用している場合は、CではなくC++ライブラリを使用してください:)
ほんの少しの追加: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 */
本当に最大のコピーパフォーマンスが必要な場合(そうでない場合があります)、どちらも使用しないでください。
メモリコピーを最適化するために実行できるlotがあります-複数のスレッド/コアを使用する場合はさらに多くなります。以下を参照してください。
このmemcpyの実装で不足している/最適でないものは何ですか?
質問と回答のいくつかは、実装または実装へのリンクを示唆しています。