web-dev-qa-db-ja.com

1サイクルあり4 FLOPという理論上の最大値を達成するにはどうすればよいですか。

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

私が理解している限りでは、 SSEaddname__には3サイクル、mulname__には5サイクルかかります。例えば Agner Fogの 'Instruction Tables' )です。パイプライン化により、アルゴリズムが少なくとも3つの独立した合計を持つ場合、1サイクルあたり1つのaddname__のスループットが得られます。これは、パックaddpdname__、スカラーaddsdname__バージョン、およびSSEレジスタに2つのdoublename __を含めることができる場合にも当てはまります。スループットは、1サイクルあたり最大2フロップになります。

さらに、(これに関する適切なドキュメントは見ていませんが)addname__とmulname__は並列に実行でき、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

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

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

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

Mysticialによる素晴らしい小さなプログラム。これが私の結果です(ただし数秒間実行してください)。

  • gcc -O2 -march=nocona:10.66 Gflopsのうち5.6 Gflops(2.1 flops/cycle)
  • cl /O2、openmpの削除:10.66 Gflopsのうち10.1 Gflops(3.8 flops/cycle)

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

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

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

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

  • 私の Core i5 750Nehalemアーキテクチャ )は交互のアドが好きではありませんまた、両方の操作を並行して実行することはできません。しかし、3のものに分類されるならば、それは突然魔法のように働きます。

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

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

  • 私がgccname__を使って管理した最良の方法は、手動で展開をループし、加算と乗算を3つのグループにまとめることでした。 g++ -O2 -march=nocona addmul_unroll.cppを使って、私はせいぜい0.207s, 4.825 Gflopsを得ます。これは1.8フロップ/サイクルに相当します。

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

   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
...
600
user1059432

私は前にこの正確な仕事をしました。しかし、それは主に消費電力と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プロセッサ(およびそれ以降)でpeak 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 x 64
  • Visual Studio 2010 SP1 - x 64リリース

スレッド数: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 x 64
  • GCC 4.5.2 x 64 - (-O2 - msse 3 - 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です。

486
Mysticial

インテルのアーキテクチャーには、ディスパッチポートがIntとFP/SIMDで共有されていることを忘れがちな点があります。つまり、ループロジックによって浮動小数点ストリームにバブルが発生する前に、FP/SIMDのバーストが一定量発生するだけです。 Mysticalは彼のコードからより多くのフロップを得ました、なぜなら彼は彼のアンロールされたループでより長いストライドを使ったからです。

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

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

私がテストするこれらのプロセッサのどちらも持っていないので、これは理論的にすぎません。

30

枝は間違いなくあなたがピークの理論的性能を維持するのを妨げることができます。手動でループ展開を行った場合、違いがありますか?たとえば、ループの反復ごとに5倍または10倍のopsを設定したとします。

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;
   }
16
TJD

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 Gflopsに非常に近いです。

編集:

おっと、アセンブリコードを見てみると、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

編集2:

要求どおり:

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

編集3:

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

#include <stdint.h>

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

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

7
Mackie Messer