Denormal Numbers in Audio DSP: A Developer's Guide
Denormal Numbers in Audio DSP: A Developer's Guide

Subnormal (denormal) floating-point values are the single most common source of surprise CPU spikes in real-time audio plugins. Per IEEE 754, subnormals are non-zero values smaller than the minimum normalized float, and most x86 and ARM microarchitectures handle them through a slow microcode path that can be orders of magnitude more expensive than normal arithmetic. The fix is usually one of two things: enable Flush-to-Zero (FTZ) and Denormals-Are-Zero (DAZ) in the MXCSR register at plugin startup, or inject a tiny alternating normalized offset (around 1e-18) into every feedback path. Both are inaudible. Neither is optional if you care about deterministic real-time behavior.
- What they are: Floating-point values below the normal exponent range, represented with a leading zero instead of the implicit leading one.
- Where they appear: IIR filter tails, reverb decays, expander/gate release stages, and any recursive structure that lets signal coast toward zero.
- Why they hurt: Arithmetic on subnormal operands triggers a slow hardware path, producing sudden CPU load spikes that can cause audio dropouts.
- Immediate fix: Enable FTZ/DAZ on x86 at startup, or add
±1e-18to the feedback accumulator each buffer.
Pro Tip: Even if your DAW host enables FTZ globally, never rely on it. A plugin that ships without its own denormal handling is one host setting away from a support ticket.
Key Takeaways
Denormal (subnormal) numbers are the most common hidden cause of real-time CPU spikes in audio plugins, and every feedback path in your DSP code is a potential source.
| Point | Details |
|---|---|
| Enable FTZ/DAZ on desktop | Set both MXCSR bits at plugin startup; use JUCE's ScopedNoDenormals in processBlock. |
| Use alternating offset as fallback | Add ±1e-18 to feedback accumulators in portable builds where hardware FTZ is unavailable. |
| Target the feedback path | Mitigate inside the accumulator, not at the output; a subnormal at the output means slow arithmetic already ran. |
| Benchmark before and after | Run a decay-generator harness with FTZ on and off; the timing difference is your denormal cost. |
| Automate in CI | Include a denormal micro-benchmark in your CI suite to catch regressions from future refactors. |
Table of Contents
- What are denormal numbers and how does IEEE 754 define them?
- How do denormal numbers arise in audio DSP?
- Why do denormals slow down your CPU so dramatically?
- How do you control denormal behavior at the platform and compiler level?
- How do you detect denormals and reproduce the slowdown in a benchmark?
- Concrete mitigation patterns you can apply today
- Numerical accuracy trade-offs and best practices for production audio
- Empirical micro-benchmark evidence and a reproducible test harness
- How Vector-dsp approaches denormal handling in production plugins
- Sources
What are denormal numbers and how does IEEE 754 define them?
Every single-precision float in IEEE 754 stores a sign bit, an 8-bit exponent, and a 23-bit mantissa. When the exponent field is non-zero, hardware assumes an implicit leading 1 before the mantissa bits, giving you 24 bits of precision for free. That is a normalized value.
The problem starts when a value is too small to fit in the normal exponent range. The smallest positive normalized float is roughly 1.175e-38. Below that, IEEE 754 does not simply round to zero. Instead, it gradually reduces precision by shifting the mantissa right and setting the exponent field to all zeros, removing the implicit leading one. These are subnormal (also called denormal) values, and they extend representable range down to approximately 1.4e-45 at the cost of progressively fewer significant bits.
W. Kahan's IEEE 754 design notes explain the motivation: gradual underflow prevents catastrophic cancellation errors near zero that a hard flush would introduce. That is the right call for general numerical computing. For real-time audio DSP, it is often the wrong trade-off.
- Normalized float: exponent field non-zero, implicit leading
1, full 24-bit precision. - Subnormal float: exponent field all zeros, no implicit leading
1, precision degrades as value shrinks. - Underflow gap: the region below ~1.175e-38 where gradual underflow applies.
- Terminology: "denormal" and "subnormal" refer to the same class of values. IEEE 754 standardized "subnormal" in the 2008 revision; older DSP docs and audio forums still use "denormal" interchangeably.
MathWorks documents the same precision-versus-range trade-off and notes that simulation environments offer flush-to-zero modes precisely because the performance cost is unacceptable in time-critical loops.
How do denormal numbers arise in audio DSP?
The short answer: any recursive structure that lets a signal decay toward zero without a hard floor. That covers a large fraction of real audio code.
IIR filters are the classic case. A biquad section processing silence still runs its feedback accumulator every sample. Once the input drops away, the internal state decays exponentially. It crosses the normalized floor and enters subnormal territory, where it can ring for thousands of samples. EarLevel Engineering's practical guide describes exactly this pattern: the filter output is inaudible, but the CPU is still working hard on subnormal arithmetic.
Reverb algorithms compound the problem. A Schroeder or FDN reverb has multiple feedback delay lines, each decaying independently. One subnormal in one delay line can propagate into every subsequent processing stage in the same block, a "poisoning" effect described in Laurent de Soras's paper on denormals in signal processing. Treat the feedback path itself as the primary mitigation target, not just the output.
Other common sources:
- Expander and noise-gate release tails: gain multipliers approaching zero drive signal into subnormal range before the gate fully closes.
- Algorithmic reverb long decays: RT60 times above 5–6 seconds mean the tail can persist in subnormal territory for hundreds of milliseconds.
- Post-gain reduction chains: a compressor with high ratio and slow release can leave residuals well below -200 dBFS.
- JUCE
processBlockcallbacks: offline rendering in a DAW often disables FTZ globally, so a plugin that works fine in real-time playback can spike during bounce.
Pro Tip: When debugging a CPU spike that only appears during silence or after a note release, check your IIR filter states first. Print the raw float values; if you see exponents in the e-40 range, you have found the source.
Why do denormals slow down your CPU so dramatically?
Normal floating-point arithmetic on x86 and ARM runs in dedicated FPU or SIMD hardware. Subnormal operands often cannot use that fast path. On many microarchitectures, the hardware detects a subnormal input and either routes the operation through a slower microcode sequence or traps to a software emulation handler. The result is a dramatic increase in cycles per operation.
Published micro-benchmark results show that subnormal operands can trigger execution paths that are orders of magnitude slower than equivalent operations on normalized values across multiple common microarchitectures. In a real-time audio context, that translates directly to CPU load spikes that the audio thread scheduler cannot absorb.
The real-time symptoms are specific: a plugin that sits at 3–5% CPU during normal playback suddenly jumps to 40–60% during a reverb tail or after a note release. The audio thread misses its deadline. The host reports a dropout. Users file bug reports about "glitches when notes stop." The root cause is almost never found without knowing to look for subnormals.
ADC 2024 speaker Attila Haraszti confirmed that even on modern processors, subnormals continue to produce real-time CPU spikes, and that enabling FTZ/DAZ remains the standard practical choice for desktop audio plugins. Modern CPUs have improved their subnormal handling, but "improved" does not mean "free."
SSE/AVX code paths are particularly susceptible because SIMD lanes process multiple samples simultaneously. One subnormal in a lane can stall the entire vector operation.
How do you control denormal behavior at the platform and compiler level?
On x86, the MXCSR register controls SSE/AVX floating-point behavior. Two bits matter here:
- FTZ (Flush-to-Zero, bit 15): results that would be subnormal are flushed to zero instead.
- DAZ (Denormals-Are-Zero, bit 6): subnormal inputs are treated as zero before the operation.
Setting both gives you the strongest protection. The bbx_audio documentation recommends flush-to-zero semantics for production audio and provides cross-platform utilities for enabling FTZ/DAZ on x86/x86_64.
A minimal C++ snippet to enable both at plugin startup:
#include <pmmintrin.h>
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
On ARM/NEON, the equivalent is the FZ bit in the FPSCR register (AArch32) or the FZ bit in FPCR (AArch64). ARM's flush-to-zero behavior is architecturally defined and generally available, but the API differs by platform and OS. iOS and Android both expose it, though you should verify behavior on each target.
- GCC/Clang:
-ffast-mathenables FTZ-equivalent behavior but also permits unsafe reassociations. Prefer-fno-math-errno -fno-trapping-mathcombined with explicit MXCSR intrinsics for surgical control. - MSVC:
/fp:fastenables similar semantics. Again, explicit intrinsics give you finer control. - WebAssembly: no hardware FTZ/DAZ control is available. Fallback to software denormal flushing or algorithm redesign is required.
- JUCE projects: JUCE provides
ScopedNoDenormalsas a RAII wrapper that sets FTZ/DAZ on entry and restores the previous state on exit. Place it at the top ofprocessBlock.
Pro Tip: Never set FTZ/DAZ globally in a shared library without restoring the previous MXCSR state. A plugin that permanently alters the host's FPU flags can break other plugins in the same process. Use RAII wrappers or save/restore the register explicitly.
Laurent de Soras's paper emphasizes that there is no single universal fix. FTZ/DAZ is the right choice for most desktop audio plugins, but portable builds targeting WebAssembly or embedded targets need a different strategy.
How do you detect denormals and reproduce the slowdown in a benchmark?
Detection starts with inspecting float values directly. A subnormal single-precision float has an exponent field of all zeros and a non-zero mantissa. In C++:
bool isDenormal(float x) {
return ((*reinterpret_cast<uint32_t*>(&x)) & 0x7F800000) == 0 && x != 0.0f;
}
For production code, a branchless flush is more useful than detection alone. But during development, logging the raw hex of filter state variables after a note-off event will immediately reveal whether subnormals are present.
Micro-benchmark pattern: isolate a single IIR biquad, feed it a short burst of signal, then process silence for 10,000 samples. Measure cycles per sample during the decay tail with and without FTZ/DAZ enabled. The difference is your denormal cost.
| Benchmark type | What to measure | Expected signal |
|---|---|---|
| IIR biquad decay loop | Cycles/sample during tail | Large spike without FTZ |
| FDN reverb silence processing | Wall-clock time per buffer | 5–20x slower without FTZ |
| Chained biquad filter bank | CPU% average | Gradual climb, then plateau |
| Expander release tail | Thread scheduling jitter | Missed deadlines at low buffer sizes |
Common false positives: background OS scheduler activity, cache misses on first buffer, and thermal throttling. Run benchmarks with CPU affinity pinned to a single core, disable turbo boost for consistency, and average over at least 1,000 buffer iterations before drawing conclusions.
Concrete mitigation patterns you can apply today
The choice of mitigation depends on your architecture and portability requirements. Laurent de Soras's paper lays out the trade-off space clearly: there is no single approach that works everywhere, and treating denormal prevention as a design requirement rather than an afterthought is the right frame.
FTZ/DAZ (preferred for desktop x86/x86_64):
// JUCE RAII wrapper — place at top of processBlock
juce::ScopedNoDenormals noDenormals;
This is the lowest-overhead solution when available. The cost is near zero; the protection is complete for SSE/AVX paths.
Tiny alternating offset (portable fallback):
Add a small normalized constant to the feedback accumulator. EarLevel Engineering recommends a value around 1e-18 (approximately -360 dBFS), which is well below the threshold of human hearing. The alternating-sign trick prevents DC blockers from removing it:
static float dcOffset = 1e-18f;
dcOffset = -dcOffset;
filterState += dcOffset;
Flip the sign every buffer, not every sample, to keep the overhead minimal.
Conditional flush (branchless):
float flushDenormal(float x) {
return ((*reinterpret_cast<uint32_t*>(&x)) & 0x7F800000) ? x : 0.0f;
}
Useful in portable builds where hardware FTZ is unavailable, but carries a per-sample branch cost.
Algorithm redesign:
-
Replace pure IIR feedback with a leaky integrator: multiply the state by
(1 - epsilon)each sample, where epsilon is small enough to be inaudible but large enough to prevent subnormal accumulation. -
Add a fade-to-zero threshold: if the absolute value of a filter state drops below 1e-15, zero it explicitly.
-
Prefer oversampled processing for reverb tails where the extra headroom keeps values in the normalized range longer.
-
Where to place mitigations: inside the feedback accumulator, not at the output. A subnormal at the output is already too late; the slow arithmetic has already happened.
-
JUCE
processBlockplacement: setScopedNoDenormalsbefore any filter processing, not just before the output write. -
Double precision: switching to
doubleshifts the subnormal floor to ~2.2e-308, which is far below any audio signal, but does not eliminate the problem on all architectures.
Pro Tip: The alternating-sign offset is your best portable fallback, but document it clearly in the code. Future developers who see filterState += 1e-18f without a comment will remove it as "dead code" during cleanup.

Numerical accuracy trade-offs and best practices for production audio
Flushing subnormals to zero is audibly transparent in almost every real-world audio scenario. Values below roughly -300 dBFS are inaudible by any practical measure. The precision loss from flushing is real in a mathematical sense but irrelevant to the listener.
That said, a few cases warrant care:
- Scientific audio analysis tools where numerical accuracy below -200 dBFS matters (spectrum analyzers, precision metering). In these contexts, avoid FTZ/DAZ and use algorithm redesign instead.
- Embedded and safety-critical targets where IEEE 754 compliance is a contractual requirement.
- WebAssembly builds where FTZ/DAZ is unavailable and the alternating-offset approach is the only portable option.
Production checklist:
- Instrument filter state variables during development: log min/max values over a 30-second silence period.
- Enable FTZ/DAZ in all desktop builds (Windows, macOS) by default; document the choice in the build config.
- Apply the alternating-sign offset in all portable/WebAssembly builds.
- Prefer leaky integrators over pure IIR feedback in long-decay reverb and modulation paths.
- Add a unit test that feeds silence into every IIR-based processor for 10,000 samples and asserts no subnormal values in the state variables.
- Include a denormal micro-benchmark in your CI performance suite and set a regression threshold.
- Keep all mitigation code in clearly labeled utility functions; never inline it silently into DSP logic.
Pro Tip: Cross-platform policy matters. A plugin that ships with FTZ/DAZ on desktop but no fallback on mobile will behave differently on iOS. Test on every target architecture before release, not just the development machine.
Plugin CPU optimization practices at the architecture level reinforce this: denormal handling belongs in the same category as buffer size selection and thread priority, not as an afterthought in the final QA pass.
Empirical micro-benchmark evidence and a reproducible test harness
The academic micro-benchmark paper demonstrates that subnormal operands produce measurably slower execution across multiple common microarchitectures. The slowdown is not uniform: some chips show a 2–5x penalty, others show far larger gaps depending on the operation type and pipeline depth. The consistent finding is that the slow path is real and measurable, not theoretical.
A practical test harness for plugin developers:
- Decay generator: initialize a biquad with a short burst of white noise at -20 dBFS, then switch input to silence.
- Timing loop: process 10,000 samples in blocks of 512, recording wall-clock time per block using
std::chrono::high_resolution_clock. - Two runs: first with FTZ/DAZ disabled, then with FTZ/DAZ enabled. Keep all other conditions identical.
- Metric: cycles per sample during the decay tail (samples 512–10,000).
| Condition | Expected behavior | Mitigation applied |
|---|---|---|
| FTZ/DAZ off, decaying IIR | Slow path active in tail | None |
| FTZ/DAZ on, decaying IIR | Fast path throughout | FTZ/DAZ |
| Alternating offset, no FTZ | Fast path throughout | ±1e-18 offset |
| Leaky integrator, no FTZ | Fast path throughout | Algorithm redesign |
Run at 48 kHz, 512-sample buffer, with CPU affinity pinned and turbo boost disabled. Average over 100 iterations. The difference between the first and any of the mitigated rows should be clearly visible in wall-clock time.
ADC 2024 confirmed that these patterns remain relevant on current hardware. The numbers shift between microarchitecture generations, but the qualitative result holds: subnormals cost more than normalized values, and the cost is large enough to cause real-time failures.
Pro Tip: Automate this harness in CI. A future refactor that accidentally removes a ScopedNoDenormals call will show up immediately as a regression in the timing numbers, before it ships.

How Vector-dsp approaches denormal handling in production plugins
At Vector-dsp, denormal handling is not a late-stage optimization. It is a design requirement that goes into every IIR-based processing block from the first prototype.
The default engineering policy: FTZ/DAZ enabled via RAII wrapper in all desktop builds (Windows and macOS, VST3/AU/AAX), with the alternating-sign offset applied in every feedback accumulator as a belt-and-suspenders fallback. Portable builds targeting environments without hardware FTZ fall back to the offset approach exclusively, with the policy documented in the build configuration.
Every release includes a CI micro-benchmark that runs the decay-generator harness described above. A regression in the timing numbers blocks the build. That is the only reliable way to catch a denormal bug introduced by a refactor, because the symptom (a CPU spike during silence) is easy to miss in manual testing.
The broader principle: a plugin that behaves differently at the end of a reverb tail than at the start is not a stable product. Real-time determinism is part of the contract with the host and the user.
Sources
These are the primary references for deeper study on subnormal numbers in audio DSP:
- A note about de-normalization | EarLevel Engineering
- Denormal numbers in floating point signal processing applications
- Subnormal number slowdowns micro-benchmark (academic paper)
- Denormal Handling - bbx_audio Documentation
- Subnormal number performance - MATLAB & Simulink
- IEEE 754 history/status - W. Kahan