【问题标题】:Why is POP slow when using register R12?为什么使用寄存器 R12 时 POP 很慢?
【发布时间】:2021-02-23 18:04:51
【问题描述】:

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

这可以使用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 个周期。添加第 5 个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?

为什么在使用寄存器R12 时,POP 指令的解码方式不同?是否还有其他说明也是如此?

【问题讨论】:

  • r12rsp 具有相同的编码,除了REX 前缀中的位,也许这会“吓到”解码器?
  • @harold:这是有道理的; pop reg 的缩写形式将寄存器编码为“操作码”字节的一部分,并且可能是哪个解码器的主要过滤可以通过操作码字节解码哪个指令(不考虑前缀)。我想知道我们是否会看到与pop r/m64 的 2 字节 ModRM 编码相同的效果,它通过 ModRM 字节对 RSP 或 R12 进行编码。 pop rsp 很特别,因为它只是 mov rsp, [old_rsp](或者正如手册所说,在应用增量之后编写 R/ESP,但仍从旧的栈顶加载。felixcloutier.com/x86/pop )。
  • @PeterCordes 使用pop R12的2字节ModRM编码,效果不会出现。
  • 也许值得为 GAS 和 NASM 提交补丁,以便使用此指令优化速度而不是大小。或者让 GCC 尽可能避免使用 R12,以避免在结尾处弹出 r12。或者在 ret 之前将其排成 4 个 insn,这样您就可以在多指令 ret 到达解码器之前获得完整的解码组。
  • @PeterCordes 在 Haswell 上,每 ~25 个 pop R12 就有一个堆栈同步微指令;没有额外的堆栈同步微指令。对于pop RSP,没有堆栈同步微指令;但是,pop RSP 解码为 3 uop,而 pop R12 仅解码为 1 uop(对于两种编码)。

标签: performance x86 intel cpu-architecture micro-optimization


【解决方案1】:

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

db  0x41, 0x8f, 0xc4        ; REX.B=1  8F /0  pop r/m64  = pop r12

pop r12 的标准编码与pop rsp 具有相同的操作码字节,仅相差一个 REX。 (short form encoding 将寄存器号放在该 1 字节的低 3 位)。

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

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

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

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


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

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

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


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

和/或 GCC 应该使用 R12 作为其最后选择的调用保留 reg 来保存/恢复? (R12 always needs a SIB byte 在寻址模式中也用作基数,所以这是避免它的另一个原因,如果编译器不打算避免在其中保留指针。)并且可能安排 r12 的推送/弹出高效解码,在多 uop ret 之前有 3 个其他 pop(或其他单 uop)。

【讨论】:

  • 其实push r12是没有效果的。此外,push rsp 被解码为 1(融合)uop;它以 3 个微指令执行,其中第三个可能是堆栈同步微指令。
猜你喜欢
  • 2012-09-12
  • 2021-11-01
  • 2014-11-21
  • 2016-06-01
  • 2014-10-31
  • 1970-01-01
  • 1970-01-01
  • 2020-09-14
  • 2017-05-25
相关资源
最近更新 更多