How NOP Nearly Became a Non-NOP on AMD64 (2007)
pagetable.com
pagetable.com
It's trivial to write microbenchmarks where spilling hot read/write variables to the stack destroys performance, much harder to find special cases where the difference isn't noticeable.
- Workloads that are LSU-bound, like simple per-pixel image operations and level 1 and level 2 BLAS[1]. Here every spill is taking up two LSU dispatch slots that would otherwise be used for "real work".
- Spilled loop-carried dependencies in tight loops; here you're simply adding latency to the critical path.
Both are fairly rare in "generic" code, but very real concerns for compute kernels.
[1] The BLAS operations have few enough buffers that spill/fill never actually happens in practice, but more complex operations on multiple buffers do run into this.
At the time, they evaluated how many registers they needed and ran simulations for 16, 32 and 64. Assuming a modern implementation with register renaming and good OoO, they found only a few % gain from 32 and practically none with 64. They decided to go with 16 to save encoding bits. If they found that x86 needed more registers, they would have added more registers, not made partial accesses more common. Remember that spilling to stack in x86 does not have a latency hit in normal conditions because all modern x86 cpus have a separate stack engine that allows the loads to retire immediately after they enter the instruction window, typically tens of cycles before any instructions immediately following them.
" An assembler would translate the mnemonic “nop” into “xchg ax, ax” (opcode 0×90)"
But 0x90 also means nop, so it's not really translating anything. And now it still means nop in AMD64.
As… the article even explained further down, sometimes. That's where the whole discussion comes from.
• In 32 bit mode, it doesn't matter. Some (dumb) CPUs treated it as xchg ax,ax; pipelined CPUs optimized it away.
• In 64 bit mode, it matters: Is "xchg eax, eax" a valid way to clear the upper 32 bits of eax? Or will it always be optimized away as legacy NOP?
AMD decided for the latter. They could also have introduced a new opcode for it instead (there are already multiple nop instructions, like nopl/nopw, so it wouldn't have been too far off) – as this only affects 64 bit mode, backwards compatibility didn't really matter, both would have been possible.
XCHG EAX,target is defined as opcode (0x90 + offset of target register), with EAX having offset 0.
So, it was xchg eax,eax originally, and documented as such, before it was turned into NOP because it happened to be safe for it.
It's still documented as "alias for the XCHG (E)AX, (E)AX instruction" in Intel's instruction set reference, and pre-486 embedded x86s still treat it as xchg.
Edit: What do you mean "still treat it as xchg"? On those chips, isn't there no distinction between xchg and what we might retrospectively call "nop"? Perhaps this is something I'm missing.
XCHG EAX, EAX in its dumbest interpretation loads EAX into a temporary register, replaces it with the contents of EAX and restores the temporary data to… EAX. So, it is an operation that does nothing, but it does nothing in an elaborate way. You can skip it instead of executing it, but only if your other code doesn't depend on 0x90 taking exactly three clock cycles.
The "treat 0x90 as NOP and skip it instead of wasting three cycles" optimization was only done with the 486 and up, and wasn't retroactively applied to the 386 embedded versions. Doing so would have messed up their timings, and would have needed a small design change, both not interesting to that customer base.
(The 386 and derivatives were still produced for embedded use for a long, long time, past 2001 – and thus, after the introduction of AMD64. When its instruction set was drafted, new 386-based embedded devices were still designed.)