The Partial Register Stall: Why Writing AL Then Reading RAX Costs You a Merge Uop

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:

Every 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.

Key Takeaway: Writing a sub-register (AL/AX/AH) forces the CPU to merge with the untouched upper bits — use movzx to a 32-bit register instead, since 32-bit writes zero-extend for free in the renamer.

All newsletters