使用pxor和xorps的混合是否会影响性能?

7
我发现了一个使用PCLMULQDQ实现的快速CRC计算。我看到,这些人在以下代码片段中大量混合使用pxor和xorps指令:
movdqa  xmm10, [rk9]
movdqa  xmm8, xmm0
pclmulqdq xmm0, xmm10, 0x11
pclmulqdq xmm8, xmm10, 0x0
pxor  xmm7, xmm8
xorps xmm7, xmm0

movdqa  xmm10, [rk11]
movdqa  xmm8, xmm1
pclmulqdq xmm1, xmm10, 0x11
pclmulqdq xmm8, xmm10, 0x0
pxor  xmm7, xmm8
xorps xmm7, xmm1

这样做有没有实际的原因呢?能提高性能吗?如果可以,那么它的实现原理是什么?或者说这只是一种编码风格,出于兴趣而已?


4
xorps 是一个三字节指令,而 pxor 则需要四个字节。除此之外, Agner Fog 的指令表微架构手册表明,在 AMD 上没有问题,因为 xorps 被视为整数域。但是,这在早期的 Intel 处理器上可能会影响性能,因为在那里,xorps 不能使用太多的执行单元,并且可能存在旁路延迟。 - EOF
@EOF:根据日期(以及它是由英特尔编写的),我猜测它是针对英特尔SnB/IvB进行调整的。UOP缓存的对齐似乎是最好的猜测,但也许有一些避免资源冲突以不延迟下一个PCLMUL的情况。 - Peter Cordes
1个回答

12
TL:DR: 看起来可能是针对这个特定代码序列的一些微体系结构特定调整。没有什么“通常推荐”的东西可以在其他情况下帮助。
经过进一步考虑,我认为@Iwillnotexist Idonotexist的理论最有可能:这是由一个非专业人士编写的,他认为这可能会有所帮助。寄存器分配是一个重要线索:通过选择所有重复使用的寄存器中的低8个,可以避免许多REX前缀。
XORPS在某些英特尔CPU上(Nehalem及更高版本)在“浮点”域中运行,而PXOR始终在“ivec”域中运行。由于将每个ALU输出直接连接到每个ALU输入以进行转发结果将很昂贵,因此CPU设计人员将它们分成不同的域。(转发可节省写回寄存器文件和重新读取的延迟)。域交叉可能需要额外的1个时钟周期(Intel SnB系列)或2个时钟周期(Nehalem)。更多阅读:What's the difference between logical SSE intrinsics?上我的答案。

我有两个理论:

  • 撰写此代码的人认为PXOR和XORPS可以提供更多的并行性,因为它们不会互相竞争。(这是错误的:PXOR可以在所有矢量ALU端口上运行,但XORPS无法实现)。

  • 这是一些非常巧妙调整的代码,有意创建旁路延迟,避免资源冲突导致执行下一个PCLMULQDQ操作的延迟。(或者像EOF建议的那样,代码大小/对齐可能与此有关)。

代码的版权声明显示“2011-2015英特尔”,因此有必要考虑它可能对某些最近的英特尔CPU有所帮助,并不仅仅是基于对英特尔CPU工作方式的误解。 Nehalem是第一个包含PCLMULQDQ的CPU,这是英特尔的产品,如果有什么问题它将被调整以在AMD CPU上表现不佳。代码历史记录不在git repo中,只有添加当前版本的May 6th提交。

Intel白皮书(2009年12月)只使用了PXOR而没有使用XORPS来实现2x pclmul / 2x xor块。

Agner Fog的表甚至没有显示Nehalem上PCLMULQDQ的uops数量或它们需要哪些端口。它的延迟为12个时钟周期,吞吐量为每8个时钟周期一个,因此它可能类似于Sandy/Ivybridge的18个uop实现。Haswell将其缩减为3个uop(2p0 p5),而在Broadwell(p0)和Skylake(p5)上仅需1个uop即可运行。

XORPS只能在port5上运行(直到Skylake才在所有三个矢量ALU端口上运行)。在Nehalem上,当其中一个输入来自PXOR时,它有2个时钟周期的旁路延迟。在SnB系列CPU上,Agner Fog说:

在某些情况下,使用错误类型的洗牌或布尔指令时没有旁路延迟。

所以我认为在SnB上从PXOR到XORPS的转发实际上没有额外的旁路延迟,因此唯一的影响是它只能在端口5上运行。在Nehalem上,它可能会延迟XORPS直到PSHUFB完成。
在主循环展开中,XOR之后有一个PSHUFB,为下一个PCLMUL设置输入。 SnB/IvB可以在p1/p5上运行整数洗牌(与Haswell及更高版本不同,在p5上仅有一个256位宽度的AVX2洗牌单元)。
由于争夺为下一个PCLMUL设置输入所需的端口似乎没有用处,如果在针对SnB进行调优时进行了此更改,则我的最佳猜测是代码大小/对齐。

在需要超过4个微操作的CPU上,PCLMULQDQ会进行微代码处理。这意味着每个PCLMULQDQ需要一个完整的微操作缓存行。由于只有3个微操作缓存行可以映射到同一32B x86指令块,这意味着大部分代码在SnB/IvB上根本无法适应微操作缓存。每个微操作缓存行只能缓存连续的指令。根据英特尔的优化手册:

Way(微操作缓存行)中的所有微操作表示静态连续的代码指令,并且它们的EIP位于同一对齐的32字节区域内。

这似乎是类似于在循环中使用整数DIV的问题: 在英特尔SnB系列CPU上涉及微代码指令的循环分支对齐。通过正确的对齐,您可以使其从uop缓存(在英特尔性能计数器术语中为DSB)中运行。@Iwillnotexist Idonotexist在Haswell CPU上进行了一些有用的测试,显示它们防止从loopback缓冲区运行。(在英特尔术语中为LSD)。
在Haswell和之后的处理器上,PCLMULQDQ没有微码,因此可以与其他指令一起放入同一个uop缓存行中。

对于以前的CPU,调整代码以在更少的位置上破坏uop缓存可能是值得尝试的。另一方面,切换uop缓存和传统解码器可能比仅从解码器运行更糟糕。

此外,我不知道这样大的展开是否真的有帮助。这可能在SnB和Skylake之间差异很大,因为微码指令对管道来说非常不同,而SKL甚至可能不会在PCLMUL吞吐量上出现瓶颈。


2
这段代码是为Westmere编写的,正如PDF本身所述。我认为可能是写这段代码的博士生,并且他并不确切知道自己在做什么。证据:1.随机使用pxor/pxor而不是pxor/xorps。2.没有使用mov[au]ps进行内存加载。3.可怕的寄存器分配,特别是xmm10,几乎将所有指令大小增加了1个字节。4.pclmulqdq在Westmere上需要18个uops,最佳吞吐量为每8个时钟周期1个,编码带有前缀7个字节,因此像对齐和端口调度这样的微观优化非常过早。在这里,pxor/xorps是一种迷信行为。 - Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist: 嗯,我也注意到了一些可疑的寄存器分配。但是这段代码的版权是2011-2015年,而PDF文档是2009年发布的。该PDF没有提到这个代码实现,这就是为什么它很可能是针对较新的CPU编写的原因。特别是我们只看到了2015年的版本,甚至没有2011年的版本。但是没错,看起来这并不是好代码。这可能在某些CPU上有所帮助,但我认为你的理论更有可能是正确的,这只是一堆垃圾。 - Peter Cordes
2
@IwillnotexistIdonotexist:我的问题反馈主要涉及汇编/x86/SSE/AVX/计算机架构/无锁/标准原子操作,所以我会回答好的问题。有很多人写了很好的C/C++答案,而我永远无法跟上这些标签中的问题数量。(此外,当我看微优化的C/C++问题时,撰写答案通常需要很长时间,因为需要比较其他答案中所有随意的好坏想法的汇编代码。因此,我限制自己只关注我能够跟上的问题反馈,因为我不能不看这些问题。) - Peter Cordes
2
虽然我正在训练自己,不再对那些无聊的新手汇编语言问题耿耿于怀,这些问题只是在同一个问题上稍作变化,已经问了一百遍,还有那些五花八门的16位DOS代码墙,里面有5个不同的错误。我很难理解为什么有些人认为在没有使用调试器的情况下打扰别人解决他们的问题是可以接受的。 - Peter Cordes
1
@AlexGuteniev:或许对于Nehalem架构可以这样做,但是对于目前的CPU来说不行。XOR归零操作本身就很特殊,尽管在AMD CPU上还需要一个执行端口来写入零。Sandy Bridge系列可以高效地从FP布尔值转移到SIMD整数,而不受布尔值所在的执行端口的限制(例如Skylake上它们不仅限于端口5)。这不是“没有原因”,xorps可以节省代码大小。(或者至少它不是毫无好处,我不知道为什么MSVC要这么做。) - Peter Cordes
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接