为什么使用寄存器 R12 时 POP 很慢?
Why is POP slow when using register R12?
在最近的 Intel CPU 上,POP
指令通常每个周期有 2 条指令的吞吐量。但是,当使用寄存器 R12
(或 RSP
,除了前缀之外具有相同的编码)时,如果指令通过传统解码器(吞吐量保持在大约如果微操作来自 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个周期。添加第 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 之间增加到 2 nops
。这表明 pop R12
是一条需要复杂解码器的指令,即使它只有一个微操作(另请参见 )
为什么 POP
指令在使用寄存器 R12
时解码不同?还有其他说明也是这样吗?
解决方法:pop r12
的 pop 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
如果 哪个解码器可以解码哪个指令的主要过滤是通过操作码字节(不考虑前缀),至少对于 ,pop r12
也会受到惩罚]this 组操作码。这是否真的反映了确切的内部结构,它至少是一个有用的心智模型来理解为什么 pop modrm 没有这种效果。 (尽管通常您只会将 pop r/m64
与内存目标一起使用,这意味着多 uop,因此仅是复杂的解码器。)
push rsp
在 Haswell 上总共是 2 微指令,不像大多数 push reg
指令是 1 微指令。但可能额外的 uop 只是在 issue/rename 期间插入的堆栈同步(因为读取 RSP), 而不是 在解码期间插入。 @Andreas 报告说 push rsp
和 push r12
在解码器中都没有显示特殊效果(我假设是 uop 缓存)。仅 1 个微融合 uop,with/without 执行时堆栈同步 uop。
像 FF /0 inc r/m32
这样的操作码,其中相同的前导字节在不同的指令之间共享(将 modrm /r
字段重载为额外的操作码字节)可能很有趣,如果有一些单-与多 uop 指令共享前导字节的 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
这样你通常只能在函数尾声中找到的不太常见的指令,因此通常不在内部循环中。只有包含函数调用的较大循环。
脚注 1:pop rsp
很特别,因为它只是 mov rsp, [rsp]
。 (或者如手册所述,POP ESP 指令在将旧堆栈顶部的数据写入目标之前递增堆栈指针 (ESP)。Haswell 的 3-uop 实现似乎没有必要vs. 字面上与 mov rsp, [rsp]
相同的 1 uop(我认为故障条件是相同的),但这可能通过在 pop reg
解码的正常方式中添加一个 uop 来节省解码器中的晶体管(可能隐含地要求总共 3) 个堆栈同步 uop,而不是将其视为一个完整的单独指令?pop rsp
很少使用,因此它的性能无关紧要。
也许 16 位 pop sp
情况是将该字节解码为 1 个纯负载 uop 的问题? x86 机器代码中没有 [sp]
寻址模式,并且 可能 限制扩展到 16 位 AGU 的内部 uops。除此之外,我认为 pop
和 mov
.
可能的故障原因是相同的
pop r12
(缩写形式)最终确实解码为正常的 1 微指令,根据@Andreas 的测试。它会因为在简单解码器中不可解码而受到惩罚,但不会受到 pop rsp
专门解码到的任何额外微指令的惩罚。
也许 GAS、NASM 和其他汇编程序应该得到一个补丁,以便可以使用 modrm 编码对 pop r12
进行编码,尽管可能不会默认为那个。解码器吞吐量通常不是问题,因此默认情况下花费额外的代码大小字节是不可取的。特别是如果对其他 uarches 没有影响,比如 AMD 或 Silvermont 系列。
And/or GCC 应该使用 R12 作为其对 save/restore 的调用保留 reg 的最后选择? ( 也用作寻址模式中的基址,所以这是避免它的另一个原因,如果编译器不打算避免在其中保留指针的话。)也许安排 push/pop 的 r12 用于高效解码,在多 uop 之前有 3 个其他 pops(或其他单 uop isns)ret
。
在最近的 Intel CPU 上,POP
指令通常每个周期有 2 条指令的吞吐量。但是,当使用寄存器 R12
(或 RSP
,除了前缀之外具有相同的编码)时,如果指令通过传统解码器(吞吐量保持在大约如果微操作来自 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个周期。添加第 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 之间增加到 2 nops
。这表明 pop R12
是一条需要复杂解码器的指令,即使它只有一个微操作(另请参见
为什么 POP
指令在使用寄存器 R12
时解码不同?还有其他说明也是这样吗?
解决方法:pop r12
的 pop 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
如果 哪个解码器可以解码哪个指令的主要过滤是通过操作码字节(不考虑前缀),至少对于 ,pop r12
也会受到惩罚]this 组操作码。这是否真的反映了确切的内部结构,它至少是一个有用的心智模型来理解为什么 pop modrm 没有这种效果。 (尽管通常您只会将 pop r/m64
与内存目标一起使用,这意味着多 uop,因此仅是复杂的解码器。)
push rsp
在 Haswell 上总共是 2 微指令,不像大多数 push reg
指令是 1 微指令。但可能额外的 uop 只是在 issue/rename 期间插入的堆栈同步(因为读取 RSP), 而不是 在解码期间插入。 @Andreas 报告说 push rsp
和 push r12
在解码器中都没有显示特殊效果(我假设是 uop 缓存)。仅 1 个微融合 uop,with/without 执行时堆栈同步 uop。
像 FF /0 inc r/m32
这样的操作码,其中相同的前导字节在不同的指令之间共享(将 modrm /r
字段重载为额外的操作码字节)可能很有趣,如果有一些单-与多 uop 指令共享前导字节的 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
这样你通常只能在函数尾声中找到的不太常见的指令,因此通常不在内部循环中。只有包含函数调用的较大循环。
脚注 1:pop rsp
很特别,因为它只是 mov rsp, [rsp]
。 (或者如手册所述,POP ESP 指令在将旧堆栈顶部的数据写入目标之前递增堆栈指针 (ESP)。Haswell 的 3-uop 实现似乎没有必要vs. 字面上与 mov rsp, [rsp]
相同的 1 uop(我认为故障条件是相同的),但这可能通过在 pop reg
解码的正常方式中添加一个 uop 来节省解码器中的晶体管(可能隐含地要求总共 3) 个堆栈同步 uop,而不是将其视为一个完整的单独指令?pop rsp
很少使用,因此它的性能无关紧要。
也许 16 位 pop sp
情况是将该字节解码为 1 个纯负载 uop 的问题? x86 机器代码中没有 [sp]
寻址模式,并且 可能 限制扩展到 16 位 AGU 的内部 uops。除此之外,我认为 pop
和 mov
.
pop r12
(缩写形式)最终确实解码为正常的 1 微指令,根据@Andreas 的测试。它会因为在简单解码器中不可解码而受到惩罚,但不会受到 pop rsp
专门解码到的任何额外微指令的惩罚。
也许 GAS、NASM 和其他汇编程序应该得到一个补丁,以便可以使用 modrm 编码对 pop r12
进行编码,尽管可能不会默认为那个。解码器吞吐量通常不是问题,因此默认情况下花费额外的代码大小字节是不可取的。特别是如果对其他 uarches 没有影响,比如 AMD 或 Silvermont 系列。
And/or GCC 应该使用 R12 作为其对 save/restore 的调用保留 reg 的最后选择? (ret
。