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

roh*_*san 4 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);
Run Code Online (Sandbox Code Playgroud)

可以使用非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);
Run Code Online (Sandbox Code Playgroud)

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

但不幸的是,在这种情况下,性能提高了零(0).

谁能帮我理解为什么?

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

更新:

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

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

我认为如果我展开循环来做6组FMA而不是3组,那么我可以在每次迭代时保持所有FMA管道忙碌.

不幸的是,当我在这种情况下检查汇编转储时,MSFT编译器没有选择允许我正在寻找的并行调度类型的寄存器分配,并且我验证了我没有得到我正在寻找的性能提升对于.

有没有办法可以改变我的C代码(使用内在函数)来使编译器生成更好的代码?

Bee*_*ope 6

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

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

TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;
Run Code Online (Sandbox Code Playgroud)

由于它是结构化的,每个操作取决于先前的到期,因为每个操作读取和写入总数.因此,此操作字符串的延迟为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
Run Code Online (Sandbox Code Playgroud)

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

因此第二种方法的延迟较低,即使它使用了更多的CPU资源(总共发出了5条指令).当然,根据整个循环的结构,可以确定的是,较低的延迟会抵消FMA对此代码的吞吐量优势 - 如果它至少部分是延迟限制的话.

对于更全面的静态分析,我强烈推荐英特尔的IACA - 它可以进行如上所述的循环迭代,并告诉您确切的瓶颈是什么,至少在最好的情况下.它可以识别循环中的关键路径,无论您是否有延迟限制等.

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

  • 是的,如果他们要放弃它,我希望他们开源或者开源.如果它的下一代CPU与Haswell有太大的不同之处,它就会变得有用,那将是一种耻辱. (3认同)
  • Haswell有5c FMA和mul,3c添加.Broadwell拥有5c FMA,3c mul和add.Skylake拥有4c FMA/mul/add.(Skylake放弃了单独的FP添加单元并完成了FMA单元中的所有三个.这使增加的吞吐量加倍.)OP在Haswell上,所以你的答案正确地指出了那里的延迟胜利.另外,要小心IACA.你必须把它的结果用一粒盐,因为它的uop计数与某些指令不匹配Agner Fog的表(或现实生活中的硬件,例如它认为SnB上的shld是2 uop).不过,这是一个很好的起点. (2认同)
  • 确实.我大多发现IACA是准确的,并且在至少一个结果与传统智慧(或Agner指南,自更新)不符的情况下,它做了正确的事情.我记得的情况是端口7 AGU操作,它正确地编码了只能执行"简单"计算(没有索引寄存器)的知识.IACA的一个大问题是它似乎不再被积极开发,自Haswell以来没有更新. (2认同)
  • @PeterCordes - 是的,我在这里找不到使用FMA的多个累加器的方法.正如我所提到的,在不理解循环的情况下很难说更多,但关键问题是即使FMA具有与其他操作(MUL,也是Haswell上的5c)相同的延迟(Haswell上的5c) - 事实上它将它们捆绑在一起会增加依赖链.通常这不是问题,因为大多数内核是吞吐量或内存绑定,而不是依赖链延迟限制. (2认同)