x86 NOP和FNOP指令有什么区别?

Mic*_*rge 8 x86 assembly x87

我正在阅读英特尔指令手册并注意到有一条'NOP'指令在主CPU上没有任何作用,而且一条'FNOP'指令在FPU上什么都不做.为什么有两个单独的指令什么都不做?

我看到的唯一不同之处是它们会抛出不同的异常,因此您可能会从FNOP中查看异常,以检测是否有可用的FPU.但是没有像CPUID这样的其他机制来检测这个吗?有什么实际的理由有两个单独的NOP指令?

Ros*_*dge 19

扩展Raymond Chen和Hans Passant的评论,有两个单独的指示以及为什么它们没有完全相同的效果的历史原因.

无论是两个指令,NOP并且FNOP,最初被设计为一个明确的无操作指令.该NOP指令实际上只是指令的别名XCHG AX,AX.(或者在32位模式下XCHG EAX, EAX.)在早期的英特尔处理器上,它实际上并没有做任何事情.虽然它没有外部可见效果,但在内部执行就像一条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仅修改EBX保留高16位未修改的低16 位.这些部分修改使CPU很难避免停顿,因此AMD决定在64位模式下使用32位指令时防止这种情况发生.只要32位操作的结果存储在(64位)寄存器中,该值就会零扩展到64位,以便修改整个寄存器.这意味着XCHG EAX,EAX不再是NOP,因为它清除了高32位EAX(因此,如果你明确地写XCHG EAX,EAX,它不能组装到0x90并且必须使用87 C0编码).在64位模式下NOP,现在是一个明确的NOP,没有其他解释.


至于FNOP指令,原始的8087上并不完全清楚FPU如何处理这条指令,但我很确定它也没有作为一个明确的无操作处理.至少有一本旧的英特尔手册,ASM86语言版本反馈手册确实记录了没有效果的事情("将堆栈顶部存储到堆栈顶部").从它的操作码地图中的位置,它看起来像它可能对于任何一个别名FST STFLD ST,两者都将在堆栈的顶部复制到堆栈的顶部.然而,它确实得到了一些特殊的处理,它平均执行了13个周期,而不是平均18或20个周期,分别用于堆栈FSTFLD指令.如果它被视为无操作指令,我希望它更快,因为有一些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则抛出异常未送达,而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
Run Code Online (Sandbox Code Playgroud)