3
int main(){

  unsigned int a = 2139156962;
  float b = *(float*)&a;       // nan(0xf1e2)
  double c = (double)b;        // nan canonicalization? 
  float d = (float)c;          // nan(0x40f1e2), d != b
  unsigned int e = *(int*)&d;  // e != a

  return 0;
}

NaN values can be represented in many different ways. As the example above shows, converting a NaN value of nan(0xf1e2) to a double type is not preserving the input bit patterns, and casting it back to a float doesn't return the same value as the original input.

From this link I can see on x64, CVTSS2SD seems to canonicalize Qnan inputs?

the sign bit is preserved, the 8-bit exponent FFH is replaced by the 11-bit exponent 7FFH, and the 24-bit significand is extended to a 53-bit significand by appending 29 bits equal to 0.

So regardless of what the Qnan bit pattern of our input is, the output will use 0x7FF and won't preserve all the original bits? Is this some sort of NaN canonicalization?

If so then this answer may not be fully accurate?

floats can be promoted to double and the value is unchanged.

Our output is still a NaN but the underlying data is now changed and c != b.

Dan
  • 2,694
  • 1
  • 6
  • 19
  • 4
    You are kind of invoking a bunch of UBs here... – Eugene Sh. Apr 06 '22 at 21:54
  • Ignore the the bit castings here, yes they are technically UB but they can be ignored for this question, they shouldn't cause an issue. – Dan Apr 06 '22 at 21:54
  • 2
    This starts out with an SNaN (signalling NaN) pattern. Operating upon an SNaN will signal an exception when floating-point exceptions are unmasked. When exceptions are masked, the SNaN is converted to a corresponding QNaN (quiet NaN) by forcing the most significant bit of the stored significand to 1. For IEEE-754 `binary32` on x86 hardware, this is equivalent to OR-ing `0x00400000` into the bit pattern. Here: `a=7f80f1e2 b=7f80f1e2 c=7ff81e3c40000000 d=7fc0f1e2 e=7fc0f1e2` – njuffa Apr 06 '22 at 22:01
  • 1
    Note that NaN "payloads" are an *optional* feature of the IEEE-754 standard, i.e. a "should" provision, not a "shall" provision. – njuffa Apr 06 '22 at 22:03
  • @njuffa so no canonicalization is going on, CVTSS2SD is just changing an SNAN to QNAN which causes difference in 1 bit between `a` and `e` ? – Dan Apr 06 '22 at 22:07
  • @Dan Yes, a conversion is a floating-point operation, so `CVTSS2SD` is operating upon an SNaN here, and the masked response to that is to convert it to the corresponding QNAN pattern. Last I checked, the differentiation of SNaN from QNaN bit patterns is machine specific (not prescribed by IEEE-754), and this particular handling is specific to x86 platforms and different values may occur on other platforms. – njuffa Apr 06 '22 at 22:11
  • I think it bit #22 in FPs , 0 means SNAN, 1 means QNAN. So then the post saying `floats can be promoted to double and the value is unchanged.` is true? – Dan Apr 06 '22 at 22:12
  • I would argue that NaN encodings do not not have a value, since NaN = Not a Number. Re "I think it bit #22 in FPs , 0 means SNAN, 1 means QNAN": For single precision on x86, but this is not true of all platforms. – njuffa Apr 06 '22 at 22:21
  • RE "this is not true of all platforms." do you have a link on this? I thought this is part of the standard. – Dan Apr 06 '22 at 22:25
  • The fact that the value is unchanged when casting from `float` to `double` doesn't mean that the bit pattern is exactly the same... In particular because `float` and `double` don't have the same size - so you MUST have bits filled within the `double` to keep its consistency. And this is false when casting from `double` to `float`! Values are the same _once decoded_, and _only_ when promoting to a "superior" type. – Wisblade Apr 06 '22 at 22:35
  • @Wisblade you only add `0s` to keep the size relevant. Maybe I had to use a better wording like "the final values are not he same", also it could be the same when down casting if the value fits, so not *only when promoting*. – Dan Apr 06 '22 at 22:44
  • @Dan That's UB to think that... It's guaranteed when **promoting**, and **only** when promoting. When you downcast to an "inferior" type... Look for a comprehensive God, in particular when you can have multiple codings for a special case like NaN. Think about booleans in C: only `false` (0) is exact. For `true`, it's anything NON-zero, that's why you can have strange things like `TrueResult1!=TrueResult2` - never ever compare "true" together when you don't have a real, normed boolean definition. You're facing exactly the same thing with your NaN. – Wisblade Apr 06 '22 at 23:04
  • @Wisblade is this a rule (UB) for double to float or any type of downcast? i.e long to int, short to char, etc. – Dan Apr 07 '22 at 00:43
  • 1
    @Dan A useful overview is provided in Andrew Shell Waterman, "Design of the RISC-V Instruction Set Architecture", [Ph.D. dissertation](https://escholarship.org/content/qt7zj0b3m7/qt7zj0b3m7.pdf]), UC Berkeley 2016. In particular: " *Table 4.4: Default single-precision NaN for several ISAs. QNaN polarity refers to whether the most significant bit of the significand indicates that the NaN is quiet when set, or quiet when clear.* " You are correct that the QNaN/SNaN differentiation bit was clarified in the 2008 edition of the IEEE-754 standard (which did not change existing hardware, however). – njuffa Apr 07 '22 at 00:47
  • 1
    @Dan Think about it: when you downcast 0x1456abd83, for example, to a char... What happens? Binary truncation? Overflow? Exception? Something else? You can't know, it can differ from platform to platform. That's why it's UB: the norm doesn't guarantee a known result on all possible platforms. Look a [C99 standard](http://www.open-std.org/JTC1/sc22/wg14/www/docs/n1256.pdf), §6.3. For floats, it says _"If the value being converted is outside the range of values that can be represented, the behavior is undefined."_ And NaN is such a value. See also §7.12.3.4 about NaN. – Wisblade Apr 07 '22 at 01:02
  • 1
    @Dan Also, most people don't understand what UB means... UB isn't "that will produce random things". It means "the behavior can't be known in advance for a given platform". In other words, nothing is guaranteed... But once the platform is **FIXED** (i.e. "I'm using VS2019, x64, on Windows 10 build 19044.1586, with C99 norm"), then the result CAN be reliable. On this platform. And only this one. It can change in x86 mode, or on Windows 11, and obviously with MinGW. You can't know beforehand. That's why UB is "dangerous": the "known behavior" can be broken _simply with a compiler/OS update_. – Wisblade Apr 07 '22 at 01:04
  • @Wisblade https://stackoverflow.com/a/67755393 it seems down casting integer types is implementation defined and not undefined, please confirm, seems like the UB issue is only related to FP types: https://en.cppreference.com/w/c/language/conversion, third bullet under `Real floating point conversions` – Dan Apr 10 '22 at 23:00
  • @Dan And that's exactly what "UB" means: implementation defined... As I said, most people don't understand that UB is NOT a random action, different on each execution/compilation, but something that MAY VARY across platform, compiler versions, OS, language standards, etc. But on YOUR platform, unless you change something, the result is PREDICTABLE. What you can't predict is what it will do on another platform. Recently, I converted a signed char to hexadecimal... My program wrote "0x-1", instead of "0xFF"... That's UB, the "Hex" func was for UNsigneds, and templates messed up casts. So "0x-1". – Wisblade Apr 11 '22 at 00:30
  • Right, but these are technically two different concepts: https://stackoverflow.com/a/2397995, applying it to this topic, integer down casting is *implementation defined*, integer overflow due to computation, as well as FP conversion which doesn't fit is *undefined*. – Dan Apr 11 '22 at 00:33

0 Answers0