Floating-Point Error Handling in C++: What Actually Works - Johnny's Software Lab<br>Sticky bits, traps, compiler flags, and the optimizer trade-offs people rarely talk about.<br>Master software performance in just 16 hours!<br>Join our Software Optimization for the Memory Subsystem Workshop taking place from May 18th to May 21st. Click here to express interest or register.In this post we are going to investigate how to efficiently handle floating-point errors in software.<br>The first part gives an introduction to different ways of detecting floating-point errors – from simply inspecting the result (checking for infinities or NaNs), over relying on the sticky exception flags in the floating-point status register, and finally to enabling hardware traps.<br>In the second part, we apply these techniques to a small example to see how they actually behave and what kind of performance overhead they introduce.<br>If you are an experienced engineer, you might want to skip the parts that are already familiar. For everyone else, I’ll try to explain everything you need to understand the context of floating-point errors.<br>Table Of Contents<br>The Beautiful TheoryWhat are floating-point errors?How are floating-point errors signaled to your program?Software Signalling – Check the ResultHardware Signalling – Sticky BitsHardware Signalling – Traps
The RealityThe BaselineManual Error HandlingVerdict<br>Sticky BitsThe Compiler Can Optimize Away Instructions Producing NaNsVerdict<br>Hardware Traps, The sigsetjmp / siglongjmp ApproachThe Order of Instruction MattersVerdict<br>Throwing C++ Exceptions from an FP Trap
Conclusion
The Beautiful Theory<br>What are floating-point errors?<br>Floating-point formats do not represent only finite numbers. They also have representations for ±infinity and NaN (not a number).<br>For example:<br>// Produce Infinity<br>float a0 = 25.0 / 0.0; // Results in +Infinity<br>float a1 = a0 + 4.0; // Infinity propagated to a1<br>float inf = std::numeric_limits::infinity();
// Produce NaN<br>float b0 = sqrt(-1); // Results in NaN<br>float b1 = b0 * 1.5; // NaN propagated to b1<br>float b2 = 0.0 * inf;In this article we will treat infinities and NaNs as “true errors”, because the result is no longer a usable finite value and will typically poison the rest of the computation.<br>Apart from these, there are also exceptions related to rounding and precision loss. The full list of IEEE-754 floating-point exceptions (from /) is:<br>FE_DIVBYZERO – Raised when a non-zero finite number is divided by zero, producing ±infinity.<br>FE_INEXACT – Raised when a result cannot be represented exactly and had to be rounded. (This exception happens very often.)<br>FE_INVALID – Raised when an operation has no mathematically defined result (e.g. 0/0, ∞−∞, sqrt(−1)).<br>FE_OVERFLOW – Raised when a finite result is too large to be represented and overflows to ±infinity.<br>FE_UNDERFLOW – Raised when a non-zero result is too small to be represented normally and is rounded to a subnormal or zero.<br>By default, these exceptions do not stop program execution. They merely set sticky flags in the floating-point status register.<br>How are floating-point errors signaled to your program?<br>In some cases you want to be notified when a floating-point error occurs. For example, you might want to know when an operation produces a NaN, overflows to infinity, or triggers some other exception. There are a few ways to achieve this. We list them here and compare them later.<br>Software Signalling – Check the Result<br>One straightforward approach is to check the result directly.<br>C++ offers std::isnan and std::isinf, which you can use to test whether the result of an operation is NaN or infinity. For example:<br>uint64_t total_nans = 0;
for (size_t i = 0; i The disadvantage of this approach is that every result is explicitly tested. A performance-conscious developer immediately sees the potential issue: additional work is executed in every iteration of the loop. Even if the check itself is cheap, it is still extra logic in the hot path.<br>Also note that this approach only detects NaNs or infinities – it does not detect exceptions such as inexact or underflow directly.<br>Hardware Signalling – Sticky Bits<br>Some errors cannot be detected by simply observing the result. For example, the result of 1.0 / 3.0 cannot be represented in finite precision – it must be rounded. In this case precision is lost, and the floating-point unit signals this by setting the FE_INEXACT flag.<br>When an exception occurs, the floating-point hardware sets a corresponding bit in the CPU’s status register1. This bit remains set until we explicitly clear it – the flag is sticky.<br>We will omit the hardware details here, but the standard way to test whether an exception occurred is to use fetestexcept and feclearexcept, for example:<br>// Clear all exception flags<br>feclearexcept(FE_ALL_EXCEPT);
// Floating-point work here
// Test whether an exception occurred<br>if (fetestexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW)) {<br>// Handle error<br>}The typical workflow is simple: clear...