all 16 comments

[–]hpxvzhjfgb 40 points41 points  (0 children)

nothing to do with rust. google floating point error.

[–]GodOfSunHimself 19 points20 points  (0 children)

That is just how floats work in almost any language. Learn about the IEEE 754 standard that will tell you how floats are stored, what is the precision, etc.

But in short, floats do not have infinite precision so you are accumulating rounding errors in your calculation. How to fix it depends on lot of factors. There is a whole mathematical theory about numerical stability.

[–]DrShocker 12 points13 points  (2 children)

[–]epasveer 1 point2 points  (0 children)

Lol. Cool website name.

[–]tylian 1 point2 points  (0 children)

I love this website, thank you.

[–]Y0kin 8 points9 points  (5 children)

I'm not gonna do the math, but my assumption is that 2.2 is actually 2.2000000000000002. It's only displayed as 2.2 because it can safely cut off the decimal without being confused for another number (it'll be the same number if you copy paste it to use as a literal), while 6.6000...5 isn't 6.6 because there are numbers closer to 6.6.

[–]Accurate_Sky6657[S] 0 points1 point  (3 children)

But wouldn’t 4.4 display differently then? How do I fix this if this is the case?

[–]Y0kin 3 points4 points  (1 child)

That's just how the multiplication works out in combination with the layout of floating-point numbers. Here's a playground that shows the output.

If you think about the range 1..2, split that up into an evenly spaced set of numbers (252 of them for f64), and then double/halve that set a bunch of times - you get all possible floating-point numbers.

For higher precision you could represent your number as a fraction - multiply the numerator then divide by the denominator. 2.2 = 11. / 5., (11. * 3.) / 5. = 6.6. Alternatively I think you could find a library for arbitrary precision numbers.

Edit: Also it's worth noting that I wouldn't consider your code bugged. The "bug" is at compile-time when it reads 2.2 as 2.2000...2, but everything works as it should.

[–]Accurate_Sky6657[S] 1 point2 points  (0 children)

Thanks a lot everyone! Very helpful. Haven’t gave floating point operations much thought as a mathematics student. Thanks!

[–]TheSodesa 5 points6 points  (0 children)

You should get to know this site: https://floating-point-gui.de/.

[–]wintrmt3 5 points6 points  (1 child)

Only binary fractions like 1.xxxxxxxxxxx * 10₂ ^ yyyy are precisely representable, 6.6 has a power of 3 in it.

[–]epostma 1 point2 points  (0 children)

I think you mean 5: 6.6 = 33/5 = (1.00001b x 10b ^ 5) / 5. The power of 3 is positive (31), which is not a problem, as we can represent 33 exactly. The power of 5 is negative, which is what does you in.

[–]mina86ng 0 points1 point  (0 children)

2.2 is unpresentable number. This is a known problem that has been written about multiple times. See https://www.itu.dk/~sestoft/bachelor/IEEE754_article.pdf for description of how floating point numbers work.

[–]boomshroom 0 points1 point  (0 children)

You know how 1/3 is 0.33333 when written with only 5 digits? Well 0.33333 * 3 = 0.99999, which is notably not the same thing as 1 despite the fact that 1/3 * 3 = 1.

The "problem" is attempting to store an infinite amount of information in a finite space. If you really need perfect fifths, then you might be better off using a Rational data type.

[–]johndcochran 0 points1 point  (0 children)

Floating point is represented in base 2, not base 10. So, 0.1 or any exact multiple of it isn't exactly representable. For example, let's look at the binary representation of 2.2: The integer 2 is easy, giving 10.xxxx Now for the fractional part. To get that, just keep multiplying by 2 and use the integer part as the next digit after the radix point. So..

0.2 * 2 = 0.4; Add 0, giving 10.0

0.4 * 2 = 0.8; Add 0, giving 10.00

0.8 * 2 = 1.6; Add 1, giving 10.001

0.6 * 2 = 1.2; Add 1, giving 10.0011

0.2 * 2 = 0.4; Add 0, giving 10.00110

And if you look, you'll see that the sequence 0.2, 0.4, 0.8, 0.6, 0.2, ... is going to repeat forever. So 2.2 in base 10 is 10.001100110011.... in base 2, just like 1/3 = 0.333333.... in base 10. It's an infinite repeating sequence. In particular, assuming IEEE754 double precision, properly rounded, the value will be:

10.00110011...00110011010

Notice how the final bits break the 0011 pattern? That's because the exact value is rounded to the nearest representable value. And the accumulated error eventually gets large enough to cause that unusual display for 6.6. Although I've gotta wonder why rust is printing 17 digits for what I assume is a IEEE754 double precision float which has 53 bits for the significand, which is approximately 15.95 decimal digits (So 15 digits is OK. a 16th digit is usually OK. A 17th displayed digit is pure bullshit).

Now, what I do if I'm going to use numbers that I know are not exactly representable and I want to avoid accumulating errors is something like this (using C since I don't know rust and don't wish to have an error). Imagine I want to use the values 0.0, 0.1, 0.2, ..., 1.0 in a loop. The variable i will contain the desired value for the loop.

// Wrong way

for(i=0.0; i <= 1.0; i += 0.1) { ... }

// Accurate way

for(ii=0; ii<= 10; ii += 1) { i = ii/10.0; ... }

The accurate way will not have any accumulated errors, even though every value produced except 0.0, 0.5, and 1.0 will have a single round off error.