Enter the 36 chambers of infrastructure wu-tang

Tuesday, March 25, 2008

Floating point arithmetic, bug reports, and monkey patching

It turns out that Barbie was right and math actually is hard. At least, math is hard if you adhere to IEEE floating point spec. C adheres to that spec. Languages built on C inherit that adherence and, even though they could fix some of the problems that come with it, they leave things broken.

In the case of Ruby, they also tell you there is no problem. And it is fixed in the next version. With this monkey patch. But I'm getting ahead of myself.

Here's Ruby (1.8.6 on OS X 10.5) in action:

>> (9.54 / 0.001)
=> 9540.0
>>

Well, that seems straightforward enough. 9540 is certainly the right answer. I need the integer representation, not the floating point version, though, so we'll just call the handy Float#to_i function:

>> (9.54 / 0.001).to_i
=> 9539
>>

Don't feel badly, I blinked several times and had a few more sips of beer before I realized what I was seeing. A second ago, the value was 9540, but now it is 9539. I'll just bang on it a bit to see what I've done wrong:

>> (9.54 / 0.001).to_s.to_i
=> 9540
>>

Yup. By converting the value to a String and then converting that String to an Integer we get the right value back. Sometimes Ruby admits to the underlying IEEE spec edge case, other times it hides it. Sadly, Python behaves similarly (though we are alerted to a problem immediately, rather than having it sneak up on us later):

>>> (9.54 / 0.001)
9539.9999999999982
>>>

Yikes! And, frowny face, the other trouble is here, too:

>>> str(9.54 / 0.001)
'9540.0'
>>> int(9.54 / 0.001)
9539
>>>

I wrote a simple C program to play directly with the underlying types. Here are two variants that illustrate the problem clearly:

#include

int
main()
{
  double f = 9.54 / 0.001;

  printf("%f %i\n", f, (int)f);

  return 0;
}

$ ./test
9540.000000 9539
$

#include

int
main()
{
  double f = 9.54 / 0.001;

  printf("%f %i\n", f, (int)(float)f);

  return 0;
}

$ ./test
9540.000000 9540
$

So, I filed a bug against Ruby. Turns out someone else filed a similar bug a few weeks ago. Both of our bugs were rejected on the grounds that things were working as they were supposed to. I continued to argue that, regardless of what the underlying data types are doing, Ruby could and should do better. We went back and forth a bit, and the Ruby gent finally noticed I filed the bug against 1.9.

Why does that matter? Well, it seems that, even though it totally, definitely, under no circumstances is a bug, they've added functionality to Float#round that, among other things, means it is possible to mostly work around the problem. You can now pass it an argument giving the rounding precision desired. So now we can do this (Ruby 1.9 on OS X 10.5):

irb(main):002:0> (9.54 / 0.001).round(2).to_i
=> 9540
irb(main):003:0>

Our grumpy Ruby Core gent was even kind enough to provide a monkey patch to Float to provide a truncate method that actually worked reliably:

class Float
  def truncate_rounding(figs)
    if figs > 0
      n = 10 ** figs
      (self * n).round / n
    else
      n = 10 ** -figs
      (self / n).round / 10 * (n * 10)
    end
  end
end

Since this is absolutely not a bug, the workaround (I can't call it a fix since it introduces problems with overflow) will no doubt remain a hack. A hack to make Float behave correctly. That's a darned shame, Ruby folks.

No comments: