1サイクルあたり理論上の最大4 FLOPs を達成するにはどうすればよいでしょうか? 質問する

1サイクルあたり理論上の最大4 FLOPs を達成するにはどうすればよいでしょうか? 質問する

最新の x86-64 Intel CPU で、1 サイクルあたり 4 回の浮動小数点演算 (倍精度) という理論上のピーク性能をどのように実現できるのでしょうか?

私の理解では、南東 add最近のほとんどのIntel CPUでは、完了までに5サイクルかかりますmul(例えばアグナー・フォグの「指示表」)。パイプライン処理により、addアルゴリズムに少なくとも 3 つの独立した合計がある場合、1 サイクルあたり 1 のスループットを得ることができます。これはパック バージョンaddpdとスカラー バージョンの両方に当てはまりaddsd、SSE レジスタには 2 つの を含めることができるためdouble、スループットは 1 サイクルあたり最大 2 フロップになります。

さらに、(これに関する適切なドキュメントを見たことはありませんが)addと をmul並列に実行して、理論上の最大スループットを 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 倍になり、1 サイクルあたり 2.8 フロップにわずかに満たなくなります。1 サイクルあたり 4 フロップを実現する簡単な例はありますか?

Mysticial による素敵な小さなプログラムです。以下は私の結果です (ただし、実行時間はほんの数秒です)。

  • gcc -O2 -march=nocona: 10.66 Glops のうち 5.6 Glops (2.1 フロップス/サイクル)
  • cl /O2、OpenMP が削除されました: 10.66 Glops のうち 10.1 Glops (3.8 フロップス/サイクル)

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

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

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

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

  • 私のコア i5 750ネハレムアーキテクチャ) は、加算と乗算を交互に行うことを好まず、両方の操作を並行して実行できないようです。ただし、3 つにグループ化すると、突然魔法のように動作します。

  • その他のアーキテクチャ(おそらくサンディブリッジおよびその他) は、アセンブリ コード内で交互に実行すれば、問題なく 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 Gflopsで 1.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
...

ベストアンサー1

これまでにもまったく同じタスクを実行したことがあります。ただし、主な目的は電力消費と 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 サイクルなので、レイテンシ ストールを回避するのに十分です。

データのオーバーフローやアンダーフローを防ぐためには、正規化の手順が必要です。何もしないコードによってデータの大きさが徐々に増加/減少するため、これが必要になります。

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


より多くの結果:

  • インテル Core i7 920 @ 3.5GHz
  • 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です。

マルチスレッド実行時にプロセッサの温度が 76 度に達しました。これらを実行する場合は、結果が 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.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です。

おすすめ記事