web-dev-qa-db-ja.com

SSEコピー、AVXコピー、およびstd :: copyのパフォーマンス

SSEおよびAVX:を介してコピー操作のパフォーマンスを改善しようとしています。

    #include <immintrin.h>

    const int sz = 1024;
    float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
    float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
    float a=0;
    std::generate(mas, mas+sz, [&](){return ++a;});

    const int nn = 1000;//Number of iteration in tester loops    
    std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3; 

    //std::copy testing
    start1 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
        std::copy(mas, mas+sz, tar);
    end1 = std::chrono::system_clock::now();
    float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();

    //SSE-copy testing
    start2 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=4, _tar+=4)
        {
           __m128 buffer = _mm_load_ps(_mas);
           _mm_store_ps(_tar, buffer);
        }
    }
    end2 = std::chrono::system_clock::now();
    float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();

    //AVX-copy testing
    start3 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=8, _tar+=8)
        {
           __m256 buffer = _mm256_load_ps(_mas);
           _mm256_store_ps(_tar, buffer);
        }
    }
    end3 = std::chrono::system_clock::now();
    float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();

    std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;

    _mm_free(mas);
    _mm_free(tar);

できます。ただし、テスターループの反復回数-nn-は増加しますが、simd-copyのパフォーマンスの向上は減少します。

nn = 10:SSE-ゲイン= 3、AVX-ゲイン= 6;

nn = 100:SSE-ゲイン= 0.75、AVX-ゲイン= 1.5;

nn = 1000:SSE-ゲイン= 0.55、AVX-ゲイン= 1.1;

上記のパフォーマンス低下効果の理由を誰かが説明できますか?コピー操作を手動でベクトル化することをお勧めしますか?

19
gorill

問題は、ベンチマークを困難にするハードウェアのいくつかの要因を移行するために、テストがうまく機能しないことです。これをテストするために、私は独自のテストケースを作成しました。このようなもの:

for blah blah:
    sleep(500ms)
    std::copy
    sse
    axv

出力:

SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy

したがって、この場合、AVXはstd::copyよりも高速です。テストケースをに変更するとどうなりますか。

for blah blah:
    sleep(500ms)
    sse
    axv
    std::copy

テストの順序を除いて、まったく何も変わっていないことに注意してください。

SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy

うわー!そんなことがあるものか? CPUがフルスピードに達するまでには時間がかかるため、後で実行されるテストには利点があります。この質問には、「承認済み」の回答を含む3つの回答があります。しかし、賛成票の数が最も少ないものだけが正しい方向に進んでいました。

これがベンチマークが難しい理由の1つであり、セットアップの詳細情報が含まれていない限り、誰かのマイクロベンチマークを信頼してはいけません。うまくいかないのはコードだけではありません。省電力機能と奇妙なドライバーは、ベンチマークを完全に台無しにする可能性があります。あるとき、ノートブックの1%未満が提供するBIOSのスイッチを切り替えることで、パフォーマンスの7倍の違いを測定しました。

23
Stefan

これは非常に興味深い質問ですが、質問自体が非常に誤解を招くため、これまでの回答はどれも正しくないと思います。

タイトルを「理論上のメモリI/O帯域幅に到達するにはどうすればよいですか?」に変更する必要があります。

どの命令セットを使用しても、CPUはRAMよりもはるかに高速であるため、純粋なブロックメモリコピーは100%I/Oに制限されます。そしてこれは、SSEとAVXのパフォーマンスにほとんど違いがない理由を説明しています。

L1Dキャッシュでホットな小さなバッファの場合、AVXは、256bロード/ストアが2つの128b操作に分割する代わりにL1Dキャッシュへの256bデータパスを実際に使用するHaswellのようなCPUでSSEよりも大幅に高速にコピーできます。

皮肉なことに、古代のX86命令rep stosqは、SSEおよびAVXよりもはるかに優れたパフォーマンスを発揮しますメモリコピーの観点から!

ここの記事 メモリ帯域幅を本当にうまく飽和させる方法を説明し、さらに探求するための豊富なリファレンスもあります。

参照 memcpyの拡張REP MOVSB ここSOで、@ BeeOnRopeの回答では、NTストア(およびrep stosb/stosqによって実行される非RFOストア)と通常のストア、およびシングルコアメモリについて説明しています。多くの場合、帯域幅は、メモリコントローラ自体ではなく、最大同時実行性/遅延によって制限されます。

6
PhD AP EcE

高速な書き込みSSEは、同等の非並列演算の代わりにSSE演算を使用するほど簡単ではありません。この場合、コンパイラは負荷を有効に展開できないと思われます/ storeペアとあなたの時間は、次の命令(ストア)で1つの低スループット操作(ロード)の出力を使用することによって引き起こされるストールによって支配されます。

このアイデアは、1つのノッチを手動で展開することでテストできます。

//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
    auto _mas = mas;
    auto _tar = tar;
    for(; _mas!=mas+sz; _mas+=8, _tar+=8)
    {
       __m128 buffer1 = _mm_load_ps(_mas);
       __m128 buffer2 = _mm_load_ps(_mas+4);
       _mm_store_ps(_tar, buffer1);
       _mm_store_ps(_tar+4, buffer2);
    }
}

通常、組み込み関数を使用するときは、出力を逆アセンブルして、何もおかしなことが起こっていないことを確認します(これを試して、元のループが展開されたかどうか/どのように展開されたかを確認できます)。より複雑なループの場合、使用する適切なツールは Intel Architecture Code Analyzer(IACA) です。これは、「パイプラインストールがあります」などの情報を提供できる静的分析ツールです。

3
Ben Jackson

これは、ちょっと短い操作では測定が正確ではないためだと思います。

IntelCPUでパフォーマンスを測定する場合

  1. 「ターボブースト」と「SpeedStep」を無効にします。これは、システムBIOSで実行できます。

  2. プロセス/スレッドの優先度を高またはリアルタイムに変更します。これにより、スレッドが実行され続けます。

  3. プロセスCPUマスクを1つのコアのみに設定します。優先度の高いCPUマスキングは、コンテキストの切り替えを最小限に抑えます。

  4. __rdtsc()組み込み関数を使用します。 Intel Coreシリーズは、__ rdtsc()を使用してCPU内部クロックカウンターを返します。 3.4GhzCPUから3400000000カウント/秒を取得します。また、__ rdtsc()は、CPUでスケジュールされたすべての操作をフラッシュするため、タイミングをより正確に測定できます。

これは、SSE/AVXコードをテストするためのテストベッド起動コードです。

    int GetMSB(DWORD_PTR dwordPtr)
    {
        if(dwordPtr)
        {
            int result = 1;
    #if defined(_WIN64)
            if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
            if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
            if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00FF00FF00; }
            if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
    #else
            if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
            if(dwordPtr & 0xFF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00; }
            if(dwordPtr & 0xF0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAA) { result += 1; }
    #endif
            return result;
        }
        else
        {
            return 0;
        }
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
        // Set Core Affinity
        DWORD_PTR processMask, systemMask;
        GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
        SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );

        // Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
        SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

        DWORD64 start, end;
        start = __rdtsc();
    // your code here.
        end = __rdtsc();
        printf("%I64d\n", end - start);
        return 0;
    }
3
zupet

あなたの主な問題/ボトルネックはあなたの _mm_malloc だと思います。

C++の局所性が心配な場合は、メインのデータ構造としてstd::vectorを使用することを強くお勧めします。

組み込み関数は、正確には「ライブラリ」ではなく、builtinに似ています。 )コンパイラから提供される関数。この関数を使用する前に、コンパイラの内部/ドキュメントに精通している必要があります。

また、AVXSSEよりも新しいという事実は、使用する予定が何であれ、AVXを高速化するわけではないことに注意してください。関数はおそらく「avxvssse」引数よりも重要です。たとえば、 この回答 を参照してください。

POD int array[]またはstd::vectorで試してください。

0
user2485710