生成慢vpermpd指令; 为什么?

use*_*717 6 c++ assembly signal-processing avx auto-vectorization

我有一个过滤器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]);
}
Run Code Online (Sandbox Code Playgroud)

perf 告诉我们这个循环在哪里热:

在此输入图像描述

vaddpdvfma231pd意义; 没有它们,我们肯定无法执行此操作.但缓慢vpermpd让我感到困惑.它完成了什么?

120*_*arm 5

这就是这个v[i - j]词.由于存储器访问作为直通存储器向后移动j增加时,洗牌需要扭转被从存储器读出的4个值的顺序.


Pet*_*des 5

vpermpd如果您的瓶颈是前端吞吐量(将微指令输入到无序核心),那么只会减慢您的速度。

vpermpd除非您使用的是 AMD CPU,否则并不是特别“慢”。(跨车道 YMM 洗牌在 AMD 的 CPU 上速度很慢,因为它们必须解码为比 256 位指令分割成的正常 2 128 位微指令更多的数据。 vpermpd在 Ryzen 上是 3 个微指令,或者在内存源上是 4 个微指令.)

在Intel上,vpermpd前端的内存源始终是2 uops(即使是非索引寻址模式也无法微熔丝)。卜

如果您的循环仅运行少量迭代,那么 OoO exec 可能能够隐藏 FMA 延迟,并且可能实际上是此循环 + 周围代码前端的瓶颈。这是可能的,考虑到循环外部的(低效的)水平求和代码得到了多少计数。

在这种情况下,也许展开 2 会有所帮助,但是对于非常小的计数,检查是否可以运行主循环的一次迭代的额外开销可能会变得昂贵。


否则(对于大量数据),您的瓶颈可能在于使用 FMA 作为d2v输入/输出操作数进行 4 到 5 个周期的循环承载依赖性。使用多个累加器展开,并使用指针增量而不是索引,将带来巨大的性能提升。比如 2 倍或 3 倍。

尝试 clang,它通常会为你做到这一点,并且它的 skylake/haswell 调整非常积极地展开。(例如clang -O3 -march=native -ffast-math

GCC-funroll-loops实际上并不使用多个累加器,IIRC。我已经有一段时间没看过了,我可能是错的,但我认为它只会使用相同的累加器寄存器重复循环体,而根本没有帮助并行运行更多的 dep 链。Clang 实际上会使用 2 或 4 个不同的向量寄存器来保存 的部分和d2v,并将它们添加到循环外的末尾。(但是对于尺寸,8个或更多会更好。 为什么mulss在Haswell上只需要3个周期,与Agner的指令表不同?

展开还使得使用指针增量变得值得,在英特尔 SnB 系列上的每个vaddpd和指令中节省 1 uop。vfmadd


为什么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的一部分。该链接是我对使用packed-single、使用两次分析GCC hsum 代码的错误的评论vhaddps

看起来循环后面的 hsum 实际上是“热”的,因此您正遭受 gcc 紧凑但效率低下的 hsum 的困扰。