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

And*_*bel 7 performance x86 intel cpu-architecture micro-optimization

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

这可以使用nanoBench重现,如下所示:

sudo ./nanoBench.sh -asm "pop R12"
Run Code Online (Sandbox Code Playgroud)

在 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;"
Run Code Online (Sandbox Code Playgroud)

执行时间增加到 2 个周期。添加第 5 个时nop

sudo ./nanoBench.sh -asm "pop R12; nop; nop; nop; nop; nop;"
Run Code Online (Sandbox Code Playgroud)

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

在 Skylake 上,在 1 和 3 之间添加时执行时间保持在 1 个周期nops,并且在 4 和 7 之间增加到 2 nops。这表明这pop R12是一条需要复杂解码器的指令,即使它只有一个 µop(另请参阅最近英特尔微架构中的简单解码器能否处理所有 1-µop 指令?

为什么POP在使用 register 时指令的解码方式不同R12?是否有任何其他说明也是这种情况?

Pet*_*des 9

解决方法: 的pop r/m64编码pop r12没有这种解码惩罚。(感谢@Andreas 测试我的猜测。)

db  0x41, 0x8f, 0xc4        ; REX.B=1  8F /0  pop r/m64  = pop r12
Run Code Online (Sandbox Code Playgroud)

的标准编码与pop r12具有相同的操作码字节pop rsp,仅通过 REX 不同。(短格式编码将寄存器编号放在该 1 个字节的低 3 位中)。

pop rsp即使在解码器中也是特殊情况;在 Haswell 上它是 3 uops 1,所以很明显只有复杂的解码器才能解码它。 pop r12如果哪个解码器可以解码哪个指令的主要过滤是通过操作码字节(不考虑前缀),至少对于组操作码,那么受到惩罚也是有意义的。无论这是否真的反映了确切的内部结构,它至少是一个有用的心理模型,可以理解为什么 pop modrm 没有这种效果。(虽然通常你只会使用pop r/m64一个内存目的地,这意味着多 uop,因此只有复杂的解码器。)

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

FF /0 inc r/m32在不同指令之间共享相同前导字节的操作码(将 modrm/r字段重载为额外的操作码字节)可能很有趣,如果有一些单 uop 指令与多 uop 指令共享一个前导字节。就像C0 /4SHL r/m8、imm8 与C0 /2RCL r/m8、imm8。 http://ref.x86asm.net/coder64.html。但是具有内存目的地的 SHL 已经可以是多个 uop,所以无论如何它可能会被简单的解码器乐观地尝试,如果结果是单 uop 则成功?虽然可能会pop r12在简单解码器中尽早退出,而不是检测 REX 前缀。

英特尔花费晶体管来确保像立即移位这样的常见指令可以有效解码是有意义的,而不是像pop r12你通常只会在函数结尾中找到的不太常见的指令,因此通常不会在内循环中找到。仅包含函数调用的较大循环。


脚注 1 :pop rsp很特别,因为它只是mov rsp, [rsp]. (或者如手册所说,POP ESP 指令在旧堆栈顶部的数据写入目标之前增加堆栈指针(ESP) 。Haswell 的 3-uop 实现似乎是不必要的,而实际上 1 uop 与mov rsp, [rsp](I认为故障条件是相同的),但这可能通过在正常pop reg解码方式中添加一个 uop 来节省解码器中的晶体管(可能隐含地需要一个堆栈同步 uop,总共 3 个),而不是将其作为一个整体单独处理指令? pop rsp很少使用,所以它的性能并不重要。

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

pop r12(简短形式)最终会解码为正常的 1 uop,根据@Andreas 的测试,不会比其他寄存器的重复弹出更多的堆栈同步 uop。它因无法在简单解码器中解码而受到惩罚,但不会受到任何pop rsp专门解码的额外 uops 的惩罚。


也许 GAS、NASM 和其他汇编程序应该得到一个补丁,以便可以pop r12使用 modrm 编码进行编码,尽管可能不会默认使用。解码器吞吐量通常不是问题,因此默认情况下花费额外的代码大小字节是不可取的。特别是如果对其他 uarch 没有影响,比如 AMD 或 Silvermont-family。

和/或 GCC 应该使用 R12 作为调用保留 reg 的最后选择来保存/恢复?(当R12在寻址模式下用作基址时,也总是需要一个 SIB 字节,所以这是避免它的另一个原因,如果编译器不打算尝试避免将指针保留在其中。)并且可能安排推送/弹出r12 用于高效解码,在 multi-uop 之前还有 3 个其他 pops(或其他单 uop iss)ret

  • 实际上,“push r12”不会出现这种效果。另外,`push rsp` 被解码为 1(融合)uop;它以 3 个 uop 的形式执行,其中第三个可能是堆栈同步 uop。 (2认同)