我正在阅读Intel 说明手册,发现有一条“NOP”指令在主 CPU 上什么都不做,而一个“FNOP”指令在 FPU 上什么也不做。为什么有两个单独的指令什么都不做?
我看到的唯一不同的是它们抛出了不同的异常,因此您可能会观察 FNOP 的异常以检测是否有可用的 FPU。但是没有像 CPUID 这样的其他机制来检测这个吗?有两个单独的 NOP 指令有什么实际原因?
我正在阅读Intel 说明手册,发现有一条“NOP”指令在主 CPU 上什么都不做,而一个“FNOP”指令在 FPU 上什么也不做。为什么有两个单独的指令什么都不做?
我看到的唯一不同的是它们抛出了不同的异常,因此您可能会观察 FNOP 的异常以检测是否有可用的 FPU。但是没有像 CPUID 这样的其他机制来检测这个吗?有两个单独的 NOP 指令有什么实际原因?
扩展 Raymond Chen 和 Hans Passant 的评论,有两个单独的指令以及为什么它们不完全具有相同效果的历史原因。
NOP
和两个指令都不FNOP
是最初设计为明确的无操作指令。该NOP
指令实际上只是指令的别名XCHG AX,AX
。(或在 32 位模式下XCHG EAX, EAX
。)在早期的 Intel 处理器上,它实际上并没有做任何事情。虽然它没有外部可见的影响,但在内部它就像一条XCHG
指令一样执行,需要尽可能多的周期来执行。'486 是第一个对其进行特殊处理的 Intel CPU,它可以NOP
在 1 个周期内执行一个周期,而执行任何其他寄存器到寄存器XCHG
指令需要 3 个周期。
XCHG AX,AX
在现代英特尔处理器中,特殊处理指令变得非常重要。如果它实际上仍在与自己交换相同的寄存器,那么如果附近的指令也使用该AX
寄存器,它可能会引入管道停顿。通过对它进行特殊处理,CPU 最终不会认为NOP
需要等待前一条指令设置AX
或后续指令需要等待NOP
.
这带来了一个事实,即有许多不同的指令什么都不做,但XCHG AX,AX
只有一个是单字节的(作为交换寄存器与累加器单字节XCHG
编码的特殊情况)。这些指令通常用作连续NOP
指令的单个指令替代,例如出于性能原因对齐循环开始时。例如,如果你想要一个 6 字节的 NOP,你可以使用LEA EAX,[EAX + 00000000]
. 英特尔最终添加了一个显式的多字节 NOP 指令。(好吧,与其说是官方记录的那样添加了自 Pentium Pro 以来一直存在的指令。)但是只有单字节形式被特殊处理;如果附近的指令使用相同的寄存器,多字节 NOP 将产生停顿。
当 AMD 为其 CPU 添加 64 位支持时,他们走得更远。NOP
不再等同XCHG EAX,EAX
于 64 位模式。英特尔指令集的问题之一是有很多指令只修改部分寄存器。例如MOV BX,AX
只修改低 16 位,不修改EBX
高 16 位。这些部分修改使 CPU 很难避免停顿,因此 AMD 决定在 64 位模式下使用 32 位指令时防止这种情况。每当 32 位操作的结果存储在(64 位)寄存器中时,该值将零扩展到 64 位,以便修改整个寄存器。这意味着XCHG EAX,EAX
不再是 NOP,因为它清除了EAX
(因此如果你明确写XCHG EAX,EAX
,它不能组装到 0x90 并且必须使用87 C0
编码)。在 64 位模式下NOP
,现在是一个没有其他解释的显式 NOP。
至于FNOP
指令,在最初的 8087 上,FPU 是如何处理这条指令的并不完全清楚,但我很确定它也没有作为明确的无操作处理。至少有一本旧的英特尔手册,ASM86 语言参考手册确实记录了做一些没有效果的事情(“将堆栈顶部存储到堆栈顶部”)。从它在操作码映射中的位置来看,它可能是FST ST
或的别名FLD ST
,两者都会将堆栈顶部复制到堆栈顶部。然而,它确实得到了一些特殊处理,它平均执行 13 个周期,而不是平均 18 或 20 个周期,以便堆栈FST
或堆栈FLD
分别指示。如果它被视为无操作指令,我希望它会更快,因为有许多 8087 指令可以在一半的时间内执行。
更重要的是,该FNOP
指令的行为与过去NOP
在英特尔处理器上实现 FPU 指令的方式不同。CPU 本身不支持浮点运算,而是将这些任务卸载到可选的浮点协处理器上,最初是 8087。协处理器的优点之一是它与 CPU 并行执行指令。然而,这意味着 CPU 有时需要等待 FPU 完成操作。CPU 在给它下一条指令之前会自动等待它完成前一条指令的执行,但是程序需要显式等待(使用WAIT
指令)才能读取协处理器写入内存的结果。
因为协处理器并行工作,这也意味着如果 FPU 指令产生浮点异常,当它检测到这一点时,CPU 已经开始执行下一条指令。通常,当一条指令在 CPU 上产生异常时,会在该指令仍在执行时对其进行处理,但当 FPU 指令产生异常时,CPU 已经通过将其移交给 FPU 完成了该指令的执行。不是中断 CPU 并异步传递浮点异常,而是仅在显式或隐式等待协处理器时通知 CPU。
在现代处理器中,FPU 不再是协处理器,而是 CPU 的一个组成部分。这意味着程序不再需要等待 FPU 将值写入内存。但是,处理 FPU 异常的方式没有改变。(事实证明,立即交付异常很难在现代 CPU 上实现,因此他们利用了他们不必这样做的一种情况。)因此,如果先前的 FPU 指令生成了未交付的浮点异常,NOP
则保留该异常undelivered, while FNOP
,因为它是一个 FPU 指令,将执行一个隐式的“等待”,导致浮点异常被传递。
此示例演示了差异:
FLD1 ; push 1.0 onto the FPU stack
FLDZ ; push 0.0
FDIV ; divide 1.0 by 0.0
NOP ; does nothing
NOP ; does nothing
FNOP ; signals a FP zero-divide exception and then does nothing