我有一个过滤器m_f
,它通过
v
Real d2v = m_f[0]*v[i];
for (size_t j = 1; j < m_f.size(); ++j)
{
d2v += m_f[j] * (v[i + j] + v[i - j]);
}
perf
告诉我们此循环最热的地方:
vaddpd
和vfma231pd
很有意义;当然,没有它们,我们将无法执行此操作。但是缓慢的vpermpd
使我感到困惑。它是做什么的?
答案 0 :(得分:5)
这是>>> convert(22)
'0+22.00'
>>> convert(23454.22123)
'234+54.22'
>>> convert(-56732)
'-567+32.00'
>>>
一词。由于内存访问随着v[i - j]
的增加而在内存中后移,因此有必要进行随机排序以反转从内存中读取的4个值的顺序。
答案 1 :(得分:4)
vpermpd
仅在您的瓶颈是前端吞吐量(将uops馈入乱序的内核)时才在这里拖慢您的速度。
vpermpd
并不是特别“慢”。 (跨YMM混洗在AMD CPU上的运行速度较慢,因为它们必须解码成比将256位指令分成的普通2 128位微指令还要多。vpermpd
在Ryzen上为3微码,或者4,并带有存储源。)
在Intel上,带有内存源的vpermpd
的前端始终为2 oups(即使非索引寻址模式也不能微熔)。布
如果您的循环只运行一小部分迭代,那么OoO执行人员可能能够隐藏FMA延迟,并且实际上可能是该循环+周围代码的前端瓶颈。鉴于有多少计数循环外的(低效)水平和代码得到了计数,这是有可能的。
在那种情况下,也许将2解开会有所帮助,但是也许检查一次是否可以运行一次主循环的额外开销可能会因为很小的计数而变得昂贵。
否则(对于较大数量),您的瓶颈可能取决于将d2v
作为输入/输出操作数执行FMA的4到5循环循环依赖。使用多个累加器展开,并且指针增加而不是索引,将是巨大的性能优势。就像2倍或3倍。
尝试使用clang,它通常会为您做到这一点,并且它的skylake / haswell调整会非常积极地展开。 (例如clang -O3 -march=native -ffast-math
)
带有-funroll-loops
的GCC实际上并未使用多个累加器,即IIRC。我好一阵子没看错了,但我可能错了,但是我认为它只会使用相同的累加器寄存器来重复循环体,根本无法并行运行更多的dep链。 Clang实际上将使用2或4个不同的向量寄存器来保存d2v
的部分和,并将其添加到循环外的末尾。 (但是对于大大小,最好8个或更多。Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?)
展开也将使它值得使用指针增量,从而在Intel SnB系列的vaddpd
和vfmadd
指令中分别节省了1个uop。
为什么将m_f.size();
保留在内存(cmp rax, [rsp+0x50]
中而不是寄存器中??您在禁用严格混叠的情况下进行编译吗?循环不写内存,所以这很奇怪。除非编译器认为循环将运行很少的迭代,否则是否值得在循环外加载最大代码?
每次迭代都复制和否定j
似乎是错过了优化。显然,从循环外的2个寄存器开始以及每次循环迭代add rax,0x20
/ sub rbx, 0x20
而不是MOV + NEG效率更高。
如果您对此有[mcve]的信息,则可能会发现一些错过的优化,可能会报告为编译器错误。这个asm对我来说就像是gcc输出。
令人失望的是,gcc使用了如此糟糕的水平和习惯用法。 VHADDPD为3 uops,其中2个需要shuffle端口。也许尝试使用较新版本的GCC,例如8.2。尽管我不确定避免VHADDPS / PD是否作为固定关闭GCC bug 80846的一部分。该链接是我对使用vhaddps
两次使用打包的单一代码分析GCC的hsum代码的错误的评论。
循环后的hsum看起来实际上很“热”,因此您正在遭受gcc紧凑但效率低下的hsum的困扰。