Roundoff problem with Float and Marshal

This is a tricky problem. I have rewritten my patch in a way that I hope
is endian-independent, and furthermore allows you to load and save exactly
across any IEEE754 system, whether big or little endian floats.

The test was

pigeon% cat b.rb
#!./ruby -v
io = if ARGV.size != 0
        args = true
        File.new('tt')
     else
        args = false
        File.new('tt', 'w')
     end

srand(1256457)

32.times do |cnt|
   w = rand(111111111)
   x = rand(222222223)
   y = rand(33333333334)
   z = rand(311414313)
   a = (x.to_f + y.to_f / z.to_f) *
      Math.exp(w.to_f / (x.to_f + y.to_f / z.to_f))
   if args
      ma = Marshal.load(io)
      if a != ma
         puts "Error #{cnt} : #{a} -- #{ma}"
      end
   else
      ma = Marshal.dump(a, io)
   end
end
pigeon%

pigeon% ./b.rb
ruby 1.8.0 (2003-04-18) [i686-linux]
pigeon%

pigeon% ./b.rb 1
ruby 1.8.0 (2003-04-18) [i686-linux]
pigeon%

nasun% ./b.rb 1
ruby 1.8.0 (2003-04-18) [sparc-solaris2.8]
Error 25 : 253341198.872703 -- 253341198.872703 -- 5.96046447753906e-08
nasun%

`32.times' was just to have the first error

Guy Decoux

I followed up with Guy on this, and it seems that my original test
script (on a single machine) passes fine on a Sun, which means that
the patch is working for big-endian and little-endian seperately.
Apparently sometimes it makes a mistake when comparing big-endian and
little-endian results. I wonder, however, what causes it? I can
imagine one possibilities:

  1. Math.exp, or some simpler floating-point operation, is behaving
    differently on the two platforms, but only by a small amount. In
    other words, the “true” value of many operations is somewhere inbetween
    two different actually representable doubles, and the FPU has to
    arbitrarily decide which one to choose. I believe the IEEE754 standard
    specifies a tolerance that allows several different answers to be
    compliant, so long as the results of the operations are reasonably close
    to the correct value.

I am wondering if there is any general way to fix this? I tend to think
this is beyond the scope of Ruby; we cannot guarantee that floating-point
programs running on different architectures that are started with the
same state will continue to evolve in the same way, because there is
always the chance for different round-off choices. I think it is ok
if we can simply guarantee that moving a floating-point number across
the network or through a file will not change its value so long as the
architecture remains consistent, and also that the value will not change
much if the architecture changes. I believe this is still the behavior
that is observed with this patch.

Rudi

Hi,

I believe the idea itself is endian-safe, and the patch as posted
it almost safe. It does not assume a binary format, it checks; The line
if (b[0] == 0xc8b43958 && b[1] == 0x3ff3be76)
may need to be adjusted to include the check that it is a little-endian
machine (if floating point numbers are reversed on big-endian machines).

It works only with IEEE754. It is the reason I called it “less
portable”.

I don’t want to solve the problem for every machine to convert
between little and big endian. I just want it to work on the most common
machine, the one I am using, and not break the others. I am confident it
won’t break the other platforms because they will set the floatingType
bits to FT_OTHER, which means that the 2 binary bytes at the end are
simply ignored, and so we are back to the original behavior.

I’m not against that point.

The important point would be that the least significant floating-point
mantissa 8 bits are put in the first byte, then the next least significant
6 bits in the second byte. This is endian-safe so long as you put them
back right, and are using IEEE754.

Sorry, but I can’t understand why you call it endian-safe. It
seems to definitely depend on the endian.

···

At Fri, 18 Apr 2003 23:53:48 +0900, Rudi Cilibrasi wrote:


Nobu Nakada

Nobu writes:

The important point would be that the least significant floating-point
mantissa 8 bits are put in the first byte, then the next least significant
6 bits in the second byte. This is endian-safe so long as you put them
back right, and are using IEEE754.

Sorry, but I can’t understand why you call it endian-safe. It
seems to definitely depend on the endian.

Firstly, I hope you are looking at the version of the patch that is most
recent, that Guy Decoux responded to, using the GETBYTE macro.

This patch works on big-endian and little-endian machines. Guy Decoux
tried my test program on both using this patch and it reports no problems;
(he did not post this test to the newsgroup, but communicated this to
me via private email when I asked him to run my test program; it produces
no failure cases, that’s why he had to create a new test program)
therefore, the patch successfully solves the problem of having different
numbers come back from Marshalling, on both an x86 and a Sun so long
as you run my original program. If you try it, you will also see that
it works fine when you load and store from little-endian to big-endian
machines, for instance x86 to Sun. In fact, Guy Decoux did just this,
and you can see that he had to make it repeat 32 times to find a
single error. That means that 31 out of 32 times, both the Sun
version and the x86 version of his program produced exactly the same
results as saved in his file. If the patch were actually not endian-safe,
then this would have failed (practically) every time. Instead, we see it
working almost all the time, and my explanation for it not being 100% accurate
when going from x86 to Sun is that the underlying calculations themselves,
such as Math.exp, floating point addition, etc, are implemented differently.
It is not from Marshalling inaccuracies, which have been fixed by this.
If you look at my patch, you can see that it checks what type of
IEEE754 is in use at runtime, and sets floatingStart and floatingDir
accordingly; either (0,1) or (7,-1) depending on if the endianness
is little or big, respectively. Then, every time I access a byte in
the double, I do it with respect to these constants (using the GETBYTE
macro which accesses bytes starting with floatingStart going +/-) thus it
is endian-independent. In other words, the first byte after the
terminating NUL in the marshalled string is always the least significant
8 bits of the mantissa, regardless of whether the machine is little
or big endian floats. The next byte is the next 6 bits of the mantissa,
which are more significant than the other 8, again regardless of the
endianness of the machine. This is just like how the htons function
works to create an endian-independent format for shorts. You could
say I store the mantissa of the float in little-endian format. When reading
these bytes back, I again adjust for the endianness appropriately, so
that nothing bad occurs.

If you still think that the patch is endian unsafe, then it should not be
hard to come up with a test program that does one of two things:

  1. on a single machine, shows that a number that is written out using
    Marshal.dump is not the same as the one read back in using Marshal.load.

  2. on a pair of machines, shows that a number written on one using
    Marshal.dump to a file is read back in incorrectly using Marshal.load
    on the same file on a different machine. Note that Guy Decoux was probably
    trying to demonstrate this effect using his test program, but in fact
    I believe he demonstrated instead that the standard floating point math
    functions are not 100% consistent across platforms (as the spec allows
    some latitude), and in fact indirectly proved that the patch is working
    in an endian-independent fashion.

Does this make sense? If not please let me clarify further as this makes
a big difference in my use (and particularly testing of math functions)
of Ruby, as many test cases for loading and saving more complicated objects
such as Complex numbers must rely on the basic loading and saving of
Marshal working correctly for floats.

Happy Easter,

Rudi

Hi,

···

At Mon, 21 Apr 2003 09:24:15 +0900, Rudi Cilibrasi wrote:

Does this make sense? If not please let me clarify further as this makes
a big difference in my use (and particularly testing of math functions)
of Ruby, as many test cases for loading and saving more complicated objects
such as Complex numbers must rely on the basic loading and saving of
Marshal working correctly for floats.

Yes, thank you. I was wrong, and apparently missed
[ruby-talk:69752].

But still I have a question: why restrict to IEEE754?

CVS head version needs no assumption on float format, and no

flag bits.


Nobu Nakada

on the same file on a different machine. Note that Guy Decoux was probably
trying to demonstrate this effect using his test program, but in fact
I believe he demonstrated instead that the standard floating point math
functions are not 100% consistent across platforms (as the spec allows
some latitude), and in fact indirectly proved that the patch is working
in an endian-independent fashion.

I was trying to show that when you work with float you'll never test for
equality but always compare the 2 floats with some precisions.

If I have switched from x86 to sparc, this was in reality to have 2
differents implementations of IEEE754. The difference that you have seen
is in the implementation of IEEE754.

You can try to store the exact representation of IEEE754 in marshal, but
who really care when you know how is defined this standard.

Guy Decoux

Nobu writes,

Hi,

Does this make sense? If not please let me clarify further as this makes
Yes, thank you.
[ruby-talk:69752].
But still I have a question: why restrict to IEEE754?

CVS head version needs no assumption on float format, and no

flag bits.

According to my test program, the version currently in CVS still has
problems; If you try
5000000.times {
w = rand(111111111)
x = rand(222222223)
y = rand(33333333334)
z = rand(311414313)
a = (x.to_f + y.to_f / z.to_f) * Math.exp(w.to_f / (x.to_f + y.to_f / z.to_f))
ma = Marshal.dump(a)
b = Marshal.load(ma)
if a == b

puts “Everything is working fine for #{a}”

else
puts “PROBLEM: a is #{a}, b is #{b}, and ma is #{ma.dump}”
puts “w is #{w}, x is #{x}, y is #{y}, z is #{z}”
exit(0)
end
}
For me produces:
PROBLEM: a is 422570262.410156, b is 422570262.40625, and ma is “\004\010f\03142
2570262.4101562\000\000\000”
w is 108244015, x is 51362626, y is 9466898912, z is 96749751

You will probably also see an error print out. I think it must still
suffer from some roundoff problems. Unfortunately, I do not believe you
can rely on any floating-point function to give exactly correct results
in the least significant bits, as there is still problems with
standardizing accuracy and roundoff rules in most implementations.
(though they are supposed to be exact)

I think it is a tricky question of whether or not to use a 2-bit
“format flag” to say that it is an IEE754 or some other type of
Marshal’d Float. Here are advantages to each:

  1. If no format flag is used:
  • saves space
  • seems cleaner in the Marshal format, because it has fewer component parts
  1. If a format flag is used:
  • it allows the possibility for other ways of saving the final bits of
    a float in the future, for instance if we want to save Cray or VAX 16-byte
    floating point numbers, it would be possible to add. Without a flag, it
    may be difficult to know how many bytes of mantissa follow, or what number
    to multiply by (instead of 0x10000) to get correct results.
  • it allows the ability to detect (through some Marshal function like
    Marshal.prevent_rounding() and subsequent Exception throwing) at Ruby
    runtime whether or not roundoff error might occur. In some applications,
    for instance in interface coding, you may want to know whether or not it
    is safe to read a Float into Ruby and then write it back out. Without
    a format flag, you cannot know whether or not you will lose precision when
    doing this. This then makes Ruby less viable for persistent data storage.

I believe that although your solution involving modf, ldexp, and frexp
appears more portable in that it uses fewer direct byte accesses, in
reality it is unlikely to work on any format but IEEE754, just as mine
will not. This is because of the constant-width addition of 2 bytes
that is only appropriate for IEEE754 8-byte double. And the more serious
problem is that it seems we cannot rely on correct precise behavior as
my test above indicates. Essentially, there’s many more opportunities for
something to go wrong in this portable solution than the less portable
but more often working simple bitmask and copy, which is easier for
overworked processors. Another issue is the reliance on these functions
being 100% accurate, even in theory. Testing shows that something is
wrong with them above, but even if this bug is fixed (and I cannot figure
it out myself with some trying), it seems to me the definitions of these
functions essentially require the FPU to be working in standard
m * 2^e style floating points, because working in say, m * 10^e
representation would not allow exact answers, or any fixed-point rep,
and so on. In the end, though, I think even the multiple-format
portability issue is of only academic interest, as I have heard essentially
all new hardware is IEEE754 compliant, and they now have special-purpose
software libraries that allow you to calculate with arbitrarily high
precision when necessary, which makes it even less useful to support
alternative formats.

Rudi

···

At Mon, 21 Apr 2003 09:24:15 +0900, >Rudi Cilibrasi wrote:

Hi again,

So, I checked out the latest version from CVS and it does seem to work.
I showed my friend John Tromp (another Rubyist) the problem, and he thinks
we are all barking up the wrong tree here: he says the problem is only in
strtod and there is no validity to the argument we’ve convinced ourselves
concerning inexact decimal representations making it impossible; he looked
at the strtod code in the ruby CVS, and thinks the technique using the
table of powers of ten is inherently imprecise. To test this, I changed
the ruby_strtod in util.c to call through to the standard libc strtod, and
now all the tests pass, even when I deactivate Nobu’s load_mantissa. I
looked at the strtod used in glibc version 2.3.2 and it uses the MP library
to do exact integer arithmetic; I suppose this explains the bug further.
It looks like the glibc strtod.c is about 1600 lines – so it seems a rather
tedious but well-defined task to convert this glibc strtod.c to use
Nobu’s BIGNUM routines solve this bug correctly. Any volunteers?
Otherwise I think we will be stuck with a hackish (but working) Marshal
representation for Float in Ruby 1.8.

Rudi

Hi,

···

At Tue, 22 Apr 2003 22:14:34 +0900, Rudi Cilibrasi wrote:

I showed my friend John Tromp (another Rubyist) the problem, and he thinks
we are all barking up the wrong tree here: he says the problem is only in
strtod and there is no validity to the argument we’ve convinced ourselves
concerning inexact decimal representations making it impossible; he looked

I agree with your friend, and posted patches at [ruby-dev:20071].
Could you try http://blade.nagaokaut.ac.jp/ruby/ruby-dev/20071?


Nobu Nakada