考虑使用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代码(使用本质)来使编译器生成更好的代码吗?
发布于 2016-02-26 02:20:34
您没有提供包含周围循环的完整代码示例(假定有一个周围的循环),因此很难确定地回答,但我看到的主要问题是,FMA代码的依赖链的延迟比乘法+加法代码的延迟要长得多。
FMA代码中的三个块中的每一个都在执行相同的独立操作:
TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;由于每个操作都是结构化的,因此每个操作都依赖于上一个到期的操作,因为每个操作都是读写总计的。因此,这个操作串的延迟时间是3 ops x5周期/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。
发布于 2016-03-26 01:27:08
re:您的编辑:您的代码有三个依赖链(r1、r2和r3),因此它可以同时运行三个FMA。在Haswell上的FMA是5c延迟,每0.5c吞吐量一个,因此该机器可以在飞行中维持10个FMA。
如果您的代码在一个循环中,并且一个迭代的输入不是由前一个迭代生成的,那么您可能会得到10个这样的FMA。(即没有涉及FMAs的循环携带依赖链)。但是,由于您没有看到perf增益,所以可能有一个dep链导致吞吐量受到延迟的限制。
你没有发布你从MSVC获得的ASM,但是你声称一些关于注册分配的东西。xorps same,same是启动新依赖链的a recognized zeroing idiom,就像使用寄存器作为只写操作数(例如,非FMA指令的目的地)。
代码不太可能是正确的,但仍然包含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操作。
在布罗德威尔,mulps的延迟比FMA低。
在Skylake上,FMA/mul/add都是4c延迟,每0.5c吞吐量就有一个。他们从port1中分离出加法器,并在FMA单元上进行操作。他们削减了FMA单位的延迟周期。
https://stackoverflow.com/questions/35636955
复制相似问题