如何达到每个周期4个FLOP的理论最大值?


642

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

据我了解,在大多数现代英特尔CPU上,一个SSE 需要三个周期,一个SSE需要add五个周期mul(例如,请参见Agner Fog的“指令表”)。由于流水线,add如果算法具有至少三个独立的求和,则每个循环的吞吐量可以达到一个。由于对于打包addpd以及标量addsd版本和SSE寄存器都可以包含2 double的情况都是如此,每个周期的吞吐量可以高达2触发器。

此外,似乎(虽然我还没有看到任何这适当的文件)add的和mul的可并行给予的四个理论最大吞吐量每个周期触发器执行。

但是,我无法使用简单的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.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

用打包版本(addpdmulpd)更改标量版本会使翻牌次数增加一倍,而无需更改执行时间,因此每个周期我只能得到2.8翻牌。有没有一个简单的示例,每个周期可以实现四个触发器?

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)

一切似乎都有些复杂,但是到目前为止我的结论是:

  • gcc -O2更改独立浮点运算的顺序,目的是交替使用 addpdmulpd。同样适用于gcc-4.6.2 -O2 -march=core2

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

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

  • 我的Core i5 750Nehalem体系结构)不喜欢交替使用add和mul,并且似乎无法并行运行这两个操作。但是,如果分组为3,则突然会像魔术一样工作。

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

  • 虽然难以接受,但是在我的系统上,我的系统cl /O2在低级优化操作上做得更好,并且对于上面的小C ++示例,其性能接近峰值。我测得的周期为1.85-2.01 flops(在Windows中使用clock()不够精确。我想,需要使用更好的计时器-感谢Mackie Messer)。

  • 我管理的最好的方法gcc是手动循环展开,并按三个一组安排加法和乘法。随着 g++ -O2 -march=nocona addmul_unroll.cpp 我充其量0.207s, 4.825 Gflops,我相当于对每个周期1.8 flop感到满意。

在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之类的操作系统中运行此程序,则可以随时随意调度您的进程。这类外部事件可能会影响您的性能评估。
tdenniston 2011年

您的GCC版本是什么?如果您使用的是默认设置的Mac,则会遇到问题(它是旧的4.2)。
semisight 2011年

2
是的,正在运行Linux,但是系统上没有负载,并且重复执行多次几乎没有什么区别(例如,标量版本的范围为4.0-4.2 Gflops,但现在带有-funroll-loops)。尝试使用gcc版本4.4.1和4.6.2,但是asm输出看起来还可以吗?
user1059432 2011年

您是否尝试-O3过启用gcc的功能-ftree-vectorize?也许结合,-funroll-loops但是如果确实有必要的话,我不会。毕竟,如果其中一个编译器进行矢量化/展开,则进行比较似乎是不公平的,而另一种编译器并非不是因为它不能这样做,而是因为它没有被告知。
灰熊队2012年

4
@Grizzly -funroll-loops可能是要尝试的东西。但是我认为这-ftree-vectorize不是重点。OP正在尝试仅维持1 mul +1个添加指令/周期。指令可以是标量或向量-没关系,因为延迟和吞吐量是相同的。因此,如果您可以用标量SSE维持2个周期,那么可以用矢量SSE替换它们,这样您将获得4个触发器/周期。在我的回答中,我只是从SSE-> AVX开始的。我用AVX替换了所有SSE-相同的延迟,相同的吞吐量,两倍的触发器。
Mysticial 2012年

Answers:


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编译器11)在编译时遇到麻烦。
  • 这些是针对FMA之前的处理器的。为了在Intel Haswell和AMD Bulldozer处理器(及更高版本)上达到峰值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 Glops


现在进行一些解释:

性能关键部分显然是内部循环中的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 Glops

线程: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个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


13
您的结果非常令人印象深刻。我已经在较旧的系统上使用g ++编译了您的代码,但是得到的结果1.814s, 5.292 Gflops, sum=0.448883却不尽如人意:10万次迭代,超出了峰值10.68 Gflops,或者每个周期不足2.0 flops。似乎add/ mul不是并行执行的。当我更改您的代码并始终与同一个寄存器相加/相乘时,例如rC,它突然达到几乎峰值:0.953s, 10.068 Gflops, sum=0或3.8触发器/周期。很奇怪。
user1059432 2011年

11
是的,因为我没有使用内联汇编,所以性能对于编译器确实非常敏感。我在这里编写的代码已针对VC2010进行了调整。如果我没记错的话,英特尔编译器也能提供同样好的结果。正如您所注意到的,您可能必须对其进行一些调整才能使其编译良好。
Mysticial 2011年

8
我可以使用cl /O2(Windows sdk的64位)在Windows 7上确认您的结果,甚至我的示例也在那里接近标量运算的峰值(1.9 flops / cycle)。编译器会循环展开和重新排序,但这可能不是需要进一步研究的原因。节流不是问题,我对我的CPU很好,并且迭代次数保持在100k。:)
user1059432

6
@Mysticial:它出现在今天的r / coding subreddit上。
greyfade

2
@haylem它融化或脱落。永远不要都。如果有足够的冷却空间,它将获得通话时间。否则,它会融化。:)
Mysticial

33

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

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

相反,由于INT和FP / SIMD管道具有各自的调度程序,因此在AMD(Bulldozer)上达到峰值性能应该更容易。

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


2
只有三个循环的开销指令:inccmp,和jl。所有这些都可以转到端口5,并且不会干扰vectorized faddfmul。我宁愿怀疑解码器(有时)也会妨碍您的操作。每个周期需要维持2到3条指令。我不记得确切的限制,但是指令长度,前缀和对齐方式都起作用。
Mackie Messer

cmpjl肯定去端口5,inc不那么肯定,因为它涉及总是在组与2人。但是您是对的,很难说出瓶颈在哪里,解码器也可以成为其中的一部分。
PatrickSchlüter

3
我在基本循环中玩了一些:指令的顺序很重要。一些安排需要13个周期,而不是最少5个周期。我该看一下性能事件计数器了……
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 ++会尝试自动展开循环(我认为它使用Duff的设备)。
Weaver

6
是的,确实确实有所改善。我现在得到大约4.1-4.3 Gflops,或每个周期1.55 flops。不,在此示例中,-O2没有循环展开。
user1059432 2011年

1
我相信,Weaver在循环展开方面是正确的。因此,手动展开可能不是必需的
jim mcnamara

5
参见上面的程序集输出,没有循环展开的迹象。
user1059432 2011年

14
自动展开功能还可提高到平均4.2 Gflops,但需要-funroll-loops甚至没有包含的选项-O3。请参阅g++ -c -Q -O2 --help=optimizers | grep unroll
user1059432 2011年

7

我在2.4GHz Intel Core 2 Duo上使用Intel 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

lang的代码的内部循环如下所示:

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

最后,有两个建议:首先,如果您喜欢这种基准测试,请考虑使用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 2011年

1
@ user1059432:addsd和的subsd说明确实混合在精确的版本中。我也尝试过clang 3.0,它不混合指令,并且在核心2 duo上非常接近2 flops / cycle。当我在笔记本电脑的核心i5上运行相同的代码时,混合代码没有任何区别。在任何一种情况下,我每个周期都能获得约3个翻牌。
Mackie Messer

1
@ user1059432:最后,所有内容都是欺骗编译器为综合基准生成“有意义的”代码。这比初看起来要难。(即icc超过了您的基准测试)如果您想要的只是在4 flops /周期运行一些代码,最简单的事情就是编写一个小的汇编循环。更少的头痛。:-)
Mackie Messer

1
好吧,所以您使用一个类似于我上面引用的汇编代码的每个周期接近2触发器?距离2有多近?我只有1.4,所以意义重大。我认为您的笔记本电脑上不会出现3 flops / cycle的问题,除非编译器进行了icc如前所述的优化,您是否可以仔细检查程序集?
user1059432 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.