The Floating-Point Subnormal Flush-to-Zero Mode: Why Your CPU Has a Switch That Silently Changes Your Math

2026-07-06

IEEE 754 defines subnormal numbers (also called denormals) as the tiny values below the smallest normal float — roughly below 1.18e-38 for float32. They preserve gradual underflow: as values shrink past the normal range, precision degrades slowly instead of snapping to zero. The problem is that handling them in hardware is a nightmare, so every modern CPU ships with a switch that just... turns them off.

The hardware pain: normal floats have an implicit leading 1 bit in the mantissa, so the multiplier and adder can assume a fixed operand width. Subnormals break that assumption — the leading bit is 0, and the true value is encoded by how many leading zeros the mantissa has. To operate on a subnormal, the FPU has to normalize it on the fly: count leading zeros, shift the mantissa, and adjust the exponent below its usual range. Most FPUs don't have this path in the fast pipeline. Instead, subnormal inputs or results trigger a microcode assist — a slow trap that can take 100–250 cycles versus the usual 4–5 cycles for an FP op.

The switches: x86 exposes two bits in MXCSR — FTZ (Flush-To-Zero) forces subnormal results to zero, and DAZ (Denormals-Are-Zero) treats subnormal inputs as zero. ARM's FPSCR has FZ. When both are set, the FPU never sees a subnormal, so no microcode assist ever fires. The math is no longer IEEE 754 compliant, but it runs at full speed.

Real-world example: audio processing with reverb tails. As a reverb decays exponentially, its signal drops through the subnormal range and stays there for milliseconds. A JUCE plugin author reported CPU usage jumping from 2% to 40% when a reverb tail entered subnormal territory — every sample was triggering a microcode assist. Fix: set FTZ+DAZ at the top of the audio callback. Same result perceptually (silence is silence), 20x faster. Every serious DSP library sets these bits on entry.

Rule of thumb: a subnormal-triggered microcode assist costs ~100 cycles. If your inner loop touches subnormals even 1% of the time, you're paying an amortized cost of ~1 cycle per iteration — often more than the entire loop's normal work. Anything below ~1e-30 in float32 is suspect.

The gotcha: FTZ/DAZ are per-thread, set in MXCSR. If a library sets them and forgets to restore, downstream numerical code (root solvers, iterative refinement) can silently lose precision near zero. Compilers with -ffast-math set FTZ/DAZ globally at startup — one reason "fast-math" occasionally produces mysteriously wrong results in unrelated code.

Key Takeaway: Subnormals are IEEE 754's gift to numerical precision and hardware's worst nightmare — FTZ/DAZ lets you trade a tiny bit of accuracy near zero for a 20-100x speedup when your data drifts into the denormal range.

All newsletters