正在生成缓慢的 vpermpd 指令;为什么?
Slow vpermpd instruction being generated; why?
我有一个过滤器 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
并不是特别 "slow" 除非你使用的是 AMD CPU。 (跨车道 YMM 洗牌在 AMD 的 CPUs 上很慢,因为它们必须解码成比 256 位指令分成的正常 2 个 128 位微指令更多。vpermpd
是 3 Ryzen 上的 uops,或带有内存源的 4。)
在 Intel 上,vpermpd
带有内存源的前端总是 2 微指令(即使是非索引寻址模式也不能微融合)。卜
如果您的循环只有 运行s 进行少量迭代,那么 OoO exec 可能能够隐藏 FMA 延迟,并且可能实际上是该循环前端的瓶颈 +周边代码。这是可能的,考虑到循环外的(低效)水平和代码得到了多少计数。
在那种情况下,也许按 2 展开会有所帮助,但也许检查是否可以的额外开销 运行 即使主循环的一次迭代对于非常小的计数也会变得昂贵。
否则(对于大量计数)您的瓶颈可能在于使用 d2v
作为 input/output 操作数。展开多个累加器和指针增量而不是索引,将是一个巨大的性能胜利。喜欢 2 倍或 3 倍。
试试 clang,它通常会为您做到这一点,而且它的 skylake/haswell 调整展开得非常积极。 (例如 clang -O3 -march=native -ffast-math
)
带有 -funroll-loops
的 GCC 实际上并不使用多个累加器,IIRC。我有一段时间没看了,我可能是错的,但我认为它只会使用相同的累加器寄存器重复循环体,对并行 运行 更多 dep 链毫无帮助。 Clang 实际上会使用 2 或 4 个不同的向量寄存器来保存 d2v
的部分和,并将它们添加到循环外的末尾。 (但对于 large 尺寸,8 个或更多会更好。)
展开也使得使用指针增量变得有价值,在英特尔 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 的一部分。 link 是我对使用 packed-single 分析 GCC 的 hsum 代码的错误的评论,使用 vhaddps
两次。
看起来循环后的 hsum 实际上是 "hot",所以你正在为 gcc 的紧凑但低效的 hsum 而苦恼。
我有一个过滤器 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
并不是特别 "slow" 除非你使用的是 AMD CPU。 (跨车道 YMM 洗牌在 AMD 的 CPUs 上很慢,因为它们必须解码成比 256 位指令分成的正常 2 个 128 位微指令更多。vpermpd
是 3 Ryzen 上的 uops,或带有内存源的 4。)
在 Intel 上,vpermpd
带有内存源的前端总是 2 微指令(即使是非索引寻址模式也不能微融合)。卜
如果您的循环只有 运行s 进行少量迭代,那么 OoO exec 可能能够隐藏 FMA 延迟,并且可能实际上是该循环前端的瓶颈 +周边代码。这是可能的,考虑到循环外的(低效)水平和代码得到了多少计数。
在那种情况下,也许按 2 展开会有所帮助,但也许检查是否可以的额外开销 运行 即使主循环的一次迭代对于非常小的计数也会变得昂贵。
否则(对于大量计数)您的瓶颈可能在于使用 d2v
作为 input/output 操作数。展开多个累加器和指针增量而不是索引,将是一个巨大的性能胜利。喜欢 2 倍或 3 倍。
试试 clang,它通常会为您做到这一点,而且它的 skylake/haswell 调整展开得非常积极。 (例如 clang -O3 -march=native -ffast-math
)
带有 -funroll-loops
的 GCC 实际上并不使用多个累加器,IIRC。我有一段时间没看了,我可能是错的,但我认为它只会使用相同的累加器寄存器重复循环体,对并行 运行 更多 dep 链毫无帮助。 Clang 实际上会使用 2 或 4 个不同的向量寄存器来保存 d2v
的部分和,并将它们添加到循环外的末尾。 (但对于 large 尺寸,8 个或更多会更好。
展开也使得使用指针增量变得有价值,在英特尔 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 的一部分。 link 是我对使用 packed-single 分析 GCC 的 hsum 代码的错误的评论,使用 vhaddps
两次。
看起来循环后的 hsum 实际上是 "hot",所以你正在为 gcc 的紧凑但低效的 hsum 而苦恼。