[In this reprinted #altdevblogaday in-depth piece, Valve Software programmer Bruce Dawson explains why it can be useful to halt when a floating-point exception is signaled.] Floating-point math has an answer for everything, but sometimes that's not what you want. Sometimes instead of getting an answer to the question sqrt(-1.0) (it's NaN), it's better to know that your software is asking imaginary questions. The IEEE standard for floating-point math defines five exceptions that shall be signaled when certain conditions are detected. Normally the flags for these exceptions are raised (set), a default result is delivered, and execution continues. This default behavior is often desirable, especially in a shipping game, but during development it can be useful to halt when an exception is signaled. Halting on exceptions can be like adding an assert to every floating-point operation in your program, and can therefore be a great way to improve code reliability, and find mysterious behavior at its root cause. This article is part of a series on floating-point. The complete list of articles in the series is:
1: Tricks With the Floating-Point Format – an overview of the float format
2: Stupid Float Tricks – incrementing the integer representation of floats
3: Don't Store That in a Float – a cautionary tale about time
3b: They sure look equal… – special bonus post (not on altdevblogaday)
4: Comparing Floating Point Numbers, 2012 Edition – tricky but important
5: Float Precision—From Zero to 100+ Digits – what does precision mean, really?
5b: C++ 11 std::async for Fast Float Format Finding – special bonus post (not on altdevblogaday)
6: Intermediate Precision – their effect on performance and results
7.0000001: Floating-Point Complexities – a lightning tour of all that is weird about floating point
8: Exception Floating point – (return *this;)
Let's get it started again The five exceptions mandated by the IEEE floating-point standard are:
Invalid operation: this is signaled if there is no usefully definable result, such as zero divided by zero, infinity minus infinity, or sqrt(-1). The default result is a NaN (Not a Number)
Division by zero: this is signaled when dividing a non-zero number by zero. The result is a correctly signed infinity.
Overflow: this is signaled when the rounded result won't fit. The default result is a correctly signed infinity.
Underflow: this is signaled when the result is non-zero and between -FLT_MIN and FLT_MIN. The default result is the rounded result.
Inexact: this is signaled any time the result of an operation is not exact. The default result is the rounded result.
The underflow exception is usually not of interest to game developers – it happens rarely, and usually doesn't detect anything of interest. The inexact result is also usually not of interest to game developers – it happens frequently (although not always, and it can be useful to understand what operations are exact) and usually doesn't detect anything of interest. That leaves invalid operation, division by zero, and overflow. In the context of game development, these are usually truly exceptional. They are rarely done intentionally, so they usually indicate a bug. In many cases, these bugs are benign, but occasionally these bugs indicate real problems. From now on, I'll refer to these first three exceptions as being the 'bad' exceptions and assume that game developers would like to avoid them, if only so that the exceptions can be enabled without causing crashes during normal game play. When can divide by zero be useful? While the 'bad' exceptions typically represent invalid operations in the context of games, this is not necessarily true in all contexts. The default result (infinity) of division by zero can allow a calculation to continue and produce a valid result, and the default result (NaN) of invalid operation can sometimes allow a fast algorithm to be used and, if a NaN result is produced, a slower and more robust algorithm to be used instead. The classic example of the value of the division by zero behavior is calculation of parallel resistance. The formula for this for two resistors with resistance R1 and R2 is:
Because division by zero gives a result of infinity, and because infinity plus another number gives infinity, and because a finite number divided by infinity gives zero, this calculation calculates the correct parallel resistance of zero when either R1 or R2 is zero. Without this behavior the code would need to check for both R1 and R2 being zero and handle that case specially. In addition, this calculation will give a result of zero if R1 or R2 are very small – smaller than the reciprocal of FLT_MAX or DBL_MAX. This zero result is not technically correct. If a programmer needs to distinguish between these scenarios then monitoring of the overflow and division by zero flags will be needed. Resistance is futile Assuming that we are not trying to make use of the divide-by-zero behavior we need a convenient way of turning on the 'bad' floating-point exceptions. And, since we have to coexist with other code (calling out to physics libraries, D3D, and other code that may not be 'exception clean') we also need a way of temporarily turning off all floating-point exceptions. The appropriate way to do this is with a pair of classes whose constructors and destructors do the necessary magic. Here are some classes that do that, for VC++:
// Declare an object of this type in a scope in order to suppress
// all floating-point exceptions temporarily. The old exception
// state will be reset at the end.
class FPExceptionDisabler
{
public:
FPExceptionDisabler()
{
// Retrieve the current state of the exception flags. This
// must be done before changing them. _MCW_EM is a bit
// mask representing all available exception masks.
_controlfp_s(&mOldValues, _MCW_EM, _MCW_EM);
// Set all of the exception flags, which suppresses FP
// exceptions on the x87 and SSE units.
_controlfp_s(0, _MCW_EM, _MCW_EM);
}
~FPExceptionDisabler()
{
// Clear any pending FP exceptions. This must be done
// prior to enabling FP exceptions since otherwise there
// may be a 'deferred crash' as soon the exceptions are
// enabled.
_clearfp();
// Reset (possibly enabling) the exception status.
_controlfp_s(0, mOldValues, _MCW_EM);
}
private:
unsigned int mOldValues;
// Make the copy constructor and assignment operator private
// and unimplemented to prohibit copying.
FPExceptionDisabler(const FPExceptionDisabler&);
FPExceptionDisabler& operator=(const FPExceptionDisabler&);
};
// Declare an object of this type in a scope in order to enable a
// specified set of floating-point exceptions temporarily. The old
// exception state will be reset at the end.
// This class can be nested.
class FPExceptionEnabler
{
public:
// Overflow, divide-by-zero, and invalid-operation are the FP
// e
No tags.