Search code examples
cfloating-pointdouble

strtod edge of denormals glitch


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).


Solution

  • 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.