Demystifying Doubles: Consistent Inaccuracy
By Adrian Sutton
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.