【问题标题】:Intel FMA Instructions Offer Zero Performance Advantage英特尔 FMA 指令提供零性能优势
【发布时间】:2016-06-08 19:07:58
【问题描述】:

考虑使用 Haswell 的 FMA 指令的以下指令序列:

  __m256 r1 = _mm256_xor_ps (r1, r1);
  r1 = _mm256_fmadd_ps (rp1, m6, r1);
  r1 = _mm256_fmadd_ps (rp2, m7, r1);
  r1 = _mm256_fmadd_ps (rp3, m8, r1);

  __m256 r2 = _mm256_xor_ps (r2, r2);
  r2 = _mm256_fmadd_ps (rp1, m3, r2);
  r2 = _mm256_fmadd_ps (rp2, m4, r2);
  r2 = _mm256_fmadd_ps (rp3, m5, r2);

  __m256 r3 = _mm256_xor_ps (r3, r3);
  r3 = _mm256_fmadd_ps (rp1, m0, r3);
  r3 = _mm256_fmadd_ps (rp2, m1, r3);
  r3 = _mm256_fmadd_ps (rp3, m2, r3);

同样的计算可以用非 FMA 指令表示如下:

  __m256 i1 = _mm256_mul_ps (rp1, m6);
  __m256 i2 = _mm256_mul_ps (rp2, m7);
  __m256 i3 = _mm256_mul_ps (rp3, m8);
  __m256 r1 = _mm256_xor_ps (r1, r1);
  r1 = _mm256_add_ps (i1, i2);
  r1 = _mm256_add_ps (r1, i3);

  i1 = _mm256_mul_ps (rp1, m3);
  i2 = _mm256_mul_ps (rp2, m4);
  i3 = _mm256_mul_ps (rp3, m5);
  __m256 r2 = _mm256_xor_ps (r2, r2);
  r2 = _mm256_add_ps (i1, i2);
  r2 = _mm256_add_ps (r2, i3);

  i1 = _mm256_mul_ps (rp1, m0);
  i2 = _mm256_mul_ps (rp2, m1);
  i3 = _mm256_mul_ps (rp3, m2);
  __m256 r3 = _mm256_xor_ps (r3, r3);
  r3 = _mm256_add_ps (i1, i2);
  r3 = _mm256_add_ps (r3, i3);

人们会期望 FMA 版本比非 FMA 版本提供一些性能优势。

但不幸的是,在这种情况下,性能改进为零 (0)。

谁能帮我理解为什么?

我在基于核心 i7-4790 的机器上测量了这两种方法。

更新:

所以我分析了生成的机器码,并确定 MSFT VS2013 C++ 编译器正在生成机器码,因此 r1 和 r2 的依赖链可以并行调度,因为 Haswell 有 2 个 FMA 管道。

r3 必须在 r1 之后调度,所以在这种情况下,第二个 FMA 管道是空闲的。

我认为如果我展开循环以执行 6 组 FMA 而不是 3 组,那么我可以让所有 FMA 管道在每次迭代中都处于忙碌状态。

不幸的是,当我在这种情况下检查程序集转储时,MSFT 编译器没有选择允许我正在寻找的并行调度类型的寄存器分配,并且我证实我没有得到性能提升我一直在寻找。

有没有一种方法可以更改我的 C 代码(使用内在函数)以使编译器能够生成更好的代码?

【问题讨论】:

  • 是的,我想我尝试了类似的方法并得到了相同的结果 - 我还尝试了混合 FMA/AVX2 以查看是否有任何可以利用的并行性,但同样没有任何好处。
  • FMA 不是为了提高准确性,而不是提高性能吗?
  • 我曾经用 FMA 和 AVX 实现过 Mandelbrot。我的 Haswell 系统没有性能改进。 FMA 主要是提高准确率,并且可以减小代码大小。
  • 我做了,非 fma 实现只生成了 1 条 fma 指令,而 fma 实现产生了更多的 fma 指令。但我使用的是 Visual Studio 2013。
  • @R. - FMA 主要是为了提高性能。无论如何,这就是英特尔和 AMD 的定位方式,也是大多数讨论的方向。对于面向吞吐量有界的内核,FMA 可能会使其性能翻倍。它还允许芯片制造商将其标称 GFLOPS 评级提高一倍......

标签: c assembly avx2 fma


【解决方案1】:

re:您的编辑:您的代码具有三个依赖链(r1、r2 和 r3),因此它可以同时保持三个 FMA 运行。 Haswell 上的 FMA 延迟为 5c,每 0.5c 吞吐量一个,因此机器可以在飞行中维持 10 个 FMA。

如果您的代码处于循环中,并且一次迭代的输入不是由前一次迭代生成的,那么您可能会以这种方式获得 10 个 FMA。 (即没有涉及 FMA 的循环承载依赖链)。但由于您没有看到性能提升,因此可能存在一个 dep 链导致吞吐量受到延迟的限制。


您没有发布您从 MSVC 获得的 ASM,但您声称有关寄存器分配的内容。 xorps same,samea recognized zeroing idiom,它启动了一个新的依赖链,就像将寄存器用作只写操作数(例如,非 FMA AVX 指令的目标。)

代码正确但仍然包含 r3 对 r1 的依赖关系的可能性很小。确保您了解使用寄存器重命名的乱序执行允许不同的依赖链使用相同的寄存器。


顺便说一句,您应该使用__m256 r1 = _mm256_setzero_ps(); 而不是__m256 r1 = _mm256_xor_ps (r1, r1);。您应该避免使用您在其自己的初始化程序中声明的变量!当您使用未初始化的向量时,编译器有时会生成愚蠢的代码,例如从堆栈内存中加载垃圾,或者做一个额外的xorps

更好的是:

__m256 r1 = _mm256_mul_ps (rp1, m6);
r1 = _mm256_fmadd_ps (rp2, m7, r1);
r1 = _mm256_fmadd_ps (rp3, m8, r1);

这避免了需要 xorps 将累加器的 reg 归零。

在 Broadwell 上,mulps 的延迟低于 FMA。

在 Skylake 上,FMA/mul/add 都是 4c 延迟,每 0.5c 吞吐量一个。他们从端口 1 中删除了单独的加法器,并在 FMA 单元上执行此操作。他们减少了 FMA 单元的一个延迟周期。

【讨论】:

  • 我确实了解寄存器重命名机制。但是,如果您有 RAW 依赖项,这对您没有帮助。我第一次尝试展开循环导致编译器生成了这个,因此多个 FMA 指令无法并行调度。
  • @rohitsan: xor r1,r1 使用只写归零操作启动新的依赖链,即使 xor 通常依赖于其输入操作数。发布 asm,因为我不相信您声称编译器在 r3 的 FMA 与 r1 属于同一依赖链的情况下编写了代码,除非您没有显示源代码。
  • 我不是指上面的示例代码。我指的是上述代码的展开版本,其中我执行 6 组类似的 FMA 计算(依赖链)。
  • @rohitsan:r1r2r3 是否只是单独的累加器,您将在循环后组合它们?在这种情况下,如果不在循环内重复使用同一个累加器,您将获得更好的结果。不管怎样,理清你的部署链,这样你就可以有很多 FMA 在运行,你应该没问题。
  • 好吧,我尝试创建 6 个累加器 r1 到 r6,希望编译器生成代码以允许其中 2 个累加器在循环体的不同阶段并行执行。在这种情况下,循环迭代的次数(显然)将减半。但是,在这种情况下,我观​​察到 0 性能改进。我需要花一些时间来找出原因。对我来说,只编写汇编语言可能会更好,但出于多种原因(代码可维护性等),我一直在避免这样做。
【解决方案2】:

您没有提供包含环绕循环的完整代码示例(大概有 is 环绕循环),因此很难明确回答,但我看到的主要问题是FMA 代码的依赖链的延迟比乘法 + 加法代码要长得多。

FMA 代码中的三个块中的每一个都在执行相同的独立操作:

TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;

由于它是结构化的,每个操作都取决于前一个到期时间,因为每个操作都是读取和写入的总和。所以这串操作的延迟是 3 ops x 5 个周期/FMA = 15 个周期。

在没有 FMA 的重写版本中,TOTAL 上的依赖链现在已断开,因为您已经完成了:

TOTAL_1 = A1 * B1;  # 1
TOTAL_2 = A2 * B2;  # 2
TOTAL_3 = A3 * B3;  # 3

TOTAL_1_2 = TOTAL_1 + TOTAL2;  # 5, depends on 1,2
TOTAL = TOTAL_1_2 + TOTAL3;    # 6, depends on 3,5

前三个 MUL 指令可以独立执行,因为它们没有任何依赖关系。两个加法指令串行依赖于乘法。因此,该序列的延迟为 5 + 3 + 3 = 11。

因此,第二种方法的延迟较低,即使它使用更多 CPU 资源(总共发出 5 条指令)。那么,根据整个循环的结构,较低的延迟肯定有可能抵消 FMA 对该代码的吞吐量优势——如果它至少部分受到延迟限制的话。

对于更全面的静态分析,我强烈推荐Intel's IACA——它可以像上面那样进行循环迭代,并准确地告诉你瓶颈是什么,至少在最好的情况下是这样。它可以识别循环中的关键路径、您是否受到延迟限制等。

另一种可能性是您受内存限制(延迟或吞吐量),在这种情况下您还会看到 FMA 与 MUL + ADD 的类似行为。

【讨论】:

  • Haswell 有 5c FMA 和 mul, 3c add。 Broadwell 有 5c FMA、3c mul 和 add。 Skylake 有 4c FMA/mul/add。 (Skylake 放弃了单独的 FP 添加单元,并在 FMA 单元中完成了所有三个操作。这使添加吞吐量翻了一番。) OP 在 Haswell 上,因此您的答案正确地指出了那里的延迟优势。另外,要小心 IACA。您必须对其结果持保留态度,因为某些指令的 uop 计数与 Agner Fog 的表(或现实生活中的硬件,例如它认为 SnB 上的 shld 为 2 uops)不匹配。不过,这是一个很好的起点。
  • 如果改变操作顺序怎么办?嗯,不,不能想出任何让 FMA 成为添加链一部分的东西。像往常一样,多个累加器是保持更多操作进行的方式。
  • 确实如此。我主要发现 IACA 是准确的,并且至少在结果与传统智慧(或 Agner 的指南,自更新以来)不匹配的情况下,它做的是正确的事情。我记得的情况是端口 7 AGU 操作,它正确地编码了只能执行“简单”计算(无索引寄存器)的知识。 IACA 的一个大问题是它似乎不再积极开发,自 Haswell 以来没有更新。
  • 是的,如果他们打算放弃它,我希望他们开源它或其他东西。如果它在未来几代 CPU 与 Haswell 相差太大而无法使用时就死掉,那将是一种耻辱。
  • @PeterCordes - 是的,我在这里看不到将多个累加器与 FMA 一起使用的方法。正如我所提到的,如果不了解循环就很难说更多,但关键问题是,即使 FMA 与其他操作之一(MUL,Haswell 上也是 5c)具有相同的延迟(Haswell 上为 5c) - 事实上它将它们捆绑在一起增加了这里的依赖链。通常这不是问题,因为大多数内核都受吞吐量或内存限制,而不是依赖链延迟限制。
猜你喜欢
  • 2016-03-22
  • 2019-09-09
  • 1970-01-01
  • 2023-03-06
  • 2020-05-12
  • 2012-11-13
  • 1970-01-01
  • 2021-01-14
  • 2012-01-22
相关资源
最近更新 更多