Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example where 1/3 + 1/3 != 2/3 #63

Open
munrocket opened this issue Sep 12, 2021 · 11 comments
Open

Example where 1/3 + 1/3 != 2/3 #63

munrocket opened this issue Sep 12, 2021 · 11 comments

Comments

@munrocket
Copy link

munrocket commented Sep 12, 2021

There some rumours that Decimal128 is silver bullet. But we can construct expression similar to

0.1 + 0.2 != 0.3

But in another base. This will be fair to add it in readme.

1m/3m + 1m/3m != 2m/3m
@leebyron
Copy link

This is a good argument for pursuing BigDecimal instead of Decimal128, or at the very least support for the proposed behavior of throwing TypeError for use of the / operator in favor of a div() function.

@munrocket
Copy link
Author

Ha-ha I believe it’s not possible with BigDecimal too. Warn about precision issue in readme section will be enough.

If you want full precision for any expression, it’s possible with symbolic computation. But this is another topic.

@Yogu
Copy link

Yogu commented Sep 30, 2021

Ha-ha I believe it’s not possible with BigDecimal too. Warn about precision issue in readme section will be enough.

The BigDecimal option in the proposal forbids to use the / operator in favor of a div function that also requires a precision argument, so silent rounding errors can't occur in this proposal.

I agree with @leebyron here that it's a good argument in favor of explicit division methods (possibly with a required precision parameter) instead of a / operator that silently loses precision.

In my opinion, an important reason to use something like BigDecimal is to avoid accidental rounding errors, and a footgun like a / that introduces rounding errors kind of defeats this purpose.

@munrocket
Copy link
Author

Thanks for clarifying, looks legit.

@jessealama
Copy link
Collaborator

There are countless examples like this where 128-bit floats won't deliver an exact result. When we make these examples, I think we're thinking of == as mathematical equality (admittedly, this is how the current Decimal128 API says == should be interpreted -- I wonder if that's intentional?). But in that case, we're setting up Decimal128 for failure because there are all sorts of counterexamples where 1/n + 2/n won't (exactly) work out to 3/n.

One way to "rescue" Decimal128 from counterexamples like these would be to use an equality relation with a specified error tolerance. Imagine an equals method that would allow us to check whether two Decimals are equal up to a certain tolerance. (This kind of equality relation makes sense for the BigDecimal interpretation, too, where some operations just can't be exactly represented.) In our aplications, we would check not so much whether 1/9 + 2/9 is equal to 3/9, but whether they diverge by less than, say, 0.0000000000001. That might be good enough for many applications. And Decimal128 would be able to do that.

What do you think?

(I say this as someone who, personally, prefers the arbitrary-precision approach. I'm just saying here this apparent flaw of Decimal128 is perhaps not as fatal as it might appear.)

@jessealama
Copy link
Collaborator

Just to add to my previous comment: you can use a language that has Decimal128 support to see that the 1/9 + 2/9 == 3/9 can be recovered. Here's a Java snippet that shows you that Decimal128 can indeed handle such cases. The syntax is quite bulky, but I hope the intention comes through:

import java.math.BigDecimal;

public class FractionSum {

    public static void main(String[] args) {
	BigDecimal one = new BigDecimal("1");
	BigDecimal two = new BigDecimal("2");
	BigDecimal three = new BigDecimal("3");
	BigDecimal nine = new BigDecimal("9");
	BigDecimal a = one.divide(nine, java.math.MathContext.DECIMAL128);
	BigDecimal b = two.divide(nine, java.math.MathContext.DECIMAL128);
	BigDecimal c = a.add(b, java.math.MathContext.DECIMAL128);
	BigDecimal d = three.divide(nine, java.math.MathContext.DECIMAL128);
	System.out.println(c.toString());
	System.out.println(d.toString());
	System.out.println(d.equals(c) ? "yes" : "no");
    }
}

Running this:

$ java FractionSum.java
0.3333333333333333333333333333333333
0.3333333333333333333333333333333333
yes

@munrocket
Copy link
Author

munrocket commented Nov 15, 2022

Yea, proper example is differ.

1m/31m + 2m/31m != 3m/31m
1m/3m + 1m/3m != 2m/3m

With java it will give this result

$ java FractionSum.java
0.09677419354838709677419354838709678
0.09677419354838709677419354838709677
no

$ java FractionSum.java
0.6666666666666666666666666666666666
0.6666666666666666666666666666666667
no

@munrocket munrocket changed the title Example where 1/9 + 2/9 != 3/9 Example where 1/3 + 1/3 != 2/3 Nov 15, 2022
@jessealama
Copy link
Collaborator

Good example! I guess somewhat bigger numbers (denominators, that is) are needed to illustrate the issue. The larger point remains: Decimal128 is definitely going to fail on all sorts of cases that BigDecimal handles correctly. But if one knows that in advance, a different approach to equality can be recommended, and which (probably) does the job.

Here's an updated Java snippet that illustrates what I have in mind with error tolerance:

import java.math.BigDecimal;

public class Decimal {

    private static BigDecimal tolerance = new BigDecimal("0.0000000001");

    public static boolean almostEqual(BigDecimal a, BigDecimal b)
    {
	return -1 == a.add(b.negate()).abs().compareTo(tolerance);
    }

    public static void main(String[] args) {
	BigDecimal one = new BigDecimal("1");
	BigDecimal two = new BigDecimal("2");
	BigDecimal three = new BigDecimal("3");
	BigDecimal nine = new BigDecimal("31");
	BigDecimal a = one.divide(nine, java.math.MathContext.DECIMAL128);
	BigDecimal b = two.divide(nine, java.math.MathContext.DECIMAL128);
	BigDecimal c = a.add(b, java.math.MathContext.DECIMAL128);
	BigDecimal d = three.divide(nine, java.math.MathContext.DECIMAL128);
	System.out.println(c.toString());
	System.out.println(d.toString());
	System.out.println(d.equals(c) ? "equal" : "not equal");
	System.out.println(almostEqual(d, c) ? "almost equal" : "not almost equal");
    }
}

Here's I've implemented a very simple "almost equals" comparison: is |a - b| < 0.0000000001? Running that, we can see that it works out:

0.09677419354838709677419354838709678
0.09677419354838709677419354838709677
not equal
almost equal

@munrocket
Copy link
Author

|a - b| < 0.0000000001 can be implemented with number too. Maybe in this case "almost equal mantissa" is better:

0.9677419354838709677419354838709678e-60
0.9677419354838709677419354838709677e-60
not equal
almost equal mantissa

"almost equal" is practical solution if you know that you have not so many floating point operation. But each FLOP can accumulate round-to-nearest-even rounding and potentially can be bigger than epsilon?

@jessealama
Copy link
Collaborator

(...gradually returning to the discussion) I took a look at your initial example and wondered if I misunderstood something important. What do you mean by "in another base", in the original question? Are you thinking of bases other than 10, similar to the discussion in #75?

@Rudxain
Copy link
Contributor

Rudxain commented Mar 7, 2023

I'm aware that 0.000...001 tolerance is just an example. But I'd like to emphasize that when "fuzzy comparison" is implemented, we should be very careful with the implementation.

An example of what NOT to do: Jest's toBeCloseTo (no offense to Jest devs). The problem is that it compares floating-points as if they were fixed-points (as if the fractional part had constant precision). So comparing 1/2**53 with 1/(2**53 - 1) works fine, but 2**53 with 2**53-1 returns false, despite the mantissas only differing by 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants