2026-07-03
Integer comparisons are cheap: subtract, look at the sign and zero flags, done. Floating-point comparisons look similar on the surface — ucomisd, fcmp, whatever your ISA calls it — but the hardware behind them is doing substantially more work, and the result is that FP compares behave in ways that break naive assumptions in your code.
The FP compare unit has to produce a four-valued result, not three. Integer compare gives you less-than, equal, or greater-than. FP compare gives you those three plus unordered — the case where at least one operand is NaN. This isn't a software convention; it's baked into the hardware. The unit examines the exponent and mantissa fields in parallel to detect NaN (exponent all-ones, mantissa non-zero) before it even bothers doing the magnitude comparison.
Two flavors of compare exist in most ISAs:
comisd on x86): raises the invalid-operation exception if either operand is NaN, even quiet NaN.ucomisd): only raises the exception on signaling NaN, silently reports unordered for quiet NaN.That distinction exists because IEEE 754 has strong opinions about NaN propagation, and hardware must honor them. The hardware sets three flags — on x86 it reuses the integer ZF, PF, and CF — with a specific encoding: PF=1 signals unordered, and the branch predictor treats jp (jump if parity) as your NaN escape hatch.
Real-world bite: if (x < y) ... else ... in C compiles to a compare plus jae (jump if above-or-equal). If x is NaN, the compare sets PF=1 and CF=1, so jae is taken — meaning NaN silently routes to the else branch. This is why x < y and !(x >= y) are not equivalent in floating point. The compiler cannot fold them without a -ffast-math promise from you.
Rule of thumb: an FP compare costs roughly 3–4 cycles latency on modern x86 (Zen 4, Golden Cove), versus 1 cycle for integer compare. Worse, the result feeds the integer flag register, which means the branch is dispatched to the integer scheduler — creating a domain crossing that costs another 1–2 cycles of forwarding penalty. A tight loop that branches on FP values can easily spend 25% of its cycles just on comparisons.
The optimization: replace branches with minsd/maxsd or cmpsd + bitmask blend. These stay in the FP domain, avoid the flag crossing, and produce vector-friendly results. This is why std::min on doubles often outruns a < b ? a : b.
