为什么使用寄存器R12时POP慢?

问题描述

在最近的Intel cpu上,POP指令通常每个周期2条指令的吞吐量。但是,当使用寄存器R12(或RSP,除了前缀外具有相同的编码)时,如果指令通过旧式解码器,则吞吐量下降到每个周期1(吞吐量保持在大约如果微指令来自DSB,则每个周期2次。

可以使用nanoBench如下复制:

sudo ./nanoBench.sh -asm "pop R12"

在Haswell机器上进行的进一步实验显示以下内容:当添加1至4 nops时,

sudo ./nanoBench.sh -asm "pop R12; nop;"
sudo ./nanoBench.sh -asm "pop R12; nop; nop;"
sudo ./nanoBench.sh -asm "pop R12; nop; nop; nop;"
sudo ./nanoBench.sh -asm "pop R12; nop; nop; nop; nop;"

执行时间增加到2个周期。当添加第五个nop时,

sudo ./nanoBench.sh -asm "pop R12; nop; nop; nop; nop; nop;"

执行时间增加到3个周期。这表明在与pop R12指令相同的周期内无法解码其他指令。 (当使用不同的寄存器,例如R11时,最后一个示例需要1.5个周期。)

在Skylake上,在1到3 nops之间相加时,执行时间停留在1个周期,而在4到7 nops之间则增加到2。这表明pop R12是需要复杂解码器的指令,即使它只有一个µop(另请参见Can the simple decoders in recent Intel microarchitectures handle all 1-µop instructions?

为什么使用寄存器POPR12指令的解码方式不同?还有其他适用的说明吗?

解决方法

解决方法:pop r/m64的{​​{1}}编码没有这种解码代价。 (感谢@Andreas测试我的猜测。)

pop r12

db 0x41,0x8f,0xc4 ; REX.B=1 8F /0 pop r/m64 = pop r12 的标准编码与pop r12具有相同的操作码字节,只是REX有所不同。 (short form encoding将寄存器号放在该1字节的低3位中。)

pop rsp即使在解码器中也有特殊情况;在Haswell上,它是3 uops 1 ,因此很明显只有复杂的解码器才能对其进行解码。如果至少对于 this 哪个解码器的主要过滤器可以通过操作码字节(不考虑前缀)对主要的过滤器进行解码,这也是有道理的 >操作码组。无论这是否真的反映出确切的内部原理,它至少是一个有用的思维模型,有助于理解pop modrm为什么没有这种效果。 (尽管通常情况下,您只将pop rsp与存储目标一起使用,这将意味着多码率,因此仅意味着复数解码器。)

pop r12在Haswell上总共为2,而大多数pop r/m64指令为1。但是可能额外的uop只是在发布/重命名(由于读取RSP)期间插入了堆栈同步,而在解码期间未插入 。 @Andreas报告push rsppush reg在解码器中均未显示任何特殊效果(我假设是uop缓存)。只有1个微融合uop,执行时带有/不带有堆栈同步uop。

push rsp这样的操作码在不同指令之间共享相同的前导字节(将mod push r12字段作为额外的操作码字节重载)可能很有趣,如果有一些单uup指令与多uu指令共享一个前导字节。就像FF /0 inc r/m32 SHL r / m8,imm8与/r RCL r / m8,imm8一样。 http://ref.x86asm.net/coder64.html。但是带有存储目标的SHL已经可以是多个微指令,因此无论如何,简单的解码器可能会乐观地尝试它,如果结果是单微指令,它会成功吗?尽管也许C0 /4在简单的解码器中较早解脱,而不是检测REX前缀。

对于英特尔来说,花更多的钱来确保常见指令(例如立即移位)可以有效地进行解码是比有意义的C0 /2(通常只能在函数结尾中找到的指令)更有意义的,并且因此通常不在内部循环中。只有包含函数调用的较大循环。


脚注1 pop r12很特殊,因为它只是pop r12。 (或者如手册所述, POP ESP指令会在将栈顶旧数据写入目标之前将堆栈指针(ESP)递增。 Haswell的3-uop实现似乎是不必要的。与pop rsp相同的1个uop(我认为故障条件是相同的),但这可能通过在正常的mov rsp,[rsp]解码方式中添加uop来节省了解码器中的晶体管(可能隐含地需要堆栈-总共要同步3个),而不是将其视为一个单独的指令?mov rsp,[rsp]很少使用,因此它的性能无关紧要。

也许以16位pop reg的情况将那个字节解码为1个纯负载uop是一个问题? x86机器代码中没有pop rsp寻址方式,并且可能的局限性扩展到16位AGU的内部uops。除此之外,我认为pop sp[sp]的可能故障原因是相同的。

根据@Andreas的测试,

pop(缩写形式)最终最终会解码为正常的1 uop,而堆栈同步uups不会比重复弹出其他寄存器多。由于无法在简单的解码器中解码,而不会受到mov专门解码到的任何额外内容的惩罚。


也许GAS,NASM和其他汇编程序应该获得补丁,才能使用modrm编码对pop r12进行编码,尽管可能不是默认设置。解码器吞吐量通常不是问题,因此默认情况下花费额外的代码大小字节是不希望的。尤其是如果对其他架构(例如AMD或Silvermont系列)没有影响。

和/或GCC应该使用R12作为保存/恢复呼叫保留注册表的最后选择吗? (R12 always needs a SIB byte在寻址模式中也用作基址,因此,如果编译器不打算避免在其中保留指针,这也是避免使用它的另一个原因。)也许可以调度r12用于高效解码,在多uop pop rsp之前的后面还有3个其他pop(或其他single-uop isns)。