They sure look equal…

This is a special bonus extra post in my floating-point series, ranting about an issue that has been a problem for years.

Some debuggers don’t display floats with enough precision.

We count on our programming tools to be precise, and accurate, so when they present us with misleading information for no good reason we should be annoyed. Here is an example:

image

The debugger is plainly showing me that f1 and f2 have the same value, but it is also showing me that they are not equal. This seems confusing.

Even though comparing floating-point numbers for equality is a a dark endeavor full of traps we should still expect that two floats (and they are both floats, see the ‘Type’ column) that contain the same value should be equal.

Huh.

An expert programmer knows that there are a few things that one can do in order to resolve this ambiguity. One can cast f1 and f2 to double in order to convince the debugger to print them using more digits, or one can display the underlying integer representation. With that done we see that f1 and f2 are indeed different numbers. Their floating-point values are slightly different, and their integer representations differ by one.

image

It turns out that eight decimal digits of mantissa is not enough to reliably distinguish floats. You need nine.

But why?

That may seem surprising. A float mantissa has 24 bits (if we include the implied one) which gives us 16,777,216 different mantissa values, which is representable in eight digits. However it turns out that if the first digit of the decimal mantissa is small enough, and if the stars align properly, then one extra decimal digit may be required.

Numbers from 1,000 to just below 1,024 are perfect for this. It takes ten binary digits to represent the integer portion (four decimal digits) which leaves fourteen binary digits for the fractional portion. Fourteen binary digits is 16,384 different values. 16,384 different values clearly require five digits to uniquely identify, which means that nine decimal digits (4 + 5) are required to uniquely identify all float numbers in this range, and in fact nine decimal digits (for the mantissa) are sufficient to uniquely identify all float numbers.

On the other hand, numbers from 1,024 to just below 10,000 only require eight decimal digits to uniquely identify. They use eleven or more binary digits for the integer portion, which leaves thirteen or fewer binary digits for the fraction.

9 digits does what?

It’s important to be precise here: nine digits is not enough to fully display the precise value of all float numbers – that actually requires over a hundred digits in some cases (more on that later). However nine digits of mantissa is enough to unambiguously identify which of the ~4 billion floats we are talking about.

Nine digits aren’t needed all the time. Most adjacent floats can be distinguished using just eight digits. Using Float_t and our ability to iterate through all floats it is easy enough to find exactly how many pairs of floats look identical when printed with an eight digit mantissa. Here’s some code:

union Float_t
{
    Float_t(float f1 = 0.0f) : f(f1) {}

    int32_t i;
    float f;
    struct
    {
        uint32_t mantissa : 23;
        uint32_t exponent : 8;
        uint32_t sign : 1;
    } parts;
};

void Count9PrintFloats()
{
    int matchCount = 0;
    uint32_t lastExponent = 0;
    Float_t allFloats(0.0f);

    char buffer[30];
    sprintf_s(buffer, “%1.7e”, allFloats.f);
    while (allFloats.parts.exponent < 255)
    {
        allFloats.i += 1;
        char buf2[_countof(buffer)];
        sprintf_s(buf2, “%1.7e”, allFloats.f);
        if (strcmp(buffer, buf2) == 0)
            matchCount += 1;
        strcpy_s(buffer, buf2);

        if (allFloats.parts.exponent != lastExponent)
        {
            printf(“%d matches found up to exponent %u.\n”,
                    matchCount, lastExponent);
            lastExponent = allFloats.parts.exponent;
        }
    }

    printf(“%d matches found.\n”, matchCount);
}

This program took a bit less than half an hour to iterate through all 2 billion positive floats and it found 32,226,412 pairs of floats that require nine digit decimal mantissas to distinguish. In other words, roughly 6% of all floats need to be printed with a nine-digit mantissa.

Visual Studio (versions 2005, 2010, and VS 11 Developer preview all behave identically) display floats with eight digits of precision, and this is insufficient. They need to display them with nine. They are sooo close. I am hopeful that this can be corrected before VS 11 ships. Finally.

In your own development if you want your floating point numbers to round-trip from float to decimal and back I recommend printing them like this:

printf_s("%1.8e", 8);

If printed in this manner you should get nine digits of mantissa and when you sscanf them back into memory you should get back the exact float that you started with.

Other debuggers

WinDbg gets this right. It displays floats with 10 digits of precision.

image

Doubles

Both VS and WinDb display doubles with 17 digits of precision. There are two reasons for this:

  1. 17 decimal digits of mantissa is how many is required to uniquely identify a double.
  2. 17 decimal digits of mantissa is the most that VC++ will print.

If you ask VC++ to print more than 17 digits (printf(“%1.30e”, d1);) then the extra digits will all be zeroes, even though that is usually not the correct value. This is standards compliant, but annoying. We’ll deal with this problem, and explore how many digits it takes to perfectly represent the value of a float, in a subsequent post.

About these ads

About brucedawson

I'm a programmer, working for Valve (http://www.valvesoftware.com/), focusing on optimization and reliability. Nothing's more fun than making code run 5x faster. Unless it's eliminating large numbers of bugs. I also unicycle. And play (ice) hockey. And juggle.
This entry was posted in Floating Point, Math, Programming and tagged , , , , . Bookmark the permalink.

15 Responses to They sure look equal…

  1. Samik R says:

    Bruce, great series on floating points and their nuances, enjoying them.
    Quick question: You mention about the integer representation of floating point in the post (they differ by 1in the watch window). What is this integer representation actually? Have you talked about this in any of your posts?

  2. brucedawson says:

    The format was briefly discussed in the very first post (http://randomascii.wordpress.com/2012/01/11/tricks-with-the-floating-point-format/) and is alluded to in every other post. It is implicit in the Float_t and Double_t types.

  3. Pingback: Comparing Floating Point Numbers, 2012 Edition | Random ASCII

  4. Pingback: Float Precision–From Zero to 100+ Digits | Random ASCII

  5. Pingback: Intermediate Floating-Point Precision | Random ASCII

  6. Pingback: Floating-point complexities | Random ASCII

  7. Pingback: Exceptional Floating Point | Random ASCII

  8. Pingback: That’s Not Normal–the Performance of Odd Floats | Random ASCII

  9. Right here is the perfect web site for everyone who hopes to find out about this topic. You realize a whole lot its almost hard to argue with you (not that I really will need to…HaHa). You definitely put a fresh spin on a topic that’s been written about for ages. Excellent stuff, just wonderful!

  10. Pingback: Game Developer Magazine Floating Point | Random ASCII

  11. Franklin says:

    printf_s(%1.8e”, 8);

    This may need correction.

  12. Pingback: Float Precision Revisited: Nine Digit Float Portability | Random ASCII

  13. tty says:

    Would printf(“%1.8e”, &floatValue) & printf(“%.9g”, &floatValue) accomplish the same purpose?

    • brucedawson says:

      As long as nine digits of mantissa are printed you’re fine. %1.8e is one format that does that, but others are fine. It doesn’t matter whether it’s one digit in front of the decimal point and eight after, or nine after, or nine before (leading and trailing zeroes don’t count), as long as you have nine digits.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s