4

考虑使用 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 代码(使用内在函数)以使编译器能够生成更好的代码?

4

2 回答 2

6

您没有提供包含周围循环的完整代码示例(大概有一个周围循环),因此很难明确回答,但我看到的主要问题是您的 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 的 IACA——它可以像上面那样进行循环迭代,并准确地告诉你瓶颈是什么,至少在最好的情况下是这样。它可以识别循环中的关键路径,是否受延迟限制等。

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

于 2016-02-26T02:20:34.877 回答
1

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

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


您没有发布从 MSVC 获得的 ASM,但您声称有关寄存器分配的内容。 xorps same,same一种公认​​的清零习惯用法,它启动一个新的依赖链,就像使用寄存器作为只写操作数(例如,非 FMA AVX 指令的目标。)

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


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

更好的是:

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

这避免xorps了累加器需要将寄存器归零。

在 Broadwell 上,mulps延迟比 FMA 低。

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

于 2016-03-26T01:27:08.983 回答