我有一个过滤器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
让我感到困惑。它在完成什么?
我有一个过滤器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
让我感到困惑。它在完成什么?
这就是v[i - j]
术语。由于内存访问随着内存的增加而向后移动j
,因此有必要进行随机播放以反转从内存中读取的 4 个值的顺序。
vpermpd
如果您的瓶颈是前端吞吐量(将 uops 馈送到无序核心),则应该只在此处减慢您的速度。
vpermpd
除非您使用的是 AMD CPU,否则它并不是特别“慢”。(车道交叉 YMM shuffle 在 AMD 的 CPU 上速度较慢,因为它们必须解码为比 256 位指令拆分成的正常 2 128 位 uop 更多。 vpermpd
在 Ryzen 上是 3 uop,或者在内存源上是 4 .)
在 Intel 上,vpermpd
前端的内存源始终为 2 uop(即使是非索引寻址模式也无法进行微熔断)。卜
如果您的循环仅运行少量迭代,那么 OoO exec 可能能够隐藏 FMA 延迟,并且可能实际上是该循环 + 周围代码的前端瓶颈。这是可能的,因为循环外的(低效的)水平和代码得到了多少计数。
在这种情况下,也许展开 2 会有所帮助,但检查是否可以运行主循环的一次迭代的额外开销对于非常小的计数可能会变得昂贵。
d2v
否则(对于大量计数),您的瓶颈可能在于使用作为输入/输出操作数的 FMA 进行 FMA 的 4 到 5 个循环循环携带依赖。使用多个累加器展开,以及指针增量而不是索引,将是一个巨大的性能提升。比如 2 倍或 3 倍。
试试 clang,它通常会为你做到这一点,它的 skylake/haswell 调音非常积极地展开。(例如clang -O3 -march=native -ffast-math
)
GCC with-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 个微指令,其中 2 个需要随机端口。也许尝试更新版本的 GCC,比如 8.2。虽然我不确定避免 VHADDPS/PD 是否是关闭GCC 错误 80846的一部分。该链接是我对使用打包单次使用vhaddps
两次分析 GCC 的 hsum 代码的错误的评论。
看起来你在循环之后的 hsum 实际上是“热的”,所以你正遭受 gcc 紧凑但效率低下的 hsum 的困扰。