英特尔FMA指令提供零性能优势

时间:2016-02-25 19:51:28

标签: c assembly avx2 fma

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

2 个答案:

答案 0 :(得分: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's IACA - 它可以像上面那样进行循环迭代,并告诉你确切的瓶颈是什么,至少在最好的情况下。它可以识别循环中的关键路径,无论您是否有延迟限制等等。

另一种可能性是你受内存限制(延迟或吞吐量),你也会看到FMA与MUL + ADD的相似行为。

答案 1 :(得分: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的依赖性。确保您了解使用寄存器重命名的无序执行允许单独的依赖链使用相同的寄存器。

BTW,而不是__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将累加器的reg归零。

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

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