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.
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!” 😉
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.
Hi Bruce,
Thanks for the pointer, though I am not sure I will get a chance to test it out.
Is there a list of fixes? I looked at http://support.microsoft.com/kb/2967191 but nothing jumped out at me as specifically related to conversions.
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/
Thanks Bruce. It looks promising — I will check it out as soon as I can.
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).
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.
@Steve,
Thanks for reporting your findings. My notes say I tested this on VS 2008 (and Paul on VS 2010). (I still have not tried on VS 2014 CTP, as per the link above in the comments.)
If you have the time and interest, I’d be curious what VS 2013 does with the examples in https://www.exploringbinary.com/incorrectly-rounded-conversions-in-visual-c-plus-plus/ . Thanks.
@Steve,
I tested this on VS2013. I get the same results as you — no errors from the testcase, but a C++ round-trip error for numeric_limits<double>::min() (I verified that the conversion to double is correct.)
VS2013 still gets the conversions wrong for the examples in https://www.exploringbinary.com/incorrectly-rounded-conversions-in-visual-c-plus-plus/ though.
@Steve,
Visual Studio Community 2015 Release Candidate does not give an error on numeric_limits::min().
@Bruce,
I finally got a chance to test it out: https://www.exploringbinary.com/visual-c-plus-plus-strtod-still-broken/ . Thanks.