2026-07-07
IEEE 754 defines two flavors of NaN, and the hardware treats them radically differently. The distinction is encoded in a single bit — the most significant bit of the mantissa. If it's 1, you have a quiet NaN (qNaN). If it's 0 (with at least one other mantissa bit set, to distinguish from infinity), you have a signaling NaN (sNaN). That one bit changes everything about how the FPU behaves.
Quiet NaNs propagate silently. Any FP op with a qNaN input produces a qNaN output, no exception raised. This is the default NaN you get from 0.0/0.0 or sqrt(-1). The FPU just forwards it through the pipeline like a poisoned value.
Signaling NaNs raise the invalid-operation exception the moment they hit an arithmetic op. The hardware detects the sNaN pattern in the input decode/unpack stage, sets the IE flag in MXCSR (x86) or FPSR (ARM), and either delivers a trap (if unmasked) or converts the sNaN to a qNaN and continues (if masked, the default). That conversion — flipping the MSB of the mantissa — is done by a dedicated combinational path, not by microcode.
Why the hardware bothers: sNaNs are meant to catch uninitialized memory. You fill a buffer with sNaN bit patterns, and any code that reads before writing raises an exception. It's a debugging tool baked into the ISA. Fortran runtimes used this heavily; modern languages rarely do.
The performance cliff: even in masked mode, an sNaN triggers a slow path in the FPU. On Skylake, a normal FP add is 4 cycles; an sNaN input can extend to 15+ cycles because the microcode assist runs to convert sNaN→qNaN and set flags. Zen 4 is similar. If you accidentally load an sNaN pattern (say, from a corrupt memory-mapped file), every downstream FP op pays this tax.
Concrete example: the bit pattern 0x7FA00000 is an sNaN in single precision (exponent all 1s, MSB of mantissa is 0, other bits set). 0x7FC00000 is the canonical qNaN. If your uninitialized buffer happens to contain 0x7FA00000 and you do a vector add over it, you'll see a mysterious 3-4x slowdown compared to running the same code on 0x7FC00000 data — same NaN semantics, wildly different microarchitectural cost.
Rule of thumb: if your FP code suddenly runs 3-10x slower with no algorithmic change, check for sNaN or subnormal inputs before blaming the cache. Both trigger microcode assists that dominate execution time.
