Usefulness of signaling NaN?
Asked Answered
R

3

59

I've recently read up quite a bit on IEEE 754 and the x87 architecture. I was thinking of using NaN as a "missing value" in some numeric calculation code I'm working on, and I was hoping that using signaling NaN would allow me to catch a floating point exception in the cases where I don't want to proceed with "missing values." Conversely, I would use quiet NaN to allow the "missing value" to propagate through a computation. However, signaling NaNs don't work as I thought they would based on the (very limited) documentation that exists on them.

Here is a summary of what I know (all of this using x87 and VC++):

  • _EM_INVALID (the IEEE "invalid" exception) controls the behavior of the x87 when encountering NaNs
  • If _EM_INVALID is masked (the exception is disabled), no exception is generated and operations can return quiet NaN. An operation involving signaling NaN will not cause an exception to be thrown, but will be converted to quiet NaN.
  • If _EM_INVALID is unmasked (exception enabled), an invalid operation (e.g., sqrt(-1)) causes an invalid exception to be thrown.
  • The x87 never generates signaling NaN.
  • If _EM_INVALID is unmasked, any use of a signaling NaN (even initializing a variable with it) causes an invalid exception to be thrown.

The Standard Library provides a way to access the NaN values:

std::numeric_limits<double>::signaling_NaN();

and

std::numeric_limits<double>::quiet_NaN();

The problem is that I see no use whatsoever for the signaling NaN. If _EM_INVALID is masked it behaves exactly the same as quiet NaN. Since no NaN is comparable to any other NaN, there is no logical difference.

If _EM_INVALID is not masked (exception is enabled), then one cannot even initialize a variable with a signaling NaN: double dVal = std::numeric_limits<double>::signaling_NaN(); because this throws an exception (the signaling NaN value is loaded into an x87 register to store it to the memory address).

You may think the following as I did:

  1. Mask _EM_INVALID.
  2. Initialize the variable with signaling NaN.
  3. Unmask_EM_INVALID.

However, step 2 causes the signaling NaN to be converted to a quiet NaN, so subsequent uses of it will not cause exceptions to be thrown! So WTF?!

Is there any utility or purpose whatsoever to a signaling NaN? I understand one of the original intents was to initialize memory with it so that use of an unitialized floating point value could be caught.

Can someone tell me if I am missing something here?


EDIT:

To further illustrate what I had hoped to do, here is an example:

Consider performing mathematical operations on a vector of data (doubles). For some operations, I want to allow the vector to contain a "missing value" (pretend this corresponds to a spreadsheet column, for example, in which some of the cells do not have a value, but their existence is significant). For some operations, I do not want to allow the vector to contain a "missing value." Perhaps I want to take a different course of action if a "missing value" is present in the set -- perhaps performing a different operation (thus this is not an invalid state to be in).

This original code would look something like this:

const double MISSING_VALUE = 1.3579246e123;
using std::vector;

vector<double> missingAllowed(1000000, MISSING_VALUE);
vector<double> missingNotAllowed(1000000, MISSING_VALUE);

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it); // sqrt() could be any operation
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it);
    else *it = 0;
}

Note that the check for the "missing value" must be performed every loop iteration. While I understand in most cases, the sqrt function (or any other mathematical operation) will likely overshadow this check, there are cases where the operation is minimal (perhaps just an addition) and the check is costly. Not to mention the fact that the "missing value" takes a legal input value out of play and could cause bugs if a calculation legitimately arrives at that value (unlikely though it may be). Also to be technically correct, the user input data should be checked against that value and an appropriate course of action should be taken. I find this solution inelegant and less-than-optimal performance-wise. This is performance-critical code, and we definitely do not have the luxury of parallel data structures or data element objects of some sort.

The NaN version would look like this:

using std::vector;

vector<double> missingAllowed(1000000, std::numeric_limits<double>::quiet_NaN());
vector<double> missingNotAllowed(1000000, std::numeric_limits<double>::signaling_NaN());

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    *it = sqrt(*it); // if *it == QNaN then sqrt(*it) == QNaN
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    try {
        *it = sqrt(*it);
    } catch (FPInvalidException&) { // assuming _seh_translator set up
        *it = 0;
    }
}

Now the explicit check is eliminated and performance should be improved. I think this would all work if I could initialize the vector without touching the FPU registers...

Furthermore, I would imagine any self-respecting sqrt implementation checks for NaN and returns NaN immediately.

Rothstein answered 11/2, 2010 at 20:19 Comment(1)
Good question. Unfortunately the only use I have ever seen for signaling NaNs is to generate a call to my cell phone at 9:30 PM on Saturday.Trihedron
T
13

As I understand it, the purpose of signaling NaN is to initialize data structures, but, of course runtime initialization in C runs the risk of having the NaN loaded into a float register as part of initialization, thereby triggering the signal because the the compiler isn't aware that this float value needs to be copied using an integer register.

I would hope that you could could initialize a static value with a signaling NaN, but even that would require some special handling by the compiler to avoid having it converted to a quiet NaN. You could perhaps use a bit of casting magic to avoid having it treated as a float value during initialization.

If you were writing in ASM, this would not be an issue. but in C and especially in C++, I think you will have to subvert the type system in order to initialize a variable with NaN. I suggest using memcpy.

Thieve answered 11/2, 2010 at 21:51 Comment(1)
Yes, I think that may be a reasonable assumption. I think it would need to be part of the language and not just part of a library for it to work the way it's supposed to.Thieve
C
3

Using special values (even NULL) can make your data a lot muddier and your code a lot messier. It would be impossible to distinguish between a QNaN result and a QNaN "special" value.

You might be better maintaining a parallel data structure to track validity, or perhaps having your FP data in a different (sparse) data structure to only keep valid data.

This is fairly general advice; special values are very useful in certain cases (e.g. really tight memory or performance constraints), but as the context grows larger they can cause more difficulty than they're worth.

Cardiograph answered 13/2, 2010 at 6:5 Comment(3)
It is a good question though. I only offer this answer as humble advice to less experienced travellers who think "that would be a neat trick!" :-)Cardiograph
Gotcha. Obviously it's difficult to comment on code I haven't seen, and I know from bitter experience how it is to work on big ugly codebases that you're told not to change, but given the chance I'd try to isolate the logic to check for the value in a function (even a macro if you check it so frequently a non-inlined function call would slow down debug builds). If nothing else it makes it all a lot clearer, and at best it gives you a chance to reorganise it if you find a better solution.Cardiograph
Ok, that makes a lot more sense now. In your example, using QNaN and avoiding a test will be much more SIMD and cache friendly. As I said in my original answer, tight memory/performance constraints are an exception to the general rule of not using special values :-) Thanks STingRaySC. I'm sorry I don't have an answer for your problem.Cardiograph
S
3

Here are the bit-patterns of the different double NaNs:

A signalling NaN is represented by any bit pattern between 7FF0000000000001 and 7FF7FFFFFFFFFFFF or between FFF0000000000001 and FFF7FFFFFFFFFFFF

A quiet NaN is represented by any bit pattern between 7FF8000000000000 and 7FFFFFFFFFFFFFFF or between FFF8000000000000 and FFFFFFFFFFFFFFFF

Source: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html

Disclaimer: As others have pointed out, casting magic is potentially dangerous and may cause undefined behavior. Using memcpy has been suggested as a safer alternative.

That being said, for academical purposes, or if you know it is safe on the intended hardware:

In theory, it seems like it should work to just have a const uint64_t where the bits have been set to those of a signaling nan. As long as you treat it as an integer type, the signaling nan is not different from other integers. Then, barring architectural special case issues, maybe you could write it where you want through pointer-casting. If it works as intended, it might even be faster than memcpy. For some embedded systems it might even be useful.

Example:

const uint64_t sNan = 0xFFF7FFFFFFFFFFFF;
double[] myData;
...
uint64_t* copier = (uint64_t*) &myData[index];
*copier = sNan & ~myErrorFlags;
Saraband answered 17/8, 2015 at 18:55 Comment(4)
Be careful, reinterpret casting this way results in Undefined Behavior, cause there was never an uint64_t at the pointed place. The correct way would be to memcpy the byte representation to the double location.Berchtesgaden
Could you please fix this code? It is so wrong. 1) It will not compile because of the uppercase letters. 2) If it did it would cause Undefined behavior. 3) It is misleading because what you call sNan is actually the pattern of negative infinity (you can check your own link)Predation
Thank you, @Predation and Kaznov. I agree with your criticism. You both got my upvote, and I have updated my long forgotten answer accordingly.Saraband
Thanks for updating your answer. I removed my downvote.Predation

© 2022 - 2024 — McMap. All rights reserved.