Incorrect Round-Trip Conversions in Visual C++

Paul Bristow, a Boost.Math library author and reader of my blog, recently alerted me to a problem he discovered many years ago in Visual C++: some double-precision floating-point values fail to round-trip through a stringstream as a 17-digit decimal string. Interestingly, the 17-digit strings that C++ generates are not the problem; they are correctly rounded. The problem is that the conversion of those strings to floating-point is sometimes incorrect, off by one binary ULP.

I’ve previously discovered that Visual Studio makes incorrect decimal to floating-point conversions, and that Microsoft is OK with it — at least based on their response to my now deleted bug report. But incorrect decimal to floating-point conversions in this context seems like a problem that needs fixing. When you serialize a double to a 17-digit decimal string, shouldn’t you get the same double back later? Apparently Microsoft doesn’t think so, because Paul’s bug report has also been deleted.

Test Program

Paul sent me the test program he used to discover the errors. He found that the errors occur within a narrow range, roughly between 0.0001 to 0.004. One of his testcases starts with a previously discovered floating-point number that round-trips incorrectly, and then iterates through subsequent floating-point numbers to find more that incorrectly round-trip. I based my test program on that testcase.

This test program I wrote tests for round-trip errors through stringstream (what I call “C++”) and sprintf()/strtod() (what I call “C”). I added the C test because in the past, I’d never found bad round-trips in C. I wanted to see if I could find them while specifically testing in Paul’s range. As it turns out, the program finds no C errors, but finds many C++ errors (for as long as I run it).

#include <stdio.h>
#include <sstream>
#include <iomanip>
#include <limits>

int main (void)
{ 
 unsigned long long i;
 double d_orig, d_back;
 char d_orig_str_C[25];
 const char* d_orig_str_CPP;

 d_orig = 0.00012208391547875944; //Known bad starting point
 for (i=1; i <= 10000000000; i++)
   {
    /* Test round-trips in C */
    sprintf_s(d_orig_str_C,sizeof(d_orig_str_C),"%.17g",d_orig);
    d_back = strtod(d_orig_str_C,NULL);
    if (d_orig != d_back)
       printf("*** C: %.13a returns as %.13a\n",d_orig,d_back);

    /* Test round-trips in C++ */
    std::stringstream stream;
    stream << std::setprecision(17) << d_orig;//To stream as string
    stream >> d_back; //From stream as double
    if (d_orig != d_back)
       printf("*** C++: %.13a returns as %.13a\n",d_orig,d_back);

    /* Verify the C and C++ output decimal strings match */
    const std::string s = stream.str();
    d_orig_str_CPP = s.c_str();
    if (strcmp(d_orig_str_C,d_orig_str_CPP) != 0)
       printf("*** String mismatch\n");

    d_orig = _nextafter(d_orig,1.0);
   }  
}

I have some code in there to test that the decimal strings generated by sprintf() and stringstream are the same, so as to eliminate that as the source of the errors (the strings never differ).

Example

Here’s one example error (the first error the program finds):

*** C++: 0x1.00074d944424fp-13 returns as 0x1.00074d944424ep-13

0x1.00074d944424fp-13 converts to the string 0.00012208391547875944, which in binary is

0.00000000000010000000000000111010011011001010001000100001001001110111111100101…

Correctly rounded to 53 bits it’s

0.00000000000010000000000000111010011011001010001000100001001001111

which is 0x1.00074d944424fp-13. The bad round-trip value, 0x1.00074d944424ep-13, is one ULP below the correct value.

Update 6/24/14: A Round-Trip Problem in C#

A recent thread on Stack Overflow talks about failed round-trip conversions in C#: Why is a round-trip conversion via a string not safe for a double?. What’s interesting is that the problem there seems to be in the generation of the string, not in the conversion of the generated string to a double.

Dingbat
RSS feed icon
RSS e-mail icon

11 comments

  1. A further clue is that extensive testing shows that about one in 3 of the values in the range 0.0001 up to about 0.0004 are wrong. A third is suspiciously like log 10 – 0.301 – which smells strongly of an out-by-one mistake in this range.

    If you using any program like Boost.Serialization or lexical cast, and rely on round-tripping, this may cause you grief.

    Microsoft say, like the Doctor, “If it hurts when you do that, don’t do that!” 😉

  2. Have you looked at the Visual Studio Dev 14 CTP? They believe that they have fixed all of the bugs that you have found. I think it would be great if you ran performance and conformance tests and commented on the CTP now, while there is still time to fix any issues that you might discover.

    I’m *really* looking forward to being able to print float/double values to hundreds of digits of precision without having to write my own code to do it.

  3. I just saw your reply — sorry for the lack of a Dev 14 CTP link in my previous e-mail. I hadn’t realized how well hidden the information is. If you look for “Floating Point Formatting and Parsing Correctness” in this post:

    http://blogs.msdn.com/b/vcblog/archive/2014/06/18/crt-features-fixes-and-breaking-changes-in-visual-studio-14-ctp1.aspx

    you’ll see that they talk about printing more than 17 digits, and scanning more than 17 input digits. You can set up a Dev 14 VM on Azure — I set one up and used the new printing to run tests related to this post:

    http://randomascii.wordpress.com/2014/10/09/intel-underestimates-error-bounds-by-1-3-quintillion/

  4. I just tried this out in VS2010 and VS2013. In 2010 I see errors like crazy, but in VS2013 I do not (the process has performed 6,000,000 iterations of the main loop without reporting a single failure).

  5. Well, it still didn’t work for numeric_limits::min(). It was serialized as 2.2250738585072014e-308 and came back as 2.225073858507e-308#DEN.

Leave a Reply

Your email address will not be published. Required fields are marked *

(Cookies must be enabled to leave a comment...it reduces spam.)