C 英特尔FMA指令提供零性能优势
使用Haswell的FMA指令考虑以下指令序列:C 英特尔FMA指令提供零性能优势,c,assembly,avx2,fma,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, r
__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版本提供一些性能优势
但不幸的是,在这种情况下,性能改进为零
有人能帮我理解为什么吗
我在基于core i7-4790的机器上测量了这两种方法
更新:
我分析了生成的机器代码,确定MSFT VS2013 C++编译器生成机器代码,使得R1和R2的依赖链可以并行调度,因为HasWELL有2个FMA管道。 r3必须在r1之后调度,因此在这种情况下,第二个FMA管道处于空闲状态
我认为,如果我展开循环以执行6组FMA,而不是3组,那么我可以在每次迭代中让所有FMA管道保持忙碌 不幸的是,当我在本例中检查程序集转储时,MSFT编译器没有选择允许我正在寻找的并行分派类型的寄存器分配,并且我验证了我没有得到我正在寻找的性能提高有什么方法可以更改我的C代码(使用内部函数)以使编译器生成更好的代码吗?您没有提供包含周围循环的完整代码示例(可能有一个周围循环),因此很难确定地回答,但我看到的主要问题是,FMA代码的依赖链的延迟比乘法+加法代码的延迟要长得多 FMA代码中的三个模块中的每一个都在执行相同的独立操作:
TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;
由于是结构化的,每个操作都依赖于上一个到期日,因为每个操作都读取和写入总计。因此,这串操作的延迟是3次操作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指令可以独立执行,因为它们没有任何依赖关系。两条add指令串行依赖于乘法。因此,该序列的延迟为5+3+3=11
因此,第二种方法的延迟更低,即使它使用更多的CPU资源(总共发出5条指令)。当然,根据整个循环的结构,较低的延迟抵消了FMA对该代码的吞吐量优势是可能的——如果它至少部分受延迟限制的话
对于一个更全面的静态分析,我强烈推荐-它可以像上面那样进行循环迭代,并准确地告诉您瓶颈是什么,至少在最佳情况下是这样的。它可以识别循环中的关键路径,是否受延迟限制,等等
另一种可能是内存受限(延迟或吞吐量),在这种情况下,您还将看到FMA与MUL+ADD的类似行为。re:您的编辑:您的代码有三个依赖链(r1、r2和r3),因此它可以同时保持三个FMA。Haswell上的FMA是5c延迟,每0.5c吞吐量一个,因此机器可以在飞行中维持10个FMA 如果您的代码处于循环中,并且一次迭代的输入不是由上一次迭代生成的,那么您可以通过这种方式获得10个FMA。(即没有涉及FMA的循环携带依赖链)。但是,因为您没有看到性能增益,所以可能有一个dep链导致吞吐量受到延迟的限制
你没有发布你从MSVC获得的ASM,但是你声明了一些关于注册分配的信息
xorps same,same
启动新的依赖链,就像使用寄存器作为只写操作数一样(例如,非FMA AVX指令的目标)
代码不太可能是正确的,但仍然包含r3对r1的依赖关系。请确保您了解寄存器重命名的无序执行允许单独的依赖链使用同一寄存器
顺便说一句,而不是
\uuuum256r1=\umm256\uxor\ups(r1,r1)代码>,您应该使用\uuuuum256 r1=\umm256\usetzero\ups()代码>。您应该避免使用您在自己的初始值设定项中声明的变量!当您使用未初始化的向量时,编译器有时会生成愚蠢的代码,例如从堆栈内存加载垃圾,或者执行额外的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单元的一个延迟周期。是的,我想我尝试了类似的方法,得到了相同的结果-我还尝试了混合FMA/AVX2,看看是否存在可以利用的并行性,但同样没有好处。FMA不是为了提高准确性,而不是性能吗?我曾经用FMA和AVX实现过Mandelbrot。我的Haswell系统没有性能改进。FMA主要是提高准确性和减少代码大小。我这样做了,非FMA实现只生成了1条FMA指令,FMA实现生成了更多的FMA指令。但我使用的是Visual Studio 2013。@R.-FMA肯定主要是为了提高性能。这就是英特尔和AMD对它的定位,也是大部分讨论的方向。对于吞吐量有限的面向内核,FMA可能使其性能加倍。它还允许芯片制造商将其名义GFLOPS评级提高一倍……Haswell拥有5c FMA和mul,3c add。Broadwell有5c FMA、3c mul和add。Skylake有4c FMA/mul/add。(Skylake放弃了单独的FP add单元,并在FMA单元中完成了所有这三项。这是一个doub