2026-07-05
Modern CPUs keep floating-point and integer values in separate physical register files — but sometimes you need to move a value between them. Extracting an FP bit pattern for hashing, checking a float's sign bit with integer ops, or reinterpreting an integer as a float via memcpy — all of these cross the FP/integer boundary. That crossing is not free.
The problem is domain crossing latency. FP and integer execution units live in physically separate clusters on the die, often with their own bypass networks. A value produced in the FP domain has to traverse extra wiring, cross a clock-domain-like boundary, and land in an integer register file port. On Intel's Skylake through Alder Lake, a MOVD or MOVQ from XMM to GPR costs 2-3 cycles, versus 1 cycle for a plain integer move. On AMD Zen, it's similar — 2-4 cycles depending on direction.
The direction matters too. Integer-to-FP is often cheaper than FP-to-integer, because FP execution tolerates a bit more latency in operand delivery (FP ops are already 3-5 cycles), while integer ALUs are latency-sensitive at 1 cycle and a slow operand delivery blows up your dependency chain.
Concrete example: A common trick in hash functions is to reinterpret a double as a uint64_t to hash its bits:
uint64_t bits; memcpy(&bits, &d, 8); — compiles to a MOVQ xmm→gpr.PMULLD, PXOR) directly on the FP-domain register. No domain crossing.Rule of thumb: Every FP↔GPR move costs ~2 cycles of extra latency on top of the nominal 1-cycle move. If you do it once per iteration in a loop with a 4-cycle body, expect a ~50% throughput hit. If you can restructure to stay in one domain — even by using SIMD integer instructions on FP-domain registers — do it.
Compilers know about this but can't always avoid it: type-punning through memcpy, calling conventions that return floats in XMM but need them in RAX for further integer work, and hand-written intrinsics that mix domains all force the crossing.
