Memcpyを実行している関数がありますが、膨大なサイクルを消費しています。 memcpyを使用してメモリを移動するよりも高速な代替手段/アプローチはありますか?
memcpy
は、メモリ内でバイトをコピーする最も速い方法です。より高速なものが必要な場合-notをコピーする方法を考えてみてください。データ自体ではなく、ポインタのみをスワップします。
これは、AVX2命令セットが存在するx86_64に対する回答です。同様のことがSIMDを使用したARM/AArch64にも適用される場合があります。
単一のメモリチャネルが完全に満たされたRyzen 1800X(2スロット、それぞれ16 GB DDR4)では、次のコードはMSVC++ 2017コンパイラのmemcpy()
よりも1.56倍高速です。両方のメモリチャネルを2つのDDR4モジュールで満たす場合、つまり4つのDDR4スロットがすべてビジーである場合、さらに2倍のメモリコピーを取得できます。トリプル(クワッド)チャネルメモリシステムの場合、コードを類似のAVX512コードに拡張すると、さらに1.5(2.0)倍高速なメモリコピーを取得できます。すべてのスロットがビジーなAVX2のみのトリプル/クワッドチャネルシステムでは、完全にロードするには32バイト以上を一度にロード/保存する必要があるため、高速化は期待できません(トリプルチャネルでは48バイト、クアッドチャネルでは64バイトシステム)、AVX2は一度に32バイトまでしかロード/保存できません。一部のシステムでのマルチスレッドは、AVX512またはAVX2なしでもこれを軽減できますが。
そのため、サイズが32の倍数であり、ブロックが32バイトにアライメントされているメモリの大きなブロックをコピーすることを想定したコピーコードを次に示します。
サイズが複数でアラインされていないブロックの場合、プロローグ/エピローグコードを記述して、ブロックの先頭と末尾の幅を16(SSE4.1)、8、4、2、最後に一度に1バイトに減らすことができます。また、中央では、ソースからの整列読み取りと宛先への整列書き込みの間のプロキシとして、2〜3個の___m256i
_値のローカル配列を使用できます。
_#include <immintrin.h>
#include <cstdint>
/* ... */
void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) {
assert(nBytes % 32 == 0);
assert((intptr_t(pvDest) & 31) == 0);
assert((intptr_t(pvSrc) & 31) == 0);
const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc);
__m256i *pDest = reinterpret_cast<__m256i*>(pvDest);
int64_t nVects = nBytes / sizeof(*pSrc);
for (; nVects > 0; nVects--, pSrc++, pDest++) {
const __m256i loaded = _mm256_stream_load_si256(pSrc);
_mm256_stream_si256(pDest, loaded);
}
_mm_sfence();
}
_
このコードの重要な機能は、コピー時にCPUキャッシュをスキップすることです:CPUキャッシュが関係する場合(つまり、__stream_
_のないAVX命令が使用される場合)、システム上でコピー速度が数回低下します。
DDR4メモリは2.6GHz CL13です。そのため、あるアレイから別のアレイに8GBのデータをコピーすると、次の速度が得られました。
_memcpy(): 17 208 004 271 bytes/sec.
Stream copy: 26 842 874 528 bytes/sec.
_
これらの測定では、入力バッファと出力バッファの両方の合計サイズが経過した秒数で除算されることに注意してください。配列の各バイトには2つのメモリアクセスがあるため、1つは入力配列からバイトを読み取るため、もう1つは出力配列にバイトを書き込むためです。つまり、あるアレイから別のアレイに8GBをコピーする場合、16GB相当のメモリアクセス操作を実行します。
中程度のマルチスレッド化により、パフォーマンスがさらに約1.44倍向上するため、memcpy()
を超える合計増加は、私のマシンでは2.55倍に達します。ストリームコピーのパフォーマンスが、マシンで使用されるスレッドの数にどのように依存するかを以下に示します。
_Stream copy 1 threads: 27114820909.821 bytes/sec
Stream copy 2 threads: 37093291383.193 bytes/sec
Stream copy 3 threads: 39133652655.437 bytes/sec
Stream copy 4 threads: 39087442742.603 bytes/sec
Stream copy 5 threads: 39184708231.360 bytes/sec
Stream copy 6 threads: 38294071248.022 bytes/sec
Stream copy 7 threads: 38015877356.925 bytes/sec
Stream copy 8 threads: 38049387471.070 bytes/sec
Stream copy 9 threads: 38044753158.979 bytes/sec
Stream copy 10 threads: 37261031309.915 bytes/sec
Stream copy 11 threads: 35868511432.914 bytes/sec
Stream copy 12 threads: 36124795895.452 bytes/sec
Stream copy 13 threads: 36321153287.851 bytes/sec
Stream copy 14 threads: 36211294266.431 bytes/sec
Stream copy 15 threads: 35032645421.251 bytes/sec
Stream copy 16 threads: 33590712593.876 bytes/sec
_
コードは次のとおりです。
_void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) {
for (; nVects > 0; nVects--, pSrc++, pDest++) {
const __m256i loaded = _mm256_stream_load_si256(pSrc);
_mm256_stream_si256(pDest, loaded);
}
}
void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) {
assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0);
const uint32_t maxThreads = std::thread::hardware_concurrency();
std::vector<std::thread> thrs;
thrs.reserve(maxThreads + 1);
const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput);
__m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput);
const int64_t nVects = cnDoubles * sizeof(*gpdInput) / sizeof(*pSrc);
for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) {
auto start = std::chrono::high_resolution_clock::now();
lldiv_t perWorker = div((long long)nVects, (long long)nThreads);
int64_t nextStart = 0;
for (uint32_t i = 0; i < nThreads; i++) {
const int64_t curStart = nextStart;
nextStart += perWorker.quot;
if ((long long)i < perWorker.rem) {
nextStart++;
}
thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart);
}
for (uint32_t i = 0; i < nThreads; i++) {
thrs[i].join();
}
_mm_sfence();
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double) / nSec);
thrs.clear();
}
}
_
詳細をお知らせください。 i386アーキテクチャでは、memcpyが最速のコピー方法である可能性が非常に高いです。しかし、コンパイラーが最適化されたバージョンを持たない別のアーキテクチャーでは、memcpy関数を書き直すことをお勧めします。これをカスタムのARMアセンブリ言語を使用したアーキテクチャで行いました。メモリの大きなチャンクを転送する場合は、 [〜#〜] dma [〜#〜] がおそらくあなたが探している答え。
アーキテクチャ、オペレーティングシステム(関連する場合)の詳細をお知らせください。
実際、memcpyは最速の方法ではありません。特に何度も呼び出す場合はそうです。私は本当にスピードアップするために必要なコードもいくつか持っていましたが、memcpyは不必要なチェックが多すぎるため遅いです。たとえば、コピー先とコピー元のメモリブロックが重複しているかどうか、および前面ではなくブロックの背面からコピーを開始する必要があるかどうかを確認します。あなたがそのような考慮事項を気にしないなら、あなたは確かに著しく良くすることができます。私はいくつかのコードを持っていますが、おそらくこれまでよりも良いバージョンです:
検索すると、他の実装も見つけることができます。しかし、真の速度を実現するには、アセンブリバージョンが必要です。
通常、コンパイラに同梱されている標準ライブラリは、memcpy()
を既にターゲットプラットフォームで可能な最速の方法で実装します。
Agner Fogにはmemcpyの高速実装があります http://www.agner.org/optimize/#asmlib
一般に、コピーをまったく作成しないほうが高速です。コピーしないように機能を調整できるかどうかはわかりませんが、調べてみる価値はあります。
Memcpy、memsetなどの関数は、2つの異なる方法で実装される場合があります。
すべてのコンパイラがデフォルトでインラインアセンブリバージョンを使用するわけではありません。コンパイラはデフォルトで関数バリアントを使用する場合があり、関数呼び出しのためにオーバーヘッドが発生します。コンパイラーをチェックして、関数の組み込みバリアント(コマンドラインオプション、プラグマなど)の使用方法を確認してください。
編集:Microsoft Cコンパイラの組み込み関数の説明については、 http://msdn.Microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx を参照してください。
プラットフォームでサポートされている場合は、mmap()システムコールを使用してデータをファイルに残すことができるかどうかを調べてください。一般的に、OSはそれをより適切に管理できます。そして、誰もが言っているように、可能な限りコピーを避けてください。このような場合、ポインターはあなたの友達です。
コード用に生成されたアセンブリコードを確認する必要があります。望ましくないのは、memcpy
呼び出しで標準ライブラリのmemcpy
関数の呼び出しを生成することです。必要なのは、コピーする最適なASM命令を繰り返し呼び出すことです。最大量のデータ-rep movsq
。
どうすればこれを達成できますか?コンパイラーは、コピーするデータ量がわかっている限り、単純なmemcpy
sに置き換えることにより、mov
への呼び出しを最適化します。 memcpy
を適切に決定された(constexpr
)値で記述すると、これを見ることができます。コンパイラが値を知らない場合、memcpy
のバイトレベルの実装にフォールバックする必要があります-memcpy
は1バイトの粒度を尊重する必要があるという問題です。一度に128ビットを移動しますが、128bごとに128bとしてコピーするのに十分なデータがあるかどうかをチェックする必要がありますか、64ビットにフォールバックする必要があり、その後32および8にフォールバックする必要がありますとにかく、しかし、私は確かに知りません)。
そのため、コンパイラが最適化できるconst式を使用してデータのサイズをmemcpy
に伝えることができます。この方法では、memcpy
への呼び出しは実行されません。望まないのは、実行時にのみ知られる変数をmemcpy
に渡すことです。これは、関数呼び出しと多数のテストに変換され、最適なコピー命令をチェックします。時々、この理由のために、単純なforループのほうがmemcpy
よりも優れています(1つの関数呼び出しを削除します)。そして、本当に必要ないは、memcpy
にコピーする奇数バイトを渡すことです。
コンパイラ/プラットフォームのマニュアルを確認してください。 memcpyを使用する一部のマイクロプロセッサおよびDSPキットでは、 intrinsic functions または [〜#〜] dma [〜#〜] 操作よりもはるかに遅くなります。
この関数は、ポインター(入力引数)の1つが32ビットにアライメントされていない場合、データアボート例外を引き起こす可能性があります。
インライン化可能なmemcpyの代替Cバージョンを次に示します。これは、使用したアプリケーションでArm64のGCCのmemcpyよりも約50%優れています。 64ビットプラットフォームに依存しません。使用インスタンスでもう少し高速化する必要がない場合は、テール処理を削除できます。 uint32_t配列をコピーします。小さなデータ型はテストされていませんが、動作する可能性があります。他のデータ型に適応できる場合があります。 64ビットコピー(2つのインデックスが同時にコピーされます)。 32ビットも動作するはずですが、遅いです。 Neoscryptプロジェクトの功績。
static inline void newmemcpy(void *__restrict__ dstp,
void *__restrict__ srcp, uint len)
{
ulong *dst = (ulong *) dstp;
ulong *src = (ulong *) srcp;
uint i, tail;
for(i = 0; i < (len / sizeof(ulong)); i++)
*dst++ = *src++;
/*
Remove below if your application does not need it.
If console application, you can uncomment the printf to test
whether tail processing is being used.
*/
tail = len & (sizeof(ulong) - 1);
if(tail) {
//printf("tailused\n");
uchar *dstb = (uchar *) dstp;
uchar *srcb = (uchar *) srcp;
for(i = len - tail; i < len; i++)
dstb[i] = srcb[i];
}
}
あなたはこれを見たいと思うかもしれません:
http://www.danielvik.com/2010/02/fast-memcpy-in-c.html
私が試みる別のアイデアは、COWテクニックを使用してメモリブロックを複製し、ページが書き込まれるとすぐにOSがオンデマンドでコピーを処理できるようにすることです。ここにはmmap()
を使用したいくつかのヒントがあります: Linuxでコピーオンライトmemcpyを実行できますか?
メモリからメモリは通常CPUのコマンドセットでサポートされており、memcpyは通常それを使用します。そして、これは通常最速の方法です。
CPUの動作を正確に確認する必要があります。 Linuxでは、sar -B 1またはvmstat 1を使用するか、/ proc/memstatを調べて、swapiの入出力と仮想メモリの有効性を監視します。コピーが多くのページを押し出してスペースを解放したり、それらを読み込んだりする必要があることがわかります。
つまり、問題はコピーに使用するものではなく、システムがメモリを使用する方法にあります。ファイルキャッシュを減らすか、より早く書き込みを開始するか、メモリ内のページをロックする必要がある場合があります。
Memcpyのパフォーマンスが問題になった場合、コピーしたいメモリの巨大な領域が必要だと思いますか?
この場合、nosの提案には同意します。
変更する必要があるたびに1つの巨大なメモリの塊をコピーする代わりに、代わりにいくつかの代替データ構造を試す必要があります。
あなたの問題領域について本当に何も知らずに、 persistent data structures をよく見て、独自のものを実装するか、既存の実装を再利用することをお勧めします。
nosは正しい、あなたはそれをやりすぎだ。
呼び出し元とその理由を確認するには、デバッガーで数回一時停止してスタックを確認します。