ChatGPT解决这个技术问题 Extra ChatGPT

如何实现每个周期 4 次 FLOP 的理论最大值?

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

据我了解,在大多数现代 Intel CPU 上完成 SSE add 需要三个周期,mul 需要五个周期(参见示例 Agner Fog's 'Instruction Tables' )。由于流水线,如果算法具有至少三个独立的求和,则每个周期可以获得一个 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> 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.66 GHz 上产生以下输出:

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)更改标量版本将使 flop 计数增加一倍,而不会改变执行时间,因此每个周期我会得到 2.8 次 flop。有没有一个简单的例子可以实现每个周期四次翻转?

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 更改独立浮点操作的顺序,目的是尽可能交替 addpd 和 mulpd。同样适用于 gcc-4.6.2 -O2 -march=core2。

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

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

我的 Core i5 750(Nehalem 架构)不喜欢交替使用 add 和 mul,并且似乎无法同时运行这两个操作。但是,如果将其分组为 3,它会突然像魔术一样起作用。

如果其他架构(可能是 Sandy Bridge 和其他架构)在汇编代码中交替执行,它们似乎能够毫无问题地并行执行 add/mul。

虽然很难承认,但在我的系统上,cl /O2 在我的系统的低级优化操作方面做得更好,并且在上面的小 C++ 示例中实现了接近峰值的性能。我在 1.85-2.01 flops/cycle 之间测量(在 Windows 中使用了 clock() 并不那么精确。我想,需要使用更好的计时器 - 感谢 Mackie Messer)。

我用 gcc 管理的最好的方法是手动循环展开并以三人一组的方式安排加法和乘法。使用 g++ -O2 -march=nocona addmul_unroll.cpp 我最多可以得到 0.207 秒,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
...
依赖挂钟时间可能是部分原因。假设您在 Linux 之类的操作系统中运行此程序,则可以随时自由地重新安排您的进程。这类外部事件会影响您的绩效测量。
你的 GCC 版本是什么?如果您在使用默认设置的 Mac 上,您会遇到问题(它是旧的 4.2)。
是的,运行 Linux,但系统上没有负载,并且多次重复它几乎没有什么区别(例如,标量版本的范围为 4.0-4.2 Gflops,但现在使用 -funroll-loops)。尝试使用 gcc 版本 4.4.1 和 4.6.2,但 asm 输出看起来还可以吗?
您是否为 gcc 尝试了 -O3,它启用了 -ftree-vectorize?也许与 -funroll-loops 结合,但如果真的有必要,我不会。毕竟,如果其中一个编译器进行矢量化/展开,那么比较似乎有点不公平,而另一个不是因为它不能,而是因为它被告知不要。
@Grizzly -funroll-loops 可能值得尝试。但我认为 -ftree-vectorize 不是重点。 OP 只是试图维持 1 mul + 1 add 指令/周期。指令可以是标量或向量 - 没关系,因为延迟和吞吐量是相同的。因此,如果您可以用标量 SSE 维持 2 个/周期,那么您可以用向量 SSE 替换它们,您将实现 4 个触发器/周期。在我的回答中,我就是从 SSE 做的-> AVX。我用 AVX 替换了所有 SSE——相同的延迟、相同的吞吐量、两倍的失败率。

M
Mysticial

我以前做过这个确切的任务。但主要是测量功耗和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 条指令。您会注意到它分为 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。


你的结果令人印象深刻。我已经在我的旧系统上使用 g++ 编译了您的代码,但没有得到几乎一样好的结果:100k 次迭代,1.814s, 5.292 Gflops, sum=0.448883 超出峰值 10.68 Gflops 或每个周期仅短于 2.0 flops。似乎 add/mul 没有并行执行。当我更改您的代码并始终使用相同的寄存器进行加法/乘法运算时,例如 rC,它突然几乎达到峰值:0.953s, 10.068 Gflops, sum=0 或 3.8 次翻转/周期。很奇怪。
是的,由于我没有使用内联汇编,性能确实对编译器非常敏感。我这里的代码已经针对 VC2010 进行了调整。如果我没记错的话,英特尔编译器给出了同样好的结果。正如您所注意到的,您可能需要对其进行一些调整才能使其编译良好。
我可以使用 cl /O2(来自 windows sdk 的 64 位)在 Windows 7 上确认您的结果,甚至我的示例在该处的标量操作(1.9 次触发器/周期)运行接近峰值。编译器循环展开和重新排序,但这可能不是需要更多研究的原因。节流不是问题我对我的 cpu 很好,并将迭代保持在 100k。 :)
@Mysticial:今天showed up on the r/coding subreddit
using namespace std; is a bad practice,永远不要使用它。
P
Patrick Schlüter

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

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

相比之下,在 AMD(推土机)上达到峰值性能应该更容易,因为 INT 和 FP/SIMD 管道具有独立的问题端口和自己的调度程序。

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


循环开销只有三个指令:inccmpjl。所有这些都可以转到端口#5,并且不会干扰矢量化 faddfmul。我宁愿怀疑解码器(有时)会碍事。每个周期它需要维持两到三个指令。我不记得确切的限制,但指令长度、前缀和对齐方式都在起作用。
cmpjl 肯定会去端口 5,inc 不太确定,因为它总是与其他 2 个一起出现。但你是对的,很难说瓶颈在哪里,解码器也可能是其中的一部分。
我玩了一下基本循环:指令的顺序确实很重要。有些安排需要 13 个周期,而不是最少的 5 个周期。是时候看看我猜的性能事件计数器了......
T
TJD

分支绝对可以使您无法维持最高的理论性能。如果您手动进行一些循环展开,您会看到区别吗?例如,如果您在每次循环迭代中放置 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;
   }

我可能弄错了,但我相信带有 -O2 的 g++ 会尝试自动展开循环(我认为它使用 Duff 的设备)。
是的,谢谢它确实有所改善。我现在得到大约 4.1-4.3 Gflops,或者每个周期 1.55 flops。不,在这个例子中 -O2 没有循环展开。
我相信,Weaver 关于循环展开是正确的。所以可能不需要手动展开
请参阅上面的汇编输出,没有循环展开的迹象。
自动展开也提高到平均 4.2 Gflops,但需要 -funroll-loops 选项,该选项甚至不包含在 -O3 中。见g++ -c -Q -O2 --help=optimizers | grep unroll
M
Mackie Messer

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

最后,有两个建议:首先,如果您喜欢这种类型的基准测试,请考虑使用 rdtsc 指令而不是 gettimeofday(2)。它更加准确,并以周期为单位提供时间,这通常是您感兴趣的。对于 gcc 和朋友,您可以像这样定义它:

#include <stdint.h>

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

其次,您应该多次运行您的基准测试程序并只使用最佳性能。在现代操作系统中,许多事情是并行发生的,cpu 可能处于低频省电模式等。重复运行程序会给你一个更接近理想情况的结果。


反汇编是什么样的?
有趣的是,这不到 1 次翻牌/周期。编译器是否将 addsdmulsd 混合在一起,或者它们是否像我的程序集输出一样分组?当编译器混合它们时,我也得到了大约 1 个 flop/cycle(我没有 -march=native)。如果在函数 addmul(...) 的开头添加一行 add=mul;,性能会如何变化?
@user1059432:addsdsubsd 指令确实混合在精确版本中。我也尝试过 clang 3.0,它不会混合指令,并且在 core 2 duo 上非常接近 2 次失败/周期。当我在笔记本电脑 core i5 上运行相同的代码时,混合代码没有任何区别。在任何一种情况下,我都会得到大约 3 次失败/周期。
user1059432:最后,这一切都是为了欺骗编译器为合成基准生成“有意义的”代码。这比乍一看要难。 (即 icc 比您的基准测试更聪明)如果您只想以 4 次触发器/周期运行一些代码,那么最简单的事情就是编写一个小的汇编循环。少了很多头疼。 :-)
好的,所以您使用类似于我上面引用的汇编代码接近 2 次失败/周期?离2有多近?我只得到 1.4,所以这很重要。我认为您的笔记本电脑上不会出现 3 次失败/周期,除非编译器像您之前在 icc 中看到的那样进行优化,您可以仔细检查程序集吗?