サイクルあたりの4つのFLOPの理論的な最大値を達成するにはどうすればよいですか?


642

最新のx86-64 Intel CPUで、サイクルあたり4つの浮動小数点演算(倍精度)の理論上のピークパフォーマンスをどのように達成できますか?

私が理解している限り、最新のIntel CPUのほとんどでSSE が完了addするmulまでに3サイクル、が完了するまでに5サイクルかかります(たとえば、Agner Fogの「Instruction Tables」を参照)。パイプライン化によりadd、アルゴリズムに少なくとも3つの独立した合計がある場合、1サイクルあたり1のスループットが得られます。これは、パックaddpdされたaddsdバージョンとスカラーバージョンおよびSSEレジスターに2を含めることができるため当てはまるためdouble、スループットはサイクルあたり2フロップと同じくらい高くなる可能性があります。

さらに、(これに関する適切なドキュメントを見たことはありませんが)addmulは並行して実行でき、サイクルあたり4フロップの理論的な最大スループットを実現できます。

ただし、単純なC / C ++プログラムではそのパフォーマンスを再現できませんでした。私の最善の試みは約2.7フロップ/サイクルをもたらしました。ピークパフォーマンスを実証する単純なC / C ++またはアセンブラープログラムを提供できる人がいれば、高く評価されます。

私の試み:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>

double stoptime(void) {
   struct timeval t;
   gettimeofday(&t,NULL);
   return (double) t.tv_sec + t.tv_usec/1000000.0;
}

double addmul(double add, double mul, int ops){
   // Need to initialise differently otherwise compiler might optimise away
   double sum1=0.1, sum2=-0.1, sum3=0.2, sum4=-0.2, sum5=0.0;
   double mul1=1.0, mul2= 1.1, mul3=1.2, mul4= 1.3, mul5=1.4;
   int loops=ops/10;          // We have 10 floating point operations inside the loop
   double expected = 5.0*add*loops + (sum1+sum2+sum3+sum4+sum5)
               + pow(mul,loops)*(mul1+mul2+mul3+mul4+mul5);

   for (int i=0; i<loops; i++) {
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
   }
   return  sum1+sum2+sum3+sum4+sum5+mul1+mul2+mul3+mul4+mul5 - expected;
}

int main(int argc, char** argv) {
   if (argc != 2) {
      printf("usage: %s <num>\n", argv[0]);
      printf("number of operations: <num> millions\n");
      exit(EXIT_FAILURE);
   }
   int n = atoi(argv[1]) * 1000000;
   if (n<=0)
       n=1000;

   double x = M_PI;
   double y = 1.0 + 1e-8;
   double t = stoptime();
   x = addmul(x, y, n);
   t = stoptime() - t;
   printf("addmul:\t %.3f s, %.3f Gflops, res=%f\n", t, (double)n/t/1e9, x);
   return EXIT_SUCCESS;
}

でコンパイル

g++ -O2 -march=native addmul.cpp ; ./a.out 1000

Intel Core i5-750、2.66 GHzで次の出力を生成します。

addmul:  0.270 s, 3.707 Gflops, res=1.326463

つまり、サイクルあたり約1.4フロップです。g++ -S -O2 -march=native -masm=intel addmul.cppメインループでアセンブラコードを見るのは、 私にとっては一種の最適のようです。

.L4:
inc    eax
mulsd    xmm8, xmm3
mulsd    xmm7, xmm3
mulsd    xmm6, xmm3
mulsd    xmm5, xmm3
mulsd    xmm1, xmm3
addsd    xmm13, xmm2
addsd    xmm12, xmm2
addsd    xmm11, xmm2
addsd    xmm10, xmm2
addsd    xmm9, xmm2
cmp    eax, ebx
jne    .L4

スカラーバージョンをパックバージョンで変更する(addpdおよびmulpd)に変更すると、実行時間を変更せずにフロップカウントが2倍になるため、サイクルあたりのフロップ数は2.8になりません。サイクルごとに4フロップを達成する簡単な例はありますか?

Mysticialによる素敵な小さなプログラム。これが私の結果です(ただし、数秒間実行します):

  • gcc -O2 -march=nocona:10.66 Gフロップのうち5.6 Gフロップ(2.1フロップ/サイクル)
  • cl /O2、openmpの削除:10.66 Gフロップのうち10.1 Gフロップ(3.8フロップ/サイクル)

それはすべて少し複雑に見えますが、これまでの私の結論:

  • gcc -O2可能であれば交互にaddpdandを目的として独立した浮動小数点演算の順序を変更し mulpdます。同じことがに適用されgcc-4.6.2 -O2 -march=core2ます。

  • gcc -O2 -march=nocona C ++ソースで定義されている浮動小数点演算の順序を維持しているようです。

  • cl /O2SDK for Windows 7の64ビットコンパイラは 自動的にループアンロールを実行し、3 addpdのグループが3 のグループと交互になるように操作を調整しようとしているようですmulpd(少なくとも、私のシステムと単純なプログラムでは) 。

  • 私のCore i5 750Nehalemアーキテクチャ)は、addとmulを交互に使用するのが好きではなく、両方の操作を並行して実行できないようです。ただし、3にグループ化すると、突然魔法のように機能します。

  • 他のアーキテクチャ(Sandy Bridgeなど)は、アセンブリコードで交互に入れ替えても、問題なくadd / mulを並行して実行できるようです。

  • 認めるのは難しいですが、私のシステムでcl /O2は、システムの低レベルの最適化操作ではるかに優れた仕事をし、上記の小さなC ++の例でピークに近いパフォーマンスを達成しています。私は1.85-2.01フロップ/サイクルの間で測定しました(Windowsではclock()を使用しましたが、それほど正確ではありません。より良いタイマーを使用する必要があります-Mackie Messerに感謝します)。

  • 私がうまく管理できたのgccは、手動でループを展開し、3つのグループで加算と乗算を調整することでした。 g++ -O2 -march=nocona addmul_unroll.cpp 私は最高の状態で入手0.207s, 4.825 Gflops1.8に対応していることはプ/私は今ではかなり満足しているサイクルを。

C ++コードでforループを次のように置き換えました

   for (int i=0; i<loops/3; i++) {
       mul1*=mul; mul2*=mul; mul3*=mul;
       sum1+=add; sum2+=add; sum3+=add;
       mul4*=mul; mul5*=mul; mul1*=mul;
       sum4+=add; sum5+=add; sum1+=add;

       mul2*=mul; mul3*=mul; mul4*=mul;
       sum2+=add; sum3+=add; sum4+=add;
       mul5*=mul; mul1*=mul; mul2*=mul;
       sum5+=add; sum1+=add; sum2+=add;

       mul3*=mul; mul4*=mul; mul5*=mul;
       sum3+=add; sum4+=add; sum5+=add;
   }

アセンブリは次のようになります

.L4:
mulsd    xmm8, xmm3
mulsd    xmm7, xmm3
mulsd    xmm6, xmm3
addsd    xmm13, xmm2
addsd    xmm12, xmm2
addsd    xmm11, xmm2
mulsd    xmm5, xmm3
mulsd    xmm1, xmm3
mulsd    xmm8, xmm3
addsd    xmm10, xmm2
addsd    xmm9, xmm2
addsd    xmm13, xmm2
...

15
壁時計時間に依存していることが原因の可能性があります。LinuxのようなOSの内部でこれを実行している場合、いつでもプロセスのスケジュールを自由に変更できます。この種の外部イベントは、パフォーマンス測定に影響を与える可能性があります。
tdenniston 2011

GCCのバージョンは何ですか?デフォルトを使用しているMacを使用している場合、問題が発生します(古い4.2です)。
semisight

2
はい、Linuxを実行していますが、システムに負荷はなく、何度も繰り返してもほとんど違いはありません(たとえば、スカラーバージョンの範囲は4.0〜4.2 Gフロップですが、現在は-funroll-loops)。gccバージョン4.4.1および4.6.2で試してみましたが、asm出力は大丈夫ですか?
user1059432

-O3有効にするgcc を試しました-ftree-vectorizeか?-funroll-loops本当に必要な場合はそうしませんが、組み合わせるかもしれません。結局のところ、一方のコンパイラーがベクトル化/アンロールを行う場合、比較は一種の不公平に思えますが、もう一方のコンパイラーは、そうすることができないわけではなく、そうではないためです。
Grizzly

4
@Grizzly -funroll-loopsはおそらく試すべきものです。しかし、私-ftree-vectorizeはポイントの外にあると思います。OPは、1 mul + 1 add命令/サイクルを維持しようとしています。命令はスカラーまたはベクトルにすることができます。レイテンシとスループットは同じであるため、問題ではありません。したがって、スカラーSSEで2 /サイクルを維持できる場合、それらをベクトルSSEで置き換えると、4フロップ/サイクルを達成できます。私の答えでは、SSE-> AVXから行っただけです。すべてのSSEをAVXに置き換えました-同じレイテンシ、同じスループット、2倍のフロップ。
Mysticial 2012年

回答:


517

私は以前にこの正確なタスクを実行しました。しかし、それは主に消費電力とCPU温度を測定することでした。次のコード(かなり長い)は、Core i7 2600Kでほぼ最適に達しています。

ここで注意すべき重要な点は、大量の手動ループ展開と乗算と加算のインターリーブです...

完全なプロジェクトは私のGitHubにあります:https : //github.com/Mysticial/Flops

警告:

これをコンパイルして実行する場合は、CPUの温度に注意してください!!!
過熱しないように注意してください。そして、CPUスロットリングが結果に影響しないことを確認してください!

さらに、私はこのコードを実行することにより生じるいかなる損害についても責任を負いません。

ノート:

  • このコードはx64用に最適化されています。x86には、これを適切にコンパイルするための十分なレジスタがありません。
  • このコードは、Visual Studio 2010/2012およびGCC 4.6で正常に機能することがテストされています。
    ICC 11(Intel Compiler 11)は、驚くほどうまくコンパイルできません。
  • これらは、FMA以前のプロセッサー用です。Intel HaswellおよびAMD Bulldozerプロセッサ(以降)でピークFLOPSを実現するには、FMA(Fused Multiply Add)命令が必要です。これらは、このベンチマークの範囲を超えています。

#include <emmintrin.h>
#include <omp.h>
#include <iostream>
using namespace std;

typedef unsigned long long uint64;

double test_dp_mac_SSE(double x,double y,uint64 iterations){
    register __m128d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF;

    //  Generate starting data.
    r0 = _mm_set1_pd(x);
    r1 = _mm_set1_pd(y);

    r8 = _mm_set1_pd(-0.0);

    r2 = _mm_xor_pd(r0,r8);
    r3 = _mm_or_pd(r0,r8);
    r4 = _mm_andnot_pd(r8,r0);
    r5 = _mm_mul_pd(r1,_mm_set1_pd(0.37796447300922722721));
    r6 = _mm_mul_pd(r1,_mm_set1_pd(0.24253562503633297352));
    r7 = _mm_mul_pd(r1,_mm_set1_pd(4.1231056256176605498));
    r8 = _mm_add_pd(r0,_mm_set1_pd(0.37796447300922722721));
    r9 = _mm_add_pd(r1,_mm_set1_pd(0.24253562503633297352));
    rA = _mm_sub_pd(r0,_mm_set1_pd(4.1231056256176605498));
    rB = _mm_sub_pd(r1,_mm_set1_pd(4.1231056256176605498));

    rC = _mm_set1_pd(1.4142135623730950488);
    rD = _mm_set1_pd(1.7320508075688772935);
    rE = _mm_set1_pd(0.57735026918962576451);
    rF = _mm_set1_pd(0.70710678118654752440);

    uint64 iMASK = 0x800fffffffffffffull;
    __m128d MASK = _mm_set1_pd(*(double*)&iMASK);
    __m128d vONE = _mm_set1_pd(1.0);

    uint64 c = 0;
    while (c < iterations){
        size_t i = 0;
        while (i < 1000){
            //  Here's the meat - the part that really matters.

            r0 = _mm_mul_pd(r0,rC);
            r1 = _mm_add_pd(r1,rD);
            r2 = _mm_mul_pd(r2,rE);
            r3 = _mm_sub_pd(r3,rF);
            r4 = _mm_mul_pd(r4,rC);
            r5 = _mm_add_pd(r5,rD);
            r6 = _mm_mul_pd(r6,rE);
            r7 = _mm_sub_pd(r7,rF);
            r8 = _mm_mul_pd(r8,rC);
            r9 = _mm_add_pd(r9,rD);
            rA = _mm_mul_pd(rA,rE);
            rB = _mm_sub_pd(rB,rF);

            r0 = _mm_add_pd(r0,rF);
            r1 = _mm_mul_pd(r1,rE);
            r2 = _mm_sub_pd(r2,rD);
            r3 = _mm_mul_pd(r3,rC);
            r4 = _mm_add_pd(r4,rF);
            r5 = _mm_mul_pd(r5,rE);
            r6 = _mm_sub_pd(r6,rD);
            r7 = _mm_mul_pd(r7,rC);
            r8 = _mm_add_pd(r8,rF);
            r9 = _mm_mul_pd(r9,rE);
            rA = _mm_sub_pd(rA,rD);
            rB = _mm_mul_pd(rB,rC);

            r0 = _mm_mul_pd(r0,rC);
            r1 = _mm_add_pd(r1,rD);
            r2 = _mm_mul_pd(r2,rE);
            r3 = _mm_sub_pd(r3,rF);
            r4 = _mm_mul_pd(r4,rC);
            r5 = _mm_add_pd(r5,rD);
            r6 = _mm_mul_pd(r6,rE);
            r7 = _mm_sub_pd(r7,rF);
            r8 = _mm_mul_pd(r8,rC);
            r9 = _mm_add_pd(r9,rD);
            rA = _mm_mul_pd(rA,rE);
            rB = _mm_sub_pd(rB,rF);

            r0 = _mm_add_pd(r0,rF);
            r1 = _mm_mul_pd(r1,rE);
            r2 = _mm_sub_pd(r2,rD);
            r3 = _mm_mul_pd(r3,rC);
            r4 = _mm_add_pd(r4,rF);
            r5 = _mm_mul_pd(r5,rE);
            r6 = _mm_sub_pd(r6,rD);
            r7 = _mm_mul_pd(r7,rC);
            r8 = _mm_add_pd(r8,rF);
            r9 = _mm_mul_pd(r9,rE);
            rA = _mm_sub_pd(rA,rD);
            rB = _mm_mul_pd(rB,rC);

            i++;
        }

        //  Need to renormalize to prevent denormal/overflow.
        r0 = _mm_and_pd(r0,MASK);
        r1 = _mm_and_pd(r1,MASK);
        r2 = _mm_and_pd(r2,MASK);
        r3 = _mm_and_pd(r3,MASK);
        r4 = _mm_and_pd(r4,MASK);
        r5 = _mm_and_pd(r5,MASK);
        r6 = _mm_and_pd(r6,MASK);
        r7 = _mm_and_pd(r7,MASK);
        r8 = _mm_and_pd(r8,MASK);
        r9 = _mm_and_pd(r9,MASK);
        rA = _mm_and_pd(rA,MASK);
        rB = _mm_and_pd(rB,MASK);
        r0 = _mm_or_pd(r0,vONE);
        r1 = _mm_or_pd(r1,vONE);
        r2 = _mm_or_pd(r2,vONE);
        r3 = _mm_or_pd(r3,vONE);
        r4 = _mm_or_pd(r4,vONE);
        r5 = _mm_or_pd(r5,vONE);
        r6 = _mm_or_pd(r6,vONE);
        r7 = _mm_or_pd(r7,vONE);
        r8 = _mm_or_pd(r8,vONE);
        r9 = _mm_or_pd(r9,vONE);
        rA = _mm_or_pd(rA,vONE);
        rB = _mm_or_pd(rB,vONE);

        c++;
    }

    r0 = _mm_add_pd(r0,r1);
    r2 = _mm_add_pd(r2,r3);
    r4 = _mm_add_pd(r4,r5);
    r6 = _mm_add_pd(r6,r7);
    r8 = _mm_add_pd(r8,r9);
    rA = _mm_add_pd(rA,rB);

    r0 = _mm_add_pd(r0,r2);
    r4 = _mm_add_pd(r4,r6);
    r8 = _mm_add_pd(r8,rA);

    r0 = _mm_add_pd(r0,r4);
    r0 = _mm_add_pd(r0,r8);


    //  Prevent Dead Code Elimination
    double out = 0;
    __m128d temp = r0;
    out += ((double*)&temp)[0];
    out += ((double*)&temp)[1];

    return out;
}

void test_dp_mac_SSE(int tds,uint64 iterations){

    double *sum = (double*)malloc(tds * sizeof(double));
    double start = omp_get_wtime();

#pragma omp parallel num_threads(tds)
    {
        double ret = test_dp_mac_SSE(1.1,2.1,iterations);
        sum[omp_get_thread_num()] = ret;
    }

    double secs = omp_get_wtime() - start;
    uint64 ops = 48 * 1000 * iterations * tds * 2;
    cout << "Seconds = " << secs << endl;
    cout << "FP Ops  = " << ops << endl;
    cout << "FLOPs   = " << ops / secs << endl;

    double out = 0;
    int c = 0;
    while (c < tds){
        out += sum[c++];
    }

    cout << "sum = " << out << endl;
    cout << endl;

    free(sum);
}

int main(){
    //  (threads, iterations)
    test_dp_mac_SSE(8,10000000);

    system("pause");
}

出力(1スレッド、10000000回の反復)-Visual Studio 2010 SP1でコンパイル-x64リリース:

Seconds = 55.5104
FP Ops  = 960000000000
FLOPs   = 1.7294e+010
sum = 2.22652

マシンはCore i7 2600K @ 4.4 GHzです。理論上のSSEピークは4フロップ* 4.4 GHz = 17.6 GFlopsです。このコードは17.3 GFlopsを達成します -悪くありません。

出力(8スレッド、10000000回の反復)-Visual Studio 2010 SP1でコンパイル-x64リリース:

Seconds = 117.202
FP Ops  = 7680000000000
FLOPs   = 6.55279e+010
sum = 17.8122

理論上のSSEピークは、4フロップ* 4コア* 4.4 GHz = 70.4 GFlopsです。実際は65.5 GFlopsです。


これをさらに一歩進めましょう。AVX ...

#include <immintrin.h>
#include <omp.h>
#include <iostream>
using namespace std;

typedef unsigned long long uint64;

double test_dp_mac_AVX(double x,double y,uint64 iterations){
    register __m256d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF;

    //  Generate starting data.
    r0 = _mm256_set1_pd(x);
    r1 = _mm256_set1_pd(y);

    r8 = _mm256_set1_pd(-0.0);

    r2 = _mm256_xor_pd(r0,r8);
    r3 = _mm256_or_pd(r0,r8);
    r4 = _mm256_andnot_pd(r8,r0);
    r5 = _mm256_mul_pd(r1,_mm256_set1_pd(0.37796447300922722721));
    r6 = _mm256_mul_pd(r1,_mm256_set1_pd(0.24253562503633297352));
    r7 = _mm256_mul_pd(r1,_mm256_set1_pd(4.1231056256176605498));
    r8 = _mm256_add_pd(r0,_mm256_set1_pd(0.37796447300922722721));
    r9 = _mm256_add_pd(r1,_mm256_set1_pd(0.24253562503633297352));
    rA = _mm256_sub_pd(r0,_mm256_set1_pd(4.1231056256176605498));
    rB = _mm256_sub_pd(r1,_mm256_set1_pd(4.1231056256176605498));

    rC = _mm256_set1_pd(1.4142135623730950488);
    rD = _mm256_set1_pd(1.7320508075688772935);
    rE = _mm256_set1_pd(0.57735026918962576451);
    rF = _mm256_set1_pd(0.70710678118654752440);

    uint64 iMASK = 0x800fffffffffffffull;
    __m256d MASK = _mm256_set1_pd(*(double*)&iMASK);
    __m256d vONE = _mm256_set1_pd(1.0);

    uint64 c = 0;
    while (c < iterations){
        size_t i = 0;
        while (i < 1000){
            //  Here's the meat - the part that really matters.

            r0 = _mm256_mul_pd(r0,rC);
            r1 = _mm256_add_pd(r1,rD);
            r2 = _mm256_mul_pd(r2,rE);
            r3 = _mm256_sub_pd(r3,rF);
            r4 = _mm256_mul_pd(r4,rC);
            r5 = _mm256_add_pd(r5,rD);
            r6 = _mm256_mul_pd(r6,rE);
            r7 = _mm256_sub_pd(r7,rF);
            r8 = _mm256_mul_pd(r8,rC);
            r9 = _mm256_add_pd(r9,rD);
            rA = _mm256_mul_pd(rA,rE);
            rB = _mm256_sub_pd(rB,rF);

            r0 = _mm256_add_pd(r0,rF);
            r1 = _mm256_mul_pd(r1,rE);
            r2 = _mm256_sub_pd(r2,rD);
            r3 = _mm256_mul_pd(r3,rC);
            r4 = _mm256_add_pd(r4,rF);
            r5 = _mm256_mul_pd(r5,rE);
            r6 = _mm256_sub_pd(r6,rD);
            r7 = _mm256_mul_pd(r7,rC);
            r8 = _mm256_add_pd(r8,rF);
            r9 = _mm256_mul_pd(r9,rE);
            rA = _mm256_sub_pd(rA,rD);
            rB = _mm256_mul_pd(rB,rC);

            r0 = _mm256_mul_pd(r0,rC);
            r1 = _mm256_add_pd(r1,rD);
            r2 = _mm256_mul_pd(r2,rE);
            r3 = _mm256_sub_pd(r3,rF);
            r4 = _mm256_mul_pd(r4,rC);
            r5 = _mm256_add_pd(r5,rD);
            r6 = _mm256_mul_pd(r6,rE);
            r7 = _mm256_sub_pd(r7,rF);
            r8 = _mm256_mul_pd(r8,rC);
            r9 = _mm256_add_pd(r9,rD);
            rA = _mm256_mul_pd(rA,rE);
            rB = _mm256_sub_pd(rB,rF);

            r0 = _mm256_add_pd(r0,rF);
            r1 = _mm256_mul_pd(r1,rE);
            r2 = _mm256_sub_pd(r2,rD);
            r3 = _mm256_mul_pd(r3,rC);
            r4 = _mm256_add_pd(r4,rF);
            r5 = _mm256_mul_pd(r5,rE);
            r6 = _mm256_sub_pd(r6,rD);
            r7 = _mm256_mul_pd(r7,rC);
            r8 = _mm256_add_pd(r8,rF);
            r9 = _mm256_mul_pd(r9,rE);
            rA = _mm256_sub_pd(rA,rD);
            rB = _mm256_mul_pd(rB,rC);

            i++;
        }

        //  Need to renormalize to prevent denormal/overflow.
        r0 = _mm256_and_pd(r0,MASK);
        r1 = _mm256_and_pd(r1,MASK);
        r2 = _mm256_and_pd(r2,MASK);
        r3 = _mm256_and_pd(r3,MASK);
        r4 = _mm256_and_pd(r4,MASK);
        r5 = _mm256_and_pd(r5,MASK);
        r6 = _mm256_and_pd(r6,MASK);
        r7 = _mm256_and_pd(r7,MASK);
        r8 = _mm256_and_pd(r8,MASK);
        r9 = _mm256_and_pd(r9,MASK);
        rA = _mm256_and_pd(rA,MASK);
        rB = _mm256_and_pd(rB,MASK);
        r0 = _mm256_or_pd(r0,vONE);
        r1 = _mm256_or_pd(r1,vONE);
        r2 = _mm256_or_pd(r2,vONE);
        r3 = _mm256_or_pd(r3,vONE);
        r4 = _mm256_or_pd(r4,vONE);
        r5 = _mm256_or_pd(r5,vONE);
        r6 = _mm256_or_pd(r6,vONE);
        r7 = _mm256_or_pd(r7,vONE);
        r8 = _mm256_or_pd(r8,vONE);
        r9 = _mm256_or_pd(r9,vONE);
        rA = _mm256_or_pd(rA,vONE);
        rB = _mm256_or_pd(rB,vONE);

        c++;
    }

    r0 = _mm256_add_pd(r0,r1);
    r2 = _mm256_add_pd(r2,r3);
    r4 = _mm256_add_pd(r4,r5);
    r6 = _mm256_add_pd(r6,r7);
    r8 = _mm256_add_pd(r8,r9);
    rA = _mm256_add_pd(rA,rB);

    r0 = _mm256_add_pd(r0,r2);
    r4 = _mm256_add_pd(r4,r6);
    r8 = _mm256_add_pd(r8,rA);

    r0 = _mm256_add_pd(r0,r4);
    r0 = _mm256_add_pd(r0,r8);

    //  Prevent Dead Code Elimination
    double out = 0;
    __m256d temp = r0;
    out += ((double*)&temp)[0];
    out += ((double*)&temp)[1];
    out += ((double*)&temp)[2];
    out += ((double*)&temp)[3];

    return out;
}

void test_dp_mac_AVX(int tds,uint64 iterations){

    double *sum = (double*)malloc(tds * sizeof(double));
    double start = omp_get_wtime();

#pragma omp parallel num_threads(tds)
    {
        double ret = test_dp_mac_AVX(1.1,2.1,iterations);
        sum[omp_get_thread_num()] = ret;
    }

    double secs = omp_get_wtime() - start;
    uint64 ops = 48 * 1000 * iterations * tds * 4;
    cout << "Seconds = " << secs << endl;
    cout << "FP Ops  = " << ops << endl;
    cout << "FLOPs   = " << ops / secs << endl;

    double out = 0;
    int c = 0;
    while (c < tds){
        out += sum[c++];
    }

    cout << "sum = " << out << endl;
    cout << endl;

    free(sum);
}

int main(){
    //  (threads, iterations)
    test_dp_mac_AVX(8,10000000);

    system("pause");
}

出力(1スレッド、10000000回の反復)-Visual Studio 2010 SP1でコンパイル-x64リリース:

Seconds = 57.4679
FP Ops  = 1920000000000
FLOPs   = 3.34099e+010
sum = 4.45305

理論上のAVXピークは8フロップ* 4.4 GHz = 35.2 GFlopsです。実際は33.4 GFlopsです。

出力(8スレッド、10000000回の反復)-Visual Studio 2010 SP1でコンパイル-x64リリース:

Seconds = 111.119
FP Ops  = 15360000000000
FLOPs   = 1.3823e+011
sum = 35.6244

理論上のAVXピークは、8フロップ* 4コア* 4.4 GHz = 140.8 GFlopsです。実際は138.2 GFlopsです。


いくつかの説明のために:

パフォーマンスが重要な部分は、明らかに内部ループ内の48命令です。それぞれが12命令の4つのブロックに分割されていることに気づくでしょう。これらの12命令ブロックはそれぞれ完全に独立しており、実行には平均6サイクルかかります。

したがって、発行から使用までの間に12の命令と6サイクルがあります。乗算のレイテンシは5サイクルであるため、レイテンシのストールを回避するのに十分です。

データがオーバーフローまたはアンダーフローしないようにするには、正規化手順が必要です。何もしないコードはデータの大きさをゆっくりと増減するため、これが必要です。

したがって、すべてゼロを使用して正規化手順を取り除くだけで、実際にこれよりも良い結果を得ることができます。ただし、消費電力と温度を測定するためのベンチマークを作成したため、ゼロではなく「実際の」データ上にフロップがあることを確認する必要がありました。実行ユニットは、消費電力が少ないゼロに対して特別なケース処理を行う可能性があるためです。そしてより少ない熱を生成します。


より多くの結果:

  • Intel Core i7 920 @ 3.5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1-x64リリース

スレッド:1

Seconds = 72.1116
FP Ops  = 960000000000
FLOPs   = 1.33127e+010
sum = 2.22652

理論的SSEピーク:4 * 3.5 GHz帯=プ14.0 GFLOPSを。実際は13.3 GFlopsです。

スレッド:8

Seconds = 149.576
FP Ops  = 7680000000000
FLOPs   = 5.13452e+010
sum = 17.8122

理論的SSEピーク:4 * 4つのコア* 3.5 GHz帯=プ56.0 GFLOPSを。実際は51.3 GFlopsです。

マルチスレッド実行でプロセッサの温度が76Cに達しました。これらを実行する場合、結果がCPUスロットリングの影響を受けないことを確認してください。


  • Intel Xeon X5482 Harpertown @ 3.2 GHz x 2
  • Ubuntu Linux 10 x64
  • GCC 4.5.2 x64-(-O2 -msse3 -fopenmp)

スレッド:1

Seconds = 78.3357
FP Ops  = 960000000000
FLOPs   = 1.22549e+10
sum = 2.22652

理論的SSEピーク:4 * 3.2 GHzの=プ12.8 GFLOPSを。実際は12.3 GFlopsです。

スレッド:8

Seconds = 78.4733
FP Ops  = 7680000000000
FLOPs   = 9.78676e+10
sum = 17.8122

理論的SSEピーク:4 * 8つのコア* 3.2 GHzの=プ102.4 GFLOPSを。実際は97.9 GFlopsです。


13
あなたの結果は非常に印象的です。古いシステムでg ++を使用してコードをコンパイルしましたが、10万回の反復、1.814s, 5.292 Gflops, sum=0.448883ピーク10.68 Gフロップまたはサイクルあたり2.0フロップに満たない、良好な結果が得られません。思えるadd/ mul並列に実行されません。私がコードを変更し、常に同じレジスタを使用して加算/乗算を行うと、たとえばrC、突然、ほぼピーク、0.953s, 10.068 Gflops, sum=0つまり3.8フロップ/サイクルに達します。非常に奇妙な。
user1059432

11
はい、インラインアセンブリを使用していないため、パフォーマンスはコンパイラに非常に敏感です。ここにあるコードはVC2010用に調整されています。そして、私が正しく思い出せば、インテルコンパイラーも同様に良い結果をもたらします。お気づきのとおり、うまくコンパイルするには、少し調整する必要があるかもしれません。
Mysticial 2011

8
cl /O2(Windows SDKの64ビット)を使用してWindows 7で結果を確認できます。私の例でも、スカラー演算(1.9フロップ/サイクル)のピークに近く実行されます。コンパイラーはループのアンロールと並べ替えを行いますが、これをもう少し詳しく調べる必要がある理由ではないかもしれません。スロットルは問題ではありません。CPUに問題はなく、反復を100kに保ちます。:)
user1059432

6
@Mysticial:今日、r / coding subredditに表示されました。
greyfade 2013

2
@haylem溶けるか離陸する。両方ではありません。十分な冷却がある場合、それは通信時間を取得します。それ以外の場合は、溶けるだけです。:)
Mysticial 2013

33

インテルアーキテクチャーではよく忘れられる点があります。ディスパッチポートはIntとFP / SIMDの間で共有されます。つまり、ループロジックが浮動小数点ストリームにバブルを作成する前に、FP / SIMDのバーストが一定量しか得られません。Mysticalは、アンロールされたループでより長いストライドを使用したため、コードからフロップを増やしました。

ここでNehalem / Sandy Bridgeアーキテクチャを見ると、http://www.realworldtech.com/page.cfm? ArticleID = RWT091810191937&p = 6 何が起きているのかは明らかです。

対照的に、INTパイプとFP / SIMDパイプには独自のスケジューラーを備えた個別の発行ポートがあるため、AMD(ブルドーザー)でピークパフォーマンスに到達する方が簡単です。

テストするプロセッサがどちらもないため、これは理論上のものです。


2
そこループのオーバーヘッドの唯一の3つの命令は、次のとおりですinccmpjl。これらはすべてポート#5に移動でき、ベクトル化faddまたはのいずれにも干渉しませんfmul。私はむしろデコーダーが(時には)邪魔になると思います。サイクルごとに2〜3命令を維持する必要があります。正確な制限は覚えていませんが、命令の長さ、プレフィックス、配置がすべて関係します。
Mackie Messer、2011

cmpそしてjl確かに、ポート5に行くincことが2つの他の人とのグループにいつも来ていないので、確認してくださいと。しかし、あなたの言うとおり、ボトルネックがどこにあるのかを見分けるのは難しく、デコーダーもその一部である可能性があります。
PatrickSchlüter11年

3
基本的なループで少し遊んでみました。命令の順序は重要です。一部のアレンジメントは、最小の5サイクルではなく13サイクルかかります。パフォーマンスイベントカウンターを確認する時間だと思います...
Mackie Messer

16

ブランチを使用すると、理論上のピークパフォーマンスを維持できなくなります。手動でループのアンロールを行うと違いがわかりますか?たとえば、ループの繰り返しごとに5倍または10倍の演算を配置する場合:

for(int i=0; i<loops/5; i++) {
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
   }

4
私は誤解しているかもしれませんが、-O2を指定したg ++はループを自動的に巻き戻そうとすると思います(ダフのデバイスを使用していると思います)。
ウィーバー

6
はい、確かにそれは多少改善されます。現在、約4.1〜4.3 Gフロップ、つまりサイクルあたり1.55フロップを取得しています。いいえ、この例では、-O2はループのアンロールを行いませんでした。
user1059432

1
ウィーバーはループの展開については正しいと思います。したがって、手動でアンロールする必要はおそらくありません
jim mcnamara

5
上記のアセンブリ出力を参照してください。ループが展開する兆候はありません。
user1059432

14
自動アンロールも平均4.2 Gフロップに向上し-funroll-loopsますが、に含まれていないオプションも必要-O3です。を参照してくださいg++ -c -Q -O2 --help=optimizers | grep unroll
user1059432

7

取得した2.4GHz Intel Core 2 DuoでIntels iccバージョン11.1を使用

Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000
addmul:  0.105 s, 9.525 Gflops, res=0.000000
Macintosh:~ mackie$ icc -v
Version 11.1 

これは理想的な9.6 Gフロップに非常に近いものです。

編集:

おっと、アセンブリコードを見ると、iccは乗算をベクトル化しただけでなく、ループから加算を引き出したようです。より厳密なfpセマンティクスを強制すると、コードはベクトル化されなくなります。

Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul:  0.516 s, 1.938 Gflops, res=1.326463

EDIT2:

要求通り:

Macintosh:~ mackie$ clang -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000
addmul:  0.209 s, 4.786 Gflops, res=1.326463
Macintosh:~ mackie$ clang -v
Apple clang version 3.0 (tags/Apple/clang-211.10.1) (based on LLVM 3.0svn)
Target: x86_64-apple-darwin11.2.0
Thread model: posix

clangのコードの内部ループは次のようになります。

        .align  4, 0x90
LBB2_4:                                 ## =>This Inner Loop Header: Depth=1
        addsd   %xmm2, %xmm3
        addsd   %xmm2, %xmm14
        addsd   %xmm2, %xmm5
        addsd   %xmm2, %xmm1
        addsd   %xmm2, %xmm4
        mulsd   %xmm2, %xmm0
        mulsd   %xmm2, %xmm6
        mulsd   %xmm2, %xmm7
        mulsd   %xmm2, %xmm11
        mulsd   %xmm2, %xmm13
        incl    %eax
        cmpl    %r14d, %eax
        jl      LBB2_4

EDIT3:

最後に、2つの提案:最初に、このタイプのベンチマークが好きな場合は、のrdtsc代わりに命令を使用することを検討してくださいgettimeofday(2)。それははるかに正確であり、サイクルで時間を提供します。これは通常、とにかく興味があることです。gccとその友達の場合、次のように定義できます。

#include <stdint.h>

static __inline__ uint64_t rdtsc(void)
{
        uint64_t rval;
        __asm__ volatile ("rdtsc" : "=A" (rval));
        return rval;
}

次に、ベンチマークプログラムを数回実行し、最高のパフォーマンスのみを使用する必要があります。最近のオペレーティングシステムでは、多くのことが並行して行われ、CPUが低周波数の省電力モードなどになっている場合があります。プログラムを繰り返し実行すると、理想的なケースに近い結果が得られます。


2
そして、分解はどのように見えますか?
バーバー、2011

1
興味深いことに、1フロップ/サイクル未満です。コンパイラは混ぜないaddsdのとmulsdののか、彼らは私のアセンブリ出力のようなグループに属していますか?また、コンパイラーがそれらを混合すると、約1フロップ/サイクルになります(これはなしで取得されます-march=native)。add=mul;関数の先頭に行を追加すると、パフォーマンスはどのように変化しますaddmul(...)か?
user1059432

1
@ user1059432:addsdとのsubsd説明は、正確なバージョンでは実際に混在しています。私もclang 3.0を試しました、それは命令を混ぜません、そしてそれはコア2 duoで2フロップ/サイクルに非常に近くなります。私のラップトップコアi5で同じコードを実行しても、コードを混ぜても違いはありません。どちらの場合も、約3フロップ/サイクルになります。
Mackie Messer

1
@ user1059432:最終的には、コンパイラーをだまして合成ベンチマーク用の「意味のある」コードを生成させることがすべてです。見た目よりも難しいです。(つまり、iccがベンチマークを上回ります)必要なのは、コードを4フロップ/サイクルで実行することだけである場合、最も簡単なことは、小さなアセンブリループを記述することです。はるかに少ない頭痛。:-)
Mackie Messer、2011

1
さて、私が上で引用したものに似たアセンブリコードで2フロップ/サイクルに近づきましたか?2にどのくらい近いですか?私は1.4しか手に入らないので、それは重要です。ラップトップで3フロップ/サイクルが得られるとは思いません。コンパイラがicc以前に見たような最適化を行わない限り、アセンブリを再確認できますか?
user1059432
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.