web-dev-qa-db-ja.com

memcpyのパフォーマンスを向上させる方法

概要:

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

全詳細:

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

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

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

Visual C++ 2010:1900 MB /秒

NI CVI 2009:550 MB /秒

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

この状況でmemcpyを高速化するためにできることはありますか?


ハードウェアの詳細:AMD Magny Cours- 4xオクタルコア128 GB DDR3 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分があり、貢献したい場合は、上記のコードをマシンで実行し、コメントとして時間を投稿できますか?

47
leecbaker

この状況で速度を上げる方法を見つけました。 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;
}
32
leecbaker

実行時に実行するのか、コンパイル時に実行する必要があるのか​​はわかりませんが、SSEまたは同様の拡張機能を有効にしておく必要があります。 CPU用の64ビット。

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

ええ、そしてboth発信元と宛先が128ビットに揃えられていることを確認してください。ソースとデスティネーションが互いに対応していない場合、memcpy()は深刻な魔法をかけなければなりません。 :)

9
onemasse

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

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

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

  3. CPU-移動中のデータは長い道のりを持っています:RAM-> L2 Cache-> L1 Cache-> CPU-> L1-> L2-> RAM。 L3キャッシュ:L1のコピー中にL2をロードしたいCPUを使用する場合、残念ながら、最新のCPUはL1のロードにかかる時間よりも速くL1キャッシュブロックを実行できます。これらのケースでは、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 it CUDAを利用して処理の一部を実行することもでき、これによりCPUがメモリ転送ループから完全に抜け出します。

5
Skizz

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

留意すべきもう1つのことは、OSメモリの仮想化がここに影響を与える可能性があることです。コピー先のメモリページが実際に物理的なRAMページ、memcpy()操作は、その物理的なバッキングを取得するためにOSにフォールトします。DMAページは物理メモリにロックされる可能性があります(DMA操作)、したがってmemcpy()へのソースメモリはこの点で問題ではない可能性があります。Win32VirtualAlloc() AP​​Iを使用して宛先メモリを確保することを検討してください。 memcpy()がコミットされているため(VirtualAlloc()がこのための正しいAPIだと思いますが、私が忘れていたより良いAPIがあるかもしれません。このようなことをする必要があります)。

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

5
Michael Burr

SSE2レジスタを使用して、memcpyのより良い実装を記述できます。 VC2010のバージョンはすでにこれを行っています。したがって、メモリを調整して渡す場合、問題はさらに大きくなります。

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

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

2
Christopher

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

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

繰り返しますが、インラインアセンブリコードを記述することはオプションですか?

2
Simone

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

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

それとも、コピー以外にもmemcpyを使用していますか?おそらく、より大きなメモリ領域を使用して、キャプチャしたデータから連続したデータストリームを構築しているのでしょうか?特に、一度に1つのキャラクターを処理している場合は、途中で会うことができるかもしれません。たとえば、「連続メモリ領域」ではなく、「バッファの配列」として表されるストリームに対応するように処理コードを適合させることが可能かもしれません。

2
Stéphan Kochen

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

1
R..