51

概要:

memcpyは、実際のアプリケーションまたはテストアプリケーションのシステムで2GB/秒を超えて転送できないようです。メモリ間のコピーを高速化するにはどうすればよいですか?

全詳細:

データキャプチャアプリケーションの一部として(いくつかの特殊なハードウェアを使用)、一時バッファからメインメモリに約3GB/秒をコピーする必要があります。データを取得するために、ハードウェアドライバーに一連のバッファー(各2MB)を提供します。ハードウェアはデータを各バッファにDMAし、各バッファがいっぱいになるとプログラムに通知します。私のプログラムはバッファを空にし(memcpyを別のより大きなRAMブロックに)、処理されたバッファをカードに再ポストして再度埋めます。memcpyがデータを十分に速く移動することに問題があります。メモリからメモリへのコピーは、実行しているハードウェアで3GB/秒をサポートするのに十分な速度である必要があるようです。Lavalys EVERESTは、9337MB /秒のメモリコピーベンチマーク結果を提供しますが、単純なテストプログラムであっても、memcpyではこれらの速度に近づくことはできません。

バッファ処理コード内のmemcpy呼び出しを追加/削除することで、パフォーマンスの問題を切り分けました。memcpyがなくても、約3GB/秒のフルデータレートで実行できます。memcpyを有効にすると、約550Mb /秒に制限されます(現在のコンパイラを使用)。

私のシステムでmemcpyのベンチマークを行うために、データのいくつかのブロックでmemcpyを呼び出すだけの別のテストプログラムを作成しました。(以下のコードを投稿しました)これは、使用しているコンパイラ/ IDE(National Instruments CVI)とVisual Studio2010の両方で実行しました。現在VisualStudioを使用していませんが、喜んで使用します。必要なパフォーマンスが得られる場合は、切り替えを行います。しかし、やみくもに移動する前に、それが私のmemcpyパフォーマンスの問題を解決することを確認したかったのです。

Visual C ++ 2010:1900MB/秒

NI CVI 2009:550MB/秒

CVIがVisualStudioよりも大幅に遅いことに驚いていませんが、memcpyのパフォーマンスがこれほど低いことに驚いています。これが直接比較できるかどうかはわかりませんが、これはEVERESTベンチマーク帯域幅よりもはるかに低くなっています。そのレベルのパフォーマンスは必要ありませんが、最低3GB/秒が必要です。確かに、標準ライブラリの実装は、エベレストが使用しているものよりもはるかに悪くなることはありません!

この状況でmemcpyを高速化するために、もしあれば、何ができますか?


ハードウェアの詳細:AMDMagnyCours-4xオクタルコア128GBDDR3 Windows Server 2003 Enterprise X64

テストプログラム:

#include <windows.h>
#include <stdio.h>

const size_t NUM_ELEMENTS = 2*1024 * 1024;
const size_t ITERATIONS = 10000;

int main (int argc, char *argv[])
{
    LARGE_INTEGER start, stop, frequency;

    QueryPerformanceFrequency(&frequency);

    unsigned short * src = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);
    unsigned short * dest = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);

    for(int ctr = 0; ctr < NUM_ELEMENTS; ctr++)
    {
        src[ctr] = rand();
    }

    QueryPerformanceCounter(&start);

    for(int iter = 0; iter < ITERATIONS; iter++)
        memcpy(dest, src, NUM_ELEMENTS * sizeof(unsigned short));

    QueryPerformanceCounter(&stop);

    __int64 duration = stop.QuadPart - start.QuadPart;

    double duration_d = (double)duration / (double) frequency.QuadPart;

    double bytes_sec = (ITERATIONS * (NUM_ELEMENTS/1024/1024) * sizeof(unsigned short)) / duration_d;

    printf("Duration: %.5lfs for %d iterations, %.3lfMB/sec\n", duration_d, ITERATIONS, bytes_sec);

    free(src);
    free(dest);

    getchar();

    return 0;
}

編集:余分な5分があり、貢献したい場合は、上記のコードをマシンで実行して、コメントとして時間を投稿できますか?

4

8 に答える 8

33

この状況で速度を上げる方法を見つけました。memcpyのマルチスレッドバージョンを作成し、コピーする領域をスレッド間で分割しました。上記と同じタイミングコードを使用した、設定されたブロックサイズのパフォーマンススケーリングの数値を次に示します。特にこの小さなサイズのブロックのパフォーマンスが、これほど多くのスレッドに対応できるとは思いもしませんでした。これは、このマシンの多数のメモリコントローラー(16)と関係があるのではないかと思います。

Performance (10000x 4MB block memcpy):

 1 thread :  1826 MB/sec
 2 threads:  3118 MB/sec
 3 threads:  4121 MB/sec
 4 threads: 10020 MB/sec
 5 threads: 12848 MB/sec
 6 threads: 14340 MB/sec
 8 threads: 17892 MB/sec
10 threads: 21781 MB/sec
12 threads: 25721 MB/sec
14 threads: 25318 MB/sec
16 threads: 19965 MB/sec
24 threads: 13158 MB/sec
32 threads: 12497 MB/sec

3スレッドと4スレッドの間でパフォーマンスが大幅に向上することを理解していません。このようなジャンプの原因は何ですか?

この同じ問題が発生する可能性のある他のコードについて、以下に記述したmemcpyコードを含めました。このコードにはエラーチェックがないことに注意してください。これは、アプリケーションに追加する必要がある場合があります。

#define NUM_CPY_THREADS 4

HANDLE hCopyThreads[NUM_CPY_THREADS] = {0};
HANDLE hCopyStartSemaphores[NUM_CPY_THREADS] = {0};
HANDLE hCopyStopSemaphores[NUM_CPY_THREADS] = {0};
typedef struct
{
    int ct;
    void * src, * dest;
    size_t size;
} mt_cpy_t;

mt_cpy_t mtParamters[NUM_CPY_THREADS] = {0};

DWORD WINAPI thread_copy_proc(LPVOID param)
{
    mt_cpy_t * p = (mt_cpy_t * ) param;

    while(1)
    {
        WaitForSingleObject(hCopyStartSemaphores[p->ct], INFINITE);
        memcpy(p->dest, p->src, p->size);
        ReleaseSemaphore(hCopyStopSemaphores[p->ct], 1, NULL);
    }

    return 0;
}

int startCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        hCopyStartSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        hCopyStopSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        mtParamters[ctr].ct = ctr;
        hCopyThreads[ctr] = CreateThread(0, 0, thread_copy_proc, &mtParamters[ctr], 0, NULL); 
    }

    return 0;
}

void * mt_memcpy(void * dest, void * src, size_t bytes)
{
    //set up parameters
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        mtParamters[ctr].dest = (char *) dest + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].src = (char *) src + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].size = (ctr + 1) * bytes / NUM_CPY_THREADS - ctr * bytes / NUM_CPY_THREADS;
    }

    //release semaphores to start computation
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
        ReleaseSemaphore(hCopyStartSemaphores[ctr], 1, NULL);

    //wait for all threads to finish
    WaitForMultipleObjects(NUM_CPY_THREADS, hCopyStopSemaphores, TRUE, INFINITE);

    return dest;
}

int stopCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        TerminateThread(hCopyThreads[ctr], 0);
        CloseHandle(hCopyStartSemaphores[ctr]);
        CloseHandle(hCopyStopSemaphores[ctr]);
    }
    return 0;
}
于 2010-11-24T17:28:34.490 に答える
9

実行時に実行するのか、コンパイル時に実行する必要があるのか​​はわかりませんが、ベクターユニットはCPUの64ビットに対して128ビットをメモリに書き込むことができるため、SSEまたは同様の拡張機能を有効にする必要があります。

この実装を試してください。

ええ、ソースと宛先の両方が128ビットにアラインされていることを確認してください。ソースと宛先が互いに整列していない場合、memcpy()はいくつかの深刻な魔法を実行する必要があります。:)

于 2010-11-23T20:49:54.727 に答える
5

注意すべきことの1つは、プロセス(したがってのパフォーマンスmemcpy())がタスクのOSスケジューリングによって影響を受けることです。これがタイミングにどの程度の要因があるかはわかりませんが、制御するのは困難です。デバイスのDMA操作は、開始されるとCPUで実行されないため、この対象にはなりません。ただし、アプリケーションは実際のリアルタイムアプリケーションであるため、まだ行っていない場合は、Windowsのプロセス/スレッドの優先順位設定を試してみることをお勧めします。他のプロセス(およびマシンのユーザーエクスペリエンス)に非常に悪影響を与える可能性があるため、これには注意する必要があることに注意してください。

もう1つ覚えておくべきことは、OSのメモリ仮想化がここに影響を与える可能性があることです。コピー先のメモリページが実際に物理RAMページによってバックアップされていない場合、memcpy()操作はOSに障害を起こし、その物理的なバックアップを取得します。場所。DMAページは物理メモリにロックされている可能性が高いため(DMA操作用である必要があるため)、memcpy()この点でソースメモリは問題にならない可能性があります。Win32 VirtualAlloc()APIを使用して、の宛先メモリmemcpy()がコミットされていることを確認することを検討してください(これには適切なAPIだと思いますVirtualAlloc()が、忘れているより良いAPIがあるかもしれません-私が持っていたのは久しぶりですこのようなことをする必要があります)。

最後に、Skizzが説明した手法を使用して、完全に回避できるかどうかをmemcpy()確認します。リソースが許せば、それが最善の策です。

于 2010-11-23T22:51:38.233 に答える
4

必要なメモリパフォーマンスを得るには、いくつかの障壁があります。

  1. 帯域幅-データがメモリからCPUに移動し、再び戻る速度には制限があります。このウィキペディアの記事によると、266MHzDDR3RAMの上限は約17GB/秒です。ここで、memcpyを使用すると、データの読み取りと書き込みが行われるため、最大転送速度を得るにはこれを半分にする必要があります。ベンチマークの結果から、システムで可能な限り最速のRAMを実行していないようです。あなたがそれを買う余裕があれば、マザーボード/ RAMをアップグレードしてください(そしてそれは安くはありません、英国のオーバークロッカーは現在£400で3x4GB PC16000を持っています)

  2. OS-WindowsはプリエンプティブなマルチタスクOSであるため、他のプロセスが調べて処理できるように、プロセスが一時停止されることがよくあります。これにより、キャッシュが破壊され、転送が停止します。最悪の場合、プロセス全体がディスクにキャッシュされる可能性があります。

  3. CPU-移動されるデータには長い道のりがあります:RAM->L2キャッシュ->L1キャッシュ->CPU-> L1->L2->RAM。L3キャッシュが存在する場合もあります。CPUを使用したい場合は、L1をコピーしながらL2をロードする必要があります。残念ながら、最近のCPUは、L1のロードにかかる時間よりも速くL1キャッシュブロックを実行できます。CPUにはメモリコントローラーがあり、データをCPUに順番にストリーミングする場合でも、問題が発生する可能性があります。

もちろん、何かをするためのより速い方法はそれをしないことです。キャプチャされたデータはRAMのどこにでも書き込むことができますか、それとも固定された場所で使用されるバッファです。どこにでも書けるのなら、memcpyはまったく必要ありません。修正された場合、データをその場で処理し、ダブルバッファタイプのシステムを使用できますか?つまり、データのキャプチャを開始し、半分がいっぱいになったら、データの前半の処理を開始します。バッファがいっぱいになったら、キャプチャしたデータの先頭への書き込みを開始し、後半を処理します。これには、アルゴリズムがキャプチャカードが生成するよりも速くデータを処理できる必要があります。また、データは処理後に破棄されることを前提としています。事実上、これはコピープロセスの一部として変換を伴うmemcpyであるため、次のようになります。

load -> transform -> save
\--/                 \--/
 capture card        RAM
   buffer

それ以外の:

load -> save -> load -> transform -> save
\-----------/
memcpy from
capture card
buffer to RAM

または、より高速なRAMを入手してください。

編集:別のオプションは、データソースとPCの間でデータを処理することです-そこにDSP / FPGAを入れることはできますか?カスタムハードウェアは、常に汎用CPUよりも高速です。

別の考え:高性能のグラフィックス処理を行ってからしばらく経ちましたが、データをグラフィックスカードにDMAしてから、もう一度DMAアウトできますか?CUDAを利用して処理の一部を実行することもできます。これにより、CPUがメモリ転送ループから完全に外れます。

于 2010-11-23T21:36:56.307 に答える
2

まず、メモリが16バイト境界に配置されていることを確認する必要があります。そうしないと、ペナルティが発生します。これが最も重要なことです。

標準に準拠したソリューションが必要ない場合は、次のようなコンパイラ固有の拡張機能を使用して、状況が改善するかどうかを確認できmemcpy64ます(利用可能なものがある場合は、コンパイラのドキュメントを確認してください)。事実は、memcpy1バイトのコピーを処理できる必要があるということですが、この制限がない場合は、一度に4バイトまたは8バイトを移動する方がはるかに高速です。

繰り返しますが、インラインアセンブリコードを作成するオプションはありますか?

于 2010-11-23T20:46:20.247 に答える
2

おそらく、より大きなメモリ領域をどのように処理しているかについてもう少し説明できますか?

アプリケーション内で、バッファをコピーするのではなく、単にバッファの所有権を渡すことは可能でしょうか?これにより、問題が完全に解消されます。

それとも、memcpyコピー以外の目的で使用していますか?おそらく、キャプチャしたものからデータのシーケンシャルストリームを構築するために、より広い領域のメモリを使用していますか?特に、一度に1文字ずつ処理している場合は、途中で会うことができる場合があります。たとえば、「連続メモリ領域」ではなく「バッファの配列」として表されるストリームに対応するように処理コードを調整できる場合があります。

于 2010-11-23T20:54:52.323 に答える
2

SSE2レジスタを使用してmemcpyのより良い実装を書くことができます。VC2010のバージョンはすでにこれを行っています。ですから、あなたがそれを整列したメモリを手渡しているのであれば、問題はもっとあります。

たぶん、VC 2010のバージョンよりもうまくやれるかもしれませんが、それを行う方法についてある程度理解する必要があります。

PS:コピーを完全に防ぐために、反転呼び出しでユーザーモードプログラムにバッファーを渡すことができます。

于 2010-11-23T21:26:47.800 に答える
1

私が読むことをお勧めする1つのソースは、MPlayerのfast_memcpy機能です。また、予想される使用パターンを考慮し、最新のCPUには、書き込んでいるデータを読み戻す必要があるかどうかをCPUに通知できる特別なストア命令があることに注意してください。データを読み戻さない(したがって、データをキャッシュする必要がない)ことを示す指示を使用すると、大規模なmemcpy操作にとって大きなメリットになります。

于 2010-11-23T21:36:16.790 に答える