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).
#include <cstdint>
suggests you are compiling this as C++. – Histerrno = 0
before callingstrtod()
and include its value after the call. See C11 7.22.1.3p10: "" – Brimmererrno
in the output. There is much implementation defined behavior with wee values. – FragerFLT_EVAL_METHOD
andFLT_ROUNDS
, so seeing those is useful too."%24.16g"
better as"% 25.17g"
asdouble
deserves at least 17 digits. Printing out yourDBL_MIN
would help show the relationship to the strings being converted. So far, I was unable to re-create your problem outputs. – Frager"%a"
has various implementation defined aspects so using"%.13a"
may force a rounded output as 53-bit may need 14 places after the.
when a leading 0 is used. Better as%24a
to not round to 13 places and risk rounding -might explain your woes. – Fragerdouble
, C requires thatstrtod()
compute the same result at runtime (given default rounding) that the compiler produces from the lexical constant at translation time. That might be an interesting point to probe. – WrongheadedERANGE
does not mean "too large". It means "out of range". The spec makes it implementation-defined whetherstrtod
setserrno
toERANGE
when the result underflows, so it certainly contemplates that that's something that some implementations will do. Given astrtod
that always returns a normalized number as its result, it is reasonable to consider subnormal numbers not to be within its range. – WrongheadedFLT_ROUNDS
normally evaluates to a small integer. I guess that might be the result of makingprintf
misinterpret such an integer as adouble
. – Wrongheaded"-0x1.fffffffffffff8p-1023" decoded errno=0 as 0x8040000000000000 -0x1.0000000000000p-1019 -1.780059086805761106472e-30
. Missing a final8
? Consider posting all 15 output, not just the select 4. – Fragerarg = (double) strtold(value, &stopstr);
give the expected results? (Note thel
)? – Fragerstrtod()
implementations and other conversions. They often lingered for many years before being fixed. You might want to check his site to see whether you hit a known issue or try contacting him directly. – Neri