我如何获得每个周期4个FLOP的理论最大值?

在现代的x86-64英特尔CPU上,如何在每个周期实现4个浮点运算(双精度)的理论峰值性能?

据我了解它需要三个周期为SSE add和五个周期mul完成对最现代的Intel CPU的(参见例如昂纳雾的“指令表”)。 由于流水线化,如果算法至少有三次独立求和,则每个周期可以获得一个add的吞吐量。 由于对于打包的addpd以及标量addsd版本和SSE寄存器都可以包含两个double ,所以每个周期的吞吐量可能高达两个触发器。

此外,看起来(尽管我没有看到任何有关这方面的适当文件), addmul可以并行执行,从而给出每个周期四个触发器的理论最大吞吐量。

但是,我还没有能够通过简单的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> millionsn");
      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=%fn", 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查看汇编代码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

使用打包版本( addpdmulpd )来更改标量版本将会在不改变执行时间的情况下将翻转计数加倍,所以每个周期我只能达到2.8个触发器。 有没有一个简单的例子可以实现每个周期四个触发器?

Mysticial的小程序; 这里是我的结果(尽管只运行了几秒):

  • gcc -O2 -march=nocona :10.66 Gflops中的5.6 Gflops(2.1个触发器/周期)
  • cl /O2 ,openmp删除:10.66 Gflops中的10.1 Gflops(3.8个触发器/周期)
  • 这一切似乎都有点复杂,但到目前为止我的结论是:

  • gcc -O2更改独立浮点操作的顺序,以便在可能的情况下交替使用addpdmulpd 。 同样适用于gcc-4.6.2 -O2 -march=core2

  • gcc -O2 -march=nocona似乎保持了C ++源代码中定义的浮点操作顺序。

  • cl /O2 ,来自SDK for Windows 7的64位编译器会自动循环展开,并似乎尝试并安排操作,以便三个addpd组与三个addpd交替进行mulpd (至少在我的系统和为我的简单程序)。

  • 我的Core i5 750(Nahelem架构)不喜欢交替添加和mul,似乎无法并行运行这两种操作。 但是,如果将其分组为3,它就会像魔术一样突然运行。

  • 其他体系结构(可能是Sandy Bridge和其他体系结构)似乎能够并行执行add / mul而不会出现问题,如果它们在汇编代码中交替出现的话。

  • 尽管很难承认,但在我的系统上, cl /O2在我的系统的低级优化操作方面做得更好,并且达到了上述小C ++示例的接近最佳性能。 我测量的是1.85-2.01个触发器/周期(在Windows中使用的时钟()并不是那么精确,我想,需要使用更好的定时器 - 感谢Mackie Messer)。

  • 我用gcc管理的最好的方法是手动循环展开,并以三个一组的方式安排加法和乘法。 用g++ -O2 -march=nocona addmul_unroll.cpp可以得到0.207s, 4.825 Gflops ,这相当于1.8 flops / cycle,我现在很满意。

  • 在C ++代码中,我用for代替了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
    ...
    

    我之前完成了这个确切的任务。 但主要是测量功耗和CPU温度。 下面的代码(这是相当长的)在我的Core i7 2600K上达到接近最佳状态。

    这里要注意的关键是大量的手动循环展开以及交织乘法和增加......

    完整的项目可以在我的GitHub上找到:https://github.com/Mysticial/Flops

    警告:

    如果你决定编译和运行这个,请注意你的CPU温度!
    确保你不会过热。 并确保CPU节流不会影响您的结果!

    此外,我对运行此代码可能导致的任何损害不承担任何责任。

    笔记:

  • 此代码针对x64进行了优化。 x86没有足够的寄存器来编译。
  • 此代码已经过测试,在Visual Studio 2010/2012和GCC 4.6上运行良好。
    ICC 11(英特尔编译器11)出人意料地编译出现问题。
  • 这些用于FMA前处理器。 为了在英特尔Haswell和AMD推土机处理器(及更高版本)上实现峰值FLOPS,需要FMA(熔合多路添加)指令。 这些超出了这个基准的范围。
  • #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条指令。 你会注意到它被分成4块,每块12条指令。 这12个指令块中的每一块都是完全独立的 - 平均需要6个周期才能执行。

    所以在问题到使用之间有12条指令和6个周期。 乘法的延迟是5个周期,所以它足以避免延迟时间延迟。

    正常化步骤需要保持数据不被溢出/下溢。 这是必需的,因为不做任何代码将缓慢地增加/减少数据的大小。

    所以如果你只使用全零并摆脱标准化步骤,那么实际上可以做得比这更好。 但是,由于我编写了测量功耗和温度的基准测试,因此我必须确保触发器处于“真实”数据而不是零点 ,因为执行单元可能会使用功耗更低的零的特殊情况处理并产生较少的热量。


    更多结果:

  • 英特尔酷睿i7 920 @ 3.5 GHz
  • Windows 7旗舰版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限制。


  • 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


    英特尔架构中有一点常常被人们忽略,调度端口在Int和FP / SIMD之间共享。 这意味着在循环逻辑将在您的浮点流中创建泡泡之前,您只会获得一定数量的FP / SIMD突发。 神秘的代码中有更多的失败,因为他在展开的循环中使用了更长的步幅。

    如果您在这里查看Nehalem / Sandy Bridge架构,请访问http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6,这很清楚会发生什么。

    相比之下,由于INT和FP / SIMD管道使用自己的调度程序将问题端口分开,因此AMD(Bulldozer)上的峰值性能应该更容易达到。

    这只是理论上的,因为我没有这些处理器要测试。


    分支机构绝对可以让您保持理论上的高峰表现。 如果您手动执行一些循环展开,您会看到差异吗? 例如,如果每循环迭代次数为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;
       }
    
    链接地址: http://www.djcxy.com/p/31503.html

    上一篇: How do I achieve the theoretical maximum of 4 FLOPs per cycle?

    下一篇: Why is it faster to process a sorted array than an unsorted array?