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代码(使用内在函数)来使编译器生成更好的代码?
你没有提供包含周围循环的完整代码示例(可能是周围的循环),因此很难明确回答,但我看到的主要问题是你的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的类似行为.