vpermpd 应该只会在您的瓶颈是前端吞吐量(将 uops 输入到无序内核中)时减慢您的速度。
vpermpd 并不是特别“慢”,除非您使用的是 AMD CPU。 (车道交叉 YMM shuffle 在 AMD 的 CPU 上速度较慢,因为它们必须解码为比 256 位指令拆分成的普通 2 128 位微指令更多。vpermpd 在 Ryzen 上是 3 微指令,或者 4内存源。)
在 Intel 上,带有内存源的 vpermpd 对于前端始终为 2 uop(即使是非索引寻址模式也无法进行微熔断)。布
如果您的循环仅运行少量迭代,那么 OoO exec 可能能够隐藏 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 的部分和,并将它们添加到循环外的末尾。 (但对于 large 尺寸,8 或更多会更好。Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?)
展开还值得使用指针增量,从而在英特尔 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 个微指令,其中 2 个需要随机端口。也许尝试更新版本的 GCC,比如 8.2。虽然我不确定避免 VHADDPS/PD 是否是关闭GCC bug 80846 的一部分。该链接是我对使用 packed-single 分析 GCC 的 hsum 代码的错误的评论,使用 vhaddps 两次。
看起来你在循环之后的 hsum 实际上是“热的”,所以你正在遭受 gcc 紧凑但效率低下的 hsum。