679

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

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

さらに、(これに関する適切なドキュメントは見ていませんが)addmulは並行して実行でき、1サイクルあたり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.66GHzで次の出力を生成します。

addmul:  0.270 s, 3.707 Gflops, res=1.326463

つまり、1サイクルあたり約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.66Gflopsのうち5.6Gflops(2.1フロップス/サイクル)
  • cl /O2、openmpが削除されました:10.66Gflopsのうち10.1Gflops(3.8フロップ/サイクル)

すべてが少し複雑に見えますが、これまでの私の結論は次のとおりです。

  • gcc -O2addpd可能な場合はとを交互に使用することを目的として、独立した浮動小数点演算の順序を変更し mulpdます。同じことが。にも当てはまりますgcc-4.6.2 -O2 -march=core2

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

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

  • 私のCorei5750Nehalemアーキテクチャ)は、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フロップ/サイクルに相当するものが得られますが、これは今では非常に満足しています。

forC ++コードでは、ループを次のように置き換えました。

   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
...
4

4 に答える 4

546

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

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

プロジェクト全体は私のGitHubで見つけることができます:https ://github.com/Mysticial/Flops

警告:

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

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

ノート:

  • このコードはx64用に最適化されています。x86には、これを適切にコンパイルするための十分なレジスタがありません。
  • このコードは、VisualStudio2010/2012およびGCC4.6で正常に機能することがテストされています。
    ICC 11(Intelコンパイラ11)は、驚くべきことに、うまくコンパイルするのに問題があります。
  • これらは、FMA以前のプロセッサ用です。IntelHaswellおよびAMDBuldozerプロセッサ(およびそれ以降)でピーク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

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

出力(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.4GFlopsです。実際は65.5GFlopsです。


これをさらに一歩進めましょう。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.4GHz = 35.2GFlopsです。実際は33.4GFlopsです。

出力(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.8GFlopsです。実際は138.2GFlopsです。


さて、いくつかの説明のために:

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

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

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

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


より多くの結果:

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

スレッド:1

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

理論上のSSEピーク:4フロップス* 3.5 GHz = 14.0GFlops。実際は13.3GFlopsです。

スレッド:8

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

理論上のSSEピーク:4フロップ*4コア*3.5 GHz = 56.0GFlops。実際は51.3GFlopsです。

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


  • 2 x Intel Xeon X5482 Harpertown @ 3.2 GHz
  • 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.8GFlops。実際は12.3GFlopsです。

スレッド:8

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

理論上のSSEピーク:4フロップ*8コア*3.2 GHz = 102.4GFlops。実際は97.9GFlopsです。

于 2011-12-05T20:43:55.563 に答える
37

Intelアーキテクチャには、人々が忘れがちなポイントがあります。ディスパッチポートはIntとFP/SIMDの間で共有されます。これは、ループロジックが浮動小数点ストリームにバブルを作成する前に、FP/SIMDの特定の量のバーストのみを取得することを意味します。Mysticalは、展開されたループでより長いストライドを使用したため、コードからより多くのフロップを取得しました。

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

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

テストするこれらのプロセッサがないため、これは理論上のものにすぎません。

于 2011-12-06T16:05:20.707 に答える
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;
   }
于 2011-12-05T18:04:38.450 に答える
8

2.4GHzのIntelCore2DuoでIntelsiccバージョン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.6Gflopsに非常に近い値です。

編集:

おっと、アセンブリコードを見ると、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は低周波数の省電力モードになっている可能性があります。プログラムを繰り返し実行すると、理想的なケースに近い結果が得られます。

于 2011-12-05T20:19:32.490 に答える