17

以下に示すように、SSE 4.2 と AVX 2 を使用して 2 つのベクトル間の内積をベクトル化しました。コードは、-O2 最適化フラグを指定して GCC 4.8.4 でコンパイルされました。予想どおり、パフォーマンスは両方で改善されました (そして、SSE 4.2 よりも AVX 2 の方が高速でした) が、PAPI を使用してコードをプロファイリングしたところ、ミスの総数 (主に L1 と L2) が大幅に増加したことがわかりました。

ベクトル化なし:

PAPI_L1_TCM: 784,112,091
PAPI_L2_TCM: 195,315,365
PAPI_L3_TCM: 79,362

SSE 4.2 の場合:

PAPI_L1_TCM: 1,024,234,171
PAPI_L2_TCM: 311,541,918
PAPI_L3_TCM: 68,842

AVX 2 の場合:

PAPI_L1_TCM: 2,719,959,741
PAPI_L2_TCM: 1,459,375,105
PAPI_L3_TCM: 108,140

私のコードに何か問題があるのでしょうか、それともこの種の動作は正常ですか?

AVX2 コード:

double vec_dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;
    const int loopBound = n-3;

    __m256d vsum, vecPi, vecCi, vecQCi;

    vsum = _mm256_set1_pd(0);

    double * const pA = vecs.x+start_a ;
    double * const pB = vecs.x+start_b ;

    for( ; i<loopBound ;i+=4){
        vecPi  = _mm256_loadu_pd(&(pA)[i]);
        vecCi  = _mm256_loadu_pd(&(pB)[i]);
        vecQCi = _mm256_mul_pd(vecPi,vecCi);
        vsum   = _mm256_add_pd(vsum,vecQCi);
    }

    vsum = _mm256_hadd_pd(vsum, vsum);

    dot = ((double*)&vsum)[0] + ((double*)&vsum)[2];

    for( ; i<n; i++)
        dot += pA[i] * pB[i];

    return dot;
}

SSE 4.2 コード:

double vec_dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;

    const int loopBound = n-1;

    __m128d vsum, vecPi, vecCi, vecQCi;

    vsum = _mm_set1_pd(0);

    double * const pA = vecs.x+start_a ;
    double * const pB = vecs.x+start_b ;

    for( ; i<loopBound ;i+=2){
        vecPi  = _mm_load_pd(&(pA)[i]);
        vecCi  = _mm_load_pd(&(pB)[i]);
        vecQCi = _mm_mul_pd(vecPi,vecCi);
        vsum   = _mm_add_pd(vsum,vecQCi);
    }

    vsum = _mm_hadd_pd(vsum, vsum);

    _mm_storeh_pd(&dot, vsum);

    for( ; i<n; i++)
        dot += pA[i] * pB[i];

    return dot;
}

ベクトル化されていないコード:

double dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;

    for (i = 0; i < n; ++i)
    {
        dot += vecs.x[start_a+i] * vecs.x[start_b+i];
    }
    return dot;
}

編集:ベクトル化されていないコードのアセンブリ:

   0x000000000040f9e0 <+0>:     mov    (%rcx),%r8d
   0x000000000040f9e3 <+3>:     test   %r8d,%r8d
   0x000000000040f9e6 <+6>:     jle    0x40fa1d <dotProduct(vec const&, unsigned int const&, unsigned int const&, int const&)+61>
   0x000000000040f9e8 <+8>:     mov    (%rsi),%eax
   0x000000000040f9ea <+10>:    mov    (%rdi),%rcx
   0x000000000040f9ed <+13>:    mov    (%rdx),%edi
   0x000000000040f9ef <+15>:    vxorpd %xmm0,%xmm0,%xmm0
   0x000000000040f9f3 <+19>:    add    %eax,%r8d
   0x000000000040f9f6 <+22>:    sub    %eax,%edi
   0x000000000040f9f8 <+24>:    nopl   0x0(%rax,%rax,1)
   0x000000000040fa00 <+32>:    mov    %eax,%esi
   0x000000000040fa02 <+34>:    lea    (%rdi,%rax,1),%edx
   0x000000000040fa05 <+37>:    add    $0x1,%eax
   0x000000000040fa08 <+40>:    vmovsd (%rcx,%rsi,8),%xmm1
   0x000000000040fa0d <+45>:    cmp    %r8d,%eax
   0x000000000040fa10 <+48>:    vmulsd (%rcx,%rdx,8),%xmm1,%xmm1
   0x000000000040fa15 <+53>:    vaddsd %xmm1,%xmm0,%xmm0
   0x000000000040fa19 <+57>:    jne    0x40fa00 <dotProduct(vec const&, unsigned int const&, unsigned int const&, int const&)+32>
   0x000000000040fa1b <+59>:    repz retq 
   0x000000000040fa1d <+61>:    vxorpd %xmm0,%xmm0,%xmm0
   0x000000000040fa21 <+65>:    retq   

Edit2: 以下は、より大きな N (x ラベルの N と y ラベルの L1 キャッシュ ミス) のベクトル化されたコードとベクトル化されていないコードの間の L1 キャッシュ ミスの比較を見つけることができます。基本的に、より大きな N の場合、ベクトル化されたバージョンでは、ベクトル化されていないバージョンよりも多くのミスが発生します。

ここに画像の説明を入力

4

2 に答える 2

1

Rostislav は、コンパイラが自動ベクトル化を行っていること、および -O2 に関する GCC のドキュメントから:

"-O2 さらに最適化します。GCC は、スペースと速度のトレードオフを含まない、サポートされているほぼすべての最適化を実行します。" (ここから: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html )

-O2 フラグを指定した GCC は、コード サイズや速度を優先せずに、最も効率的なコードを生成しようとします。

したがって、CPU サイクルに関しては、-O2 自動ベクトル化コードは実行に必要なワット数が最も少なくなりますが、最速または最小のコードにはなりません。これは、モバイル デバイスやマルチユーザー システムで実行されるコードに最適なケースであり、これらは C++ の優先的な使用法となる傾向があります。使用するワット数に関係なく絶対最大速度が必要な場合は、GCC のバージョンがサポートされている場合は -O3 または -Ofast を試すか、手動で最適化されたより高速なソリューションを使用してください。

この原因は、おそらく 2 つの要因の組み合わせです。

まず、高速なコードは、同じ時間内にメモリ/キャッシュに対してより多くのリクエストを生成します。これにより、プリフェッチ予測アルゴリズムに負荷がかかります。L1 キャッシュはそれほど大きくなく、通常は 1MB から 3MB であり、その CPU コアで実行中のすべてのプロセス間で共有されるため、CPU コアは、以前にプリフェッチされたブロックが使用されなくなるまでプリフェッチできません。コードがより高速に実行されている場合、ブロック間でプリフェッチする時間が短くなり、効果的にパイプライン処理を行うコードでは、保留中のフェッチが完了するまで CPU コアが完全に停止する前に、より多くのキャッシュ ミスが実行されます。

次に、最新のオペレーティング システムは通常、複数のコア間で余分なキャッシュを利用するために、スレッド アフィニティを動的に調整することで、シングル スレッド プロセスを複数のコアに分割します。ただし、コードを並行して実行することはできません (例: コア 0 を埋める)。データをキャッシュしてから、コア 1 のキャッシュを埋めながら実行し、コア 0 のキャッシュを補充しながらコア 1 で実行し、完了するまでラウンドロビンします。この疑似並列処理により、シングルスレッド プロセスの全体的な速度が向上し、キャッシュ ミスが大幅に削減されますが、非常に特殊な状況でしか実行できません... 優れたコンパイラが可能な限りコードを生成する特定の状況です。

于 2016-01-28T05:39:08.230 に答える