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

据我了解花的SSE add三个周期和mul完成大部分现代的英特尔 Cpu (例如见Agner 雾指令表) 上的五个周期。由于管道一个一个add每个周期的吞吐时会得到的算法具有至少三个独立的求和。由于时为 true,打包addpd以及标量addsd版本,SSE 寄存器可能包含两个double的吞吐量可以尽最大的两个 flops 每个周期。

此外,它看起来 (尽管我不在这上看到任何正确的说明文件)add的和mul可以并行执行的给予每个周期的四个 flops 理论最大吞吐量。

但是,我不已经能够复制该性能与简单的 C/c + + 课程。我最好的尝试会导致每个周期约 2.7 flops。如果任何人都可以提供简单的 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>
", argv[0]);
      printf("number of operations: <num> millions
");
      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:	 %.3f s, %.3f Gflops, res=%f
", t, (double)n/t/1e9, x);
   return EXIT_SUCCESS;
}

使用编译

g++ -O2 -march=native addmul.cpp ; ./a.out 1000

将生成以下输出上英特尔酷睿 i5-750,2.66 g h z。

addmul:  0.270 s, 3.707 Gflops, res=1.326463

也就是说,只需大约 1.4 flops 每个周期。查看与汇编代码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 flops 每个周期。有一个简单的例子,它实现了每个周期的四个 flops 吗?

很好很少计划通过 Mysticial;这里是我的结果 (不过运行仅为几秒)︰

  • gcc -O2 -march=nocona: 10.66 此处从 5.6 此处 (2.1 flops/周期)
  • cl /O2,openmp 删除︰ 10.66 此处出 10.1 此处 (3.8 flops/周期)

这一切看起来有点复杂,但到目前为止,我得出结论︰

  • gcc -O2更改本着尽可能交替addpdmulpd的独立浮点运算的顺序。这同样适用于gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona似乎保持浮点操作,因为在 c + + 源文件中定义的顺序。

  • cl /O2,64 位Windows 7 的 SDK编译器会自动执行循环展开和似乎尝试并排列操作这样的三个addpd组的具有三个mulpd的备用 (嗯,至少在我的系统,为我简单的计划)。

  • 酷睿 i5 750Nahelem 体系结构) 不喜欢交替添加的和 mul 的并且看起来不能并行运行这两种操作。但是,如果在第 3 的分组它突然起作用如同变魔术一样。

  • 其他体系结构 (可能是Sandy Bridge和其他人) 似乎能够执行添加 mul 并行没有问题,如果程序集代码中交替。

  • 尽管很难,但不得不承认,但我的系统上cl /O2不更好地在我的系统的低级优化操作,并实现了上面的一些 c + + 示例的峰值性能接近。我衡量 1.85 2.01 flops/周期之间 (使用 clock() 在 Windows 中这是不准确的。我猜出答案,需要使用更好的计时器-谢谢 Mackie Messer)。

  • 我用gcc管理的最佳是手动循环取消并排列添加和三个组中的乘法运算。g++ -O2 -march=nocona addmul_unroll.cpp得到最0.207s, 4.825 Gflops相当于 1.8 flops/周期中我非常满意现在。

我已经在 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
...
2011-12-05 17:54:56
问题评论:

表示。 低级别处理器优化指标和本机程序集语言完成...我激动的 strangest 的东西。

依赖于 wallclock 的时间可能是原因的一部分。假设您正在运行 Linux 等操作系统在这,很自由地在任何时候 deschedule 您的进程。这种外部事件可能会影响您的性能测量结果。

GCC 版本是什么?如果你在使用默认 mac 上,会遇到的问题 (它是旧的 4.2)。

是运行 Linux 系统上没有负载而多次重复它使小差异 (如范围 4.0 4.2 此处标量版本,但现在-funroll-loops使用)。尝试使用 gcc 版本 4.4.1 和 4.6.2,但 asm 输出看起来正常吗?

@Grizzly -funroll-loops可能是要尝试的东西。但我认为, -ftree-vectorize是除了点。操作只试图维持 1 mul + 1 添加指令/周期。说明可以是标量或矢量-它并不重要因为延迟和吞吐量相同。如果可以维持 2/周期与标量 SSE,然后您可以替换这些向量 SSE,将达到 4 的 flops 周期。在我的答案只是这样做从 SSE AVX->。我所有 SSE 都替换 AVX-相同的延迟时间、 相同的吞吐量,2 倍 flops。

回答:

我已经完成了前的此确切任务。但这主要是测量功率消耗和 CPU 温度。下面的代码 (这是较长) 来实现关闭到上我的酷睿 i7 最佳 2600 K。

关键的一点要强调的是大量手动循环的展开,以及交替的相乘,把...

我 GitHub 上找不到完整的项目︰ https://github.com/Mysticial/Flops

警告︰

如果您决定要编译并运行此操作,请注意您的 CPU 温度!!!
请确保您没有过热。并确保 CPU 限制不会影响您的结果 !

此外,我要学习运行这段代码,可能会导致任何损害不负的责任。

注释︰

  • X64 优化此代码。x86 并不一定要很好地编译足够寄存器。
  • 此代码已经过测试,能够很好地在 Visual Studio 2010 的 2012年和 GCC 4.6 上。
    ICC 11 (英特尔编译器 11) 极具已编译好的问题。
  • 这些都是预 FMA 处理器。为了达到峰值 FLOPS Haswell 英特尔和 AMD Bulldozer 处理器 (和更高版本),将需要 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");
}

使用 Visual Studio 2010 SP1 的 x64 编译的输出 (1 个线程,10000000 次迭代) 的版本︰

Seconds = 55.5104
FP Ops  = 960000000000
FLOPs   = 1.7294e+010
sum = 2.22652

机器是 4.4 g h z。 理论 SSE 峰值 @ 酷睿 i7 2600 K 是 4 flops * 4.4 g h z = 17.6 此处这段代码达到17.3 此处-不坏。

使用 Visual Studio 2010 SP1 的 x64 编译的输出 (8 线程的 10000000 次迭代) 的版本︰

Seconds = 117.202
FP Ops  = 7680000000000
FLOPs   = 6.55279e+010
sum = 17.8122

理论的 SSE 高峰期是 4 flops * 4 核 * 4.4 g h z = 70.4 此处。实际是65.5 此处.


让我们进一步采取此一步骤。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");
}

使用 Visual Studio 2010 SP1 的 x64 编译的输出 (1 个线程,10000000 次迭代) 的版本︰

Seconds = 57.4679
FP Ops  = 1920000000000
FLOPs   = 3.34099e+010
sum = 4.45305

理论 AVX 高峰期是 8 flops * 4.4 g h z = 35.2 此处实际是33.4 此处.

使用 Visual Studio 2010 SP1 的 x64 编译的输出 (8 线程的 10000000 次迭代) 的版本︰

Seconds = 111.119
FP Ops  = 15360000000000
FLOPs   = 1.3823e+011
sum = 35.6244

理论 AVX 高峰期是 8 flops * 4 核 * 4.4 g h z = 140.8 此处。实际是138.2 此处.


现在的一些说明︰

性能的关键部件很明显内, 环内的 48 说明进行操作。您会注意到它可分为 12 说明的 4 块。每个这些 12 说明块完全独立于彼此-并采取平均 6 个周期执行。

因此没有 12 说明和使用问题之间 6 个周期。乘法的反应是 5 周期,使其刚好足够,以避免延迟时间隔。

规范化步骤需要保留的数据从转移/underflowing。这需要因为什么代码将缓慢增加/减少数据量。

因此实际可能,如果只是使用全部为零并除去规范化步骤做的更好呢。但是,因为我编写的基准测量功耗和温度,我必须确保 flops"实际"数据,而不是零-作为执行单位可能很好地拥有特殊案例处理的零,使用更少电源,并产生更少的热量。


更多的结果︰

  • 英特尔酷睿 i7 920 @ 3.5 g h z
  • Windows 7 旗舰版的 x64
  • Visual Studio 2010 SP1 的 x64 版本

线程︰ 1

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

理论的 SSE 高峰︰ 4 flops * 3.5 g h z = 14.0 此处实际是13.3 此处.

线程︰ 8

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

理论的 SSE 高峰︰ 4 flops * 4 核 * 3.5 g h z = 56.0 此处实际是51.3 此处.

在多线程命中 76 C 我处理器 temps 运行 !如果运行这些操作时,请确保结果不受 CPU 限制。


  • 2 x @ 3.2 g h z 的英特尔至强 X5482 Harpertown
  • 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 flops * 3.2 g h z = 12.8 此处实际是12.3 此处.

线程︰ 8

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

理论的 SSE 高峰︰ 4 flops * 8 核 * 3.2 g h z = 102.4 此处实际是97.9 此处.

我知道型号,但当我看到"CPU 温度"附近"2600 K"我做了双带。

结果是非常令人印象深刻。已编译您的代码使用 g + + 我旧的系统上,但没有得到几乎一样好的结果︰ 100 k 次迭代, 1.814s, 5.292 Gflops, sum=0.448883从峰值 10.68 此处或只是短的 2.0 flops 每个周期。看起来addmul不执行并行。在更改您的代码,并始终添加/乘与同一个寄存器,说rC时,它突然几乎达到峰值︰ 0.953s, 10.068 Gflops, sum=0或 3.8 的 flops 周期。非常奇怪。

是的我没有使用内联程序集,因为性能确实是非常敏感的编译器。我这里有的代码已被优化为 VC2010。并且如果我记得正确,英特尔编译器给出一样很好的结果。您已经注意到,您可能需要对其进行一些调整以使其很好地编译。

我可以确认您在使用cl /O2 (64 位的 windows sdk) 的 Windows 7 的结果,甚至我的示例运行接近峰值标量操作 (1.9 flops/周期) 存在。循环 unrolls 编译器,并重新进行排序,但这可能并不需要考虑这一点的原因。调节不是什么问题给我的 cpu 是很好,并保持在 100k 的迭代。:)

@Mysticial︰ 它现在显示编码 r/subreddit

英特尔® 体系结构通常人们忘记点,派单端口共享 Int 和 FP/单指令多数据之间。这意味着,才会出现一定程度的突发的 FP/单指令多数据前循环逻辑将创建在您浮动的气泡流点。神秘提供了更多 flops 超出他的代码,因为他在他展开循环中使用更长时间的努力。

如果您看一下这里的 Nehalem/Sandy Bridge 体系结构http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6它是很清楚会发生什么情况。

与此相反,它应该是容易到达峰值性能上 AMD (Bulldozer) 为 INT 和 FP/单指令多数据管道有使用自己的调度程序的单独问题端口。

因为我有这两类处理器测试,这是只有理论。

只有三个循环的系统开销的说明︰ inccmpjl所有这些可以转到端口 #5,并不会干扰矢量化的faddfmul而我会怀疑解码器 (有时) 进入方式。它需要维持两个和三个指令,每个周期之间。我不记得具体限制,但长度、 前缀和对齐方式会发挥所有的指令。

cmpjl肯定转到端口 5, inc不太确定,因为谬论始终与第 2 组中其他人。但您是正确的很难判断其中的瓶颈是,解码器也可以是它的一部分。

我玩耍有点基本循环︰ 重要指令集的排序。一些工作花费 13 周期,而不是最少 5 周期。看一看我想这些性能事件计数器的时间...

分支肯定可以使您从维持最佳理论性能。如果您手动执行一些循环展开是否看到区别了?例如,如果将每个循环迭代的 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;
   }

我可能会被误认为,但我相信,g + + 与-O2 将尝试自动展开循环 (我认为它使用 Duff 的设备)。

是的感谢确实它提高了某种程度。现在获取有关 4.1-4.3 此处或 1.55 flops 每个周期。并没有,在此示例中的循环 O2 没有取消。

Weaver 是有关循环展开,我认为正确的。手动展开则可能不需要

请参阅上面,输出程序集的循环展开的任何迹象。

自动展开也提高到平均 4.2 此处,但需要-funroll-loops选项,它甚至不包括在-O3请参阅g++ -c -Q -O2 --help=optimizers | grep unroll.

使用准 icc 版本 11.1 上得到 2.4 g h z 英特尔酷睿 2 双核处理器

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 此处。

编辑︰

天哪,它查看程序集代码看起来 icc 不仅 vectorized 乘,而且还拉补充到循环外。强制执行更严格的 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:

最后,两个建议︰ 首先,如果您喜欢这种类型的性能基准管理工具,请考虑使用gettimeofday(2) rdtsc指令 istead。它是更加准确和提供的时间周期,通常是什么感兴趣。对于 gcc 和朋友可以将其定义如下所示︰

#include <stdint.h>

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

第二,应该多次运行基准测试程序,并使用最佳的性能在现代的操作系统会以并行执行许多操作,cpu 可能处于低频电源节能模式等。反复运行程序使您可以更接近于理想情况下的结果。

和是什么反汇编样子吗?

这就是有趣的小于 1 的垂直周期。编译器是否混合有addsdmulsd还是在我的程序集输出组中?编译器可将它们混合时也得到几乎 1 垂直/周期 (其中获取而-march=native)。如何性能改变如果添加行add=mul; addmul(...)函数的开头?

@user1059432: addsdsubsd说明确实混合在精确的版本。我已尝试过 clang 3.0、 它不混合使用说明和它亮起非常接近每个周期 2 flops 酷睿 2 双核处理器。I 在我的笔记本电脑酷睿 i5 上运行相同的代码,当混合代码没有什么差别。得到有关 3 flops/周期中这两种情况。

@user1059432︰ 最后就是以欺骗手段使编译器生成综合准则"有意义"的代码。这是不是它第一个外观看起来更加困难。(亦即 icc outsmarts 您的基准测试)如果只想运行某些代码 4 flops/周期最简单的操作是编写一个小程序集循环。要少得多的 headake。:-)

好了,因此可以获得接近 2 的 flops 周期与程序集的代码类似于我上面已经说吗?如何接近 2?仅收到 1.4,非常重要。我认为除非编译器不 optimisations,如您所见与之前的icc 3 的 flops 周期获取您的便携式计算机上,您可以仔细检查程序集?

请输入您的翻译

How do I achieve the theoretical maximum of 4 FLOPs per cycle?

确认取消