Demystifying Doubles: Consistent Inaccuracy

LMAX Exchange

Of all the data types, double is probably one of the most
misunderstood. A huge amount of folk lore has been built up around it to
help protect developers from falling into its many pitfalls.
 Lately I’ve done a lot of work replacing usage of BigDecimal with
double and learnt a lot about where those pitfalls are and how the folk
lore can be misleading.

The great challenge with double is that it
has a degree of inaccuracy because of the way the number is actually
stored.  For example, the number 2983792734924.2293 actually
winds up being stored as 2983792734924.2295, a tiny difference but
with far reaching consequences. All of the folk lore around doubles is
designed to deal with this potential for inaccuracy.

One piece of
folk lore is that you should never do a strict comparison of double
values (e.g. with == or !=).  Due to the inaccuracy of doubles you
should always compare them with a level of tolerance:

abs(doubleA - doubleB) < tolerance

It’s
important to realise however, that this inaccuracy isn’t random, it’s
perfectly consistent and predictable.  The
number 2983792734924.2293 always comes out
as 2983792734924.2295.  The same applies if you parse it from a
string: parseDouble(“2983792734924.2293″) always comes out as 2983792734924.2295.  As a result the following are consistently true:

2983792734924.2293 == 2983792734924.2293
parseDouble("2983792734924.2293") == 2983792734924.2293
parseDouble("2983792734924.2293") == parseDouble("2983792734924.2293")

Introducing
tolerance to the comparison can lead to subtle bugs. For example, if we
were updating our model with user input we might have something like:

// In our model:
public void setAmount(double amount) {
  this.amount = amount;
  fireChangeEvent();
}
// In our UI code:
model.setAmount(parseDouble(widget.getText()));

So far so good,
but what if we want to avoid firing the change event if the value hadn’t
really changed?  We might change the model code to:

public void setAmount(double amount) {
  if (abs(amount - this.amount) > 0.01) {
    this.amount = amount;
    fireChangeEvent();
  }
}

 It follows the double folk lore, but also introduces a
bug. Let’s say the user first enters the amount ’1′.  Then they
change the amount to ’1.001′.  The second amount is within the
tolerance so it’s not considered an actual change. The user is now
looking at a text field that says 1.0001, but the model thinks the value
is still 1.   You can make the tolerance smaller but that just
means the user has to enter more zeros to trigger the problem.

The
right answer is to take advantage of the fact that inaccuracy with
doubles is consistent and do the comparison using strict comparison:

public void setAmount(double amount) {
  if (about != this.amount) {
    this.amount = amount;
    fireChangeEvent();
  }
}

So when doesn’t this work? When the two values are calculated in different ways. So:

(1491896367462.1147 * 29.837927349242293) / 2.3982983923
= 18561116318075.207
1491896367462.1147 * (29.837927349242293 / 2.3982983923)
= 18561116318075.203

The only difference between the two equations is the order of evaluation they should
give the same answer, but because doubles have a degree of inaccuracy
the intermediate result is rounded to something that will fit in a
double, so the order of evaluation matters because it affects what
rounding is done to the intermediate result. Repeating the exact same
calculation will always give the exact same result, but if there’s any
chance that the values were calculated in different ways, comparisons
must be done with tolerance.

So the folk lore about needing to
compare doubles with some tolerance is perfectly valid, good advice, but
be aware that it introduces its own inaccuracy which may cause
problems. You should consider what the impact of falsely deciding that
the values are different vs falsely deciding that the values are the
same; which side should you err on?

Sometimes you really do want a strict comparison, even between doubles.

Any opinions, news, research, analyses, prices or other information ("information") contained on this Blog, constitutes marketing communication and it has not been prepared in accordance with legal requirements designed to promote the independence of investment research. Further, the information contained within this Blog does not contain (and should not be construed as containing) investment advice or an investment recommendation, or an offer of, or solicitation for, a transaction in any financial instrument. LMAX Group has not verified the accuracy or basis-in-fact of any claim or statement made by any third parties as comments for every Blog entry.

LMAX Group will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information. No representation or warranty is given as to the accuracy or completeness of the above information. While the produced information was obtained from sources deemed to be reliable, LMAX Group does not provide any guarantees about the reliability of such sources. Consequently any person acting on it does so entirely at his or her own risk. It is not a place to slander, use unacceptable language or to promote LMAX Group or any other FX and CFD provider and any such postings, excessive or unjust comments and attacks will not be allowed and will be removed from the site immediately.