2

I develop a c++ app (Windows 7, 64 Bit, VS 2008) where the following formula is used (all vars are of type double):

mValue = floor(mValue/mStepping)*mStepping;

The idea is to shorten a number to a given count of decimal places. I think there are better ways to do this, but this is not the question here (but if you have a better option, please bring it on!).

mValue comes from user input, so in most cases the number of decimal places is already ok. But for some cases the output differs from the input.

For example mStepping has a value of 0.1 (which should round to one decimal place). Now if mValue has a value of 14.6, everything is ok. If mValue is 14.7 the result is 14.6.

So, why is floor(14.7/0.1)*0.1 = 14.6 ?

I tested other values and around 20% of them differed by 0.1. I digged further and discovered, that 14.7/0.1 has a different binary encoding than 147.0:

14.7/0.1 = ff ff ff ff ff 5f 62 40
147.0    = 00 00 00 00 00 60 62 40

I understand that the same number can be encoded differently as a double. But why does floor() handle them differently? And what can I do against it?

McNumber
  • 177
  • 10

4 Answers4

6

The usual problem: Not all exact decimal fractions can be represented exactly in binary. In your case it's 0.1 and 14.7 as well. By dividing by 0.1 you're actually not arriving at 147 but rather a minuscule amount below it:

14.699999999999999289457264239899814128875732421875

By multiplying with 0.1:

0.1000000000000000055511151231257827021181583404541

You're arriving at:

146.999999999999971578290569595992565155029296875

I think you're beginning to see the problem now, right? Flooring that number will obviously give you 146.

What you can do against it? If you want exact decimal results, then use a number type that represents decimal fractions, or a bignum library.

Oh, and a side note: No, the same number does not have different double representations. It's just that your interpretation what a number is and how it behaves is different from floating-point math.

Joey
  • 344,408
  • 85
  • 689
  • 683
3

I understand that the same number can be encoded differently as a double.

What you don't understand is that 14.7/0.1 and 147.0 are not the same number.

Write for example the following test code:

if (14.7/0.1 == 147.0)
{
    cout << "We are equal!";      
}
else
{
    cout << "We are different!";
}

You will see that 14.7/0.1 is not equal to 147.0. It's not "the same number encoded differently", but a different number.

floor doesn't have anything to do with your surprise. floor simply returns a (possibly) different value when called with different input.

Daniel Daranas
  • 22,454
  • 9
  • 63
  • 116
3

The issue is that none of the numbers that you are using can be exactly representing in binary floating point format. That's the nature of binary floating point. For more details read What Every Computer Scientist Should Know About Floating-Point Arithmetic.

The closest single precision floats to your two numbers are:

  • 0.1 = + 0.10000 00014 90116 11938 47656 25
  • 14.7 = + 14.69999 98092 65136 71875

So, 14.7/0.1 is evaluated first, and because the values are not exactly representable, that just happens to evaluate to a value that is less than 147. So when you floor it, you turn it into 146. And hence the result that you observe.

I understand that the same number can be encoded differently as a double.

In general that is not the case. Your problem is that neither 14.7 nor 0.1 can be exactly represented at all. The issue is nothing to do with uniqueness of representation.

So, to perform your sum exactly, you need to use decimal arithmetic. That's not something that is built in to C++ and you would need to use a third party library for that.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
3

In addition to the other answers about the imprecision of floating point representation of decimal and fractional numbers, floor() is also the wrong choice when calculating a decimal representation to a number of significant digits of a floating-point number. You should be rounding instead - and the result shouldn't be stored in a floating point number but in some other way.

Community
  • 1
  • 1
Joris Timmermans
  • 10,814
  • 2
  • 49
  • 75
  • 1
    I'm not so sure I agree. Since 14.7 cannot be represented, no amount of rounding helps. What you typically do is apply decimal digit limits when you format as text. But leave the underlying value as is. Or use decimal arithmetic. – David Heffernan May 08 '13 at 13:23
  • @DavidHeffernan - you have a point. Rounding would only be useful if you were going to some sort of decimal (fixed point, rational) representation. – Joris Timmermans May 08 '13 at 13:37