2026-08-27
x86's register hierarchy is a fossil record. RAX contains EAX contains AX contains AH and AL. When you write to one of these sub-registers, the CPU has to figure out what happens to the untouched bits — and that decision costs cycles.
The renamer's job is simple: every write produces a new physical register mapping. But a partial write doesn't produce a full 64-bit value — it produces bits [7:0] while bits [63:8] still live in the previous physical register. When a later instruction reads the full register, the CPU has two choices:
Modern Intel (Skylake+) and AMD Zen use the merge-uop approach for AH/AL/AX writes, but still specifically avoid it for 32-bit writes: any write to a 32-bit register zeroes the upper 32 bits. That's why compilers emit xor eax, eax instead of xor rax, rax — same effect, one byte shorter, and it breaks the false dependency.
Concrete example. Consider this innocent-looking loop reading a byte stream:
mov al, [rsi] — writes low 8 bits of RAXcmp rax, rdx — reads full RAXEvery iteration inserts a merge uop into the pipeline. On a 4-wide decode CPU, that's a 25% front-end bandwidth tax you didn't ask for. The fix: movzx eax, byte ptr [rsi] — one instruction, zeroes the upper bits explicitly, no merge needed. Agner Fog measured this at ~2x speedup on tight decode loops.
The AH/BH/CH/DH trap is worse. Writing AH (bits [15:8]) creates a merge dependency that can't be broken by any idiom — there's no "zero-extend AH" instruction. Skylake in particular treats AH writes as producing a separate physical register that must be merged before any full-register read, adding 1 cycle latency on the dependency chain. This is why modern compilers essentially never use AH/BH/CH/DH.
Rule of thumb: If you're touching sub-registers in a hot loop, prefer movzx/movsx to a 32-bit destination. The zero-extension is free (renamer handles it), and you avoid both false dependencies on the old value and merge uops in the pipeline.
The partial register stall is a great example of how backwards compatibility has a silicon cost. RISC-V doesn't have this problem — every write produces a full-width value. x86 pays for its history one merge uop at a time.
movzx to a 32-bit register instead, since 32-bit writes zero-extend for free in the renamer.
