I have been looking at some code which had a weird optimisation fault and, in the process, stumbled upon a fault condition in strtod()
, which has a different behaviour to strtof()
, right on the edge of denormal values. The behaviour of strtof()
seems entirely reasonable to me, but strtod()
does not! Specifically, it returns -0.0
for the input value "-0x1.fffffffffffffp-1023"
.
That is one extra bit set to one in the representation "-0x1.ffffffffffffep-1023"
which it decodes correctly. More peculiar still adding extra trailing digits gets a value 2-1018 which I cannot explain. It looks to me like the special edge case on the transition from denormals to normal floats has been handled incorrectly leading to a zero value.
Can anyone explain the other strange number that additional extra digits provoke?
The fault is identical on both MSC 2022 and Intel 2023
Sample code MRE for doubles & output (float works as you would expect)
// strtod() fails to handle edge case overflow from denormals correctly
//
// Problem manifests on both MS 2022 and Intel 2023 compilers so by design? but why???
//
// using Windows 11 and Microsoft Visual Studio Community 2022 (64-bit) - Version 17.1.0
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void Show(const char *name, int err, double arg)
{
unsigned iarg[2];
memcpy(&iarg, &arg, sizeof arg);
printf("\n\"%25s\" decoded errno=%i as 0x%08x%08x %.13a %30.22g",
name, err, iarg[1], iarg[0], arg, arg);
}
void DecodeShow(const char *value)
{
char *stopstr;
double arg;
errno = 0;
arg = strtod(value, &stopstr);
Show(value, errno, arg);
}
int main(void)
{
printf("Test of doubles near denorm boundary\n");
DecodeShow("-0x1.ffffffffffffp-1023");
DecodeShow("-0x1.ffffffffffffep-1023");
DecodeShow("-0x1.ffffffffffffe8p-1023");
DecodeShow("-0x1.fffffffffffff0p-1023"); // hard fail == -0.0 !
DecodeShow("-0x1.fffffffffffff8p-1023");
DecodeShow("-0x1.ffffffffffffffp-1023");
DecodeShow("-0x1.fffffffffffffffp-1023");
printf("\n");
DecodeShow("0x1.ffffffffffffe8p-1023");
DecodeShow("0x1.fffffffffffff0p-1023"); // hard fail == +0.0
DecodeShow("0x1.fffffffffffff8p-1023");
DecodeShow("0x1.ffffffffffffffp-1023");
printf("\n");
DecodeShow("0x1.ffffffffffffep-1022");
DecodeShow("-0x1.fffffffffffffp-1022");
DecodeShow("-0x1.fffffffffffff8p-1022");
DecodeShow("-0x1.ffffffffffffffp-1022");
}
Selected output around failure boundary:
" -0x1.ffffffffffffep-1023" decoded errno=0 as 0x800fffffffffffff -0x0.fffffffffffffp-1022 -2.225073858507200889025e-308
"-0x1.ffffffffffffe8p-1023" decoded errno=0 as 0x800fffffffffffff -0x0.fffffffffffffp-1022 -2.225073858507200889025e-308
"-0x1.fffffffffffff0p-1023" decoded errno=0 as 0x8000000000000000 -0x0.0000000000000p+0 -0
"-0x1.fffffffffffff8p-1023" decoded errno=0 as 0x8040000000000000 -0x1.0000000000000p-1019 -1.780059086805761106472e-30
It is my view that extra trailing digits going beyond machine precision should not radically alter the floating point value that strtod
decodes like this. I could not provoke the same failure by using decimal digit input strings - the transition across the boundary seemed well-behaved (although I can't rule out one spot value not working that I haven't yet found).
This is the same issue reported at https://developercommunity.visualstudio.com/t/strtod-parses-0x0fffffffffffff8p-1022-i/10293606 . A bug was filed for it in Feb 2023 (it appears it was fixed in the compiler previously, but not strtod()).
I ran the five strtod() examples in the report and these four give incorrect conversions (includes your 0 example). Each should result in 0x1.0000000000000p-1022 but
0x0.fffffffffffff8p-1022 => 0x1.0000000000000p-1020
0x1.fffffffffffffp-1023 => 0x0.0000000000000p+0
0x7.ffffffffffffcp-1025 => 0x1.0000000000000p-1021
0xf.ffffffffffff8p-1026 => 0x1.0000000000000p-1020
Note the incorrect exponents. There was no speculation in the report about what went wrong.