Say it with me:
Floating point is not precise.
Everybody knows it, but it’s worth re-inking the tattoo every few weeks.
Here’s what happens when you maintain a running total using single-precision floating point, double-precision floating point, and .NET’s Decimal (precise) types:
Running totals
Add Single Double Decimal
+ 0.00 = 0 0 0
+ 265.14 = 265.14 265.14 265.14
+ 556.21 = 821.350037 821.35 821.35
+ 989.34 = 1810.69006 1810.69 1810.69
+ 611.05 = 2421.74 2421.74 2421.74
+ 880.13 = 3301.86987 3301.87 3301.87
+ 252.96 = 3554.82983 3554.83 3554.83
+ 953.09 = 4507.92 4507.92 4507.92
+ 704.50 = 5212.42 5212.42 5212.42
+ 773.02 = 5985.44 5985.4400000000005 5985.44
+ 141.35 = 6126.79 6126.7900000000009 6126.79
+ 861.57 = 6988.36 6988.3600000000006 6988.36
+ 555.05 = 7543.40967 7543.4100000000008 7543.41
+ 331.99 = 7875.4 7875.4000000000005 7875.40
+ 68.30 = 7943.69971 7943.7000000000007 7943.70
+ 126.06 = 8069.76 8069.7600000000011 8069.76
+ 93.85 = 8163.61 8163.6100000000015 8163.61
+ 371.99 = 8535.6 8535.6000000000022 8535.60
+ 640.86 = 9176.46 9176.4600000000028 9176.46
+ 791.58 = 9968.04 9968.0400000000027 9968.04
+ 48.06 = 10016.1 10016.100000000002 10016.10
+ 994.79 = 11010.89 11010.890000000003 11010.89
+ 462.06 = 11472.9492 11472.950000000003 11472.95
+ 894.17 = 12367.1191 12367.120000000003 12367.12
+ 73.58 = 12440.6992 12440.700000000003 12440.70
+ 928.79 = 13369.4893 13369.490000000002 13369.49
+ 912.05 = 14281.5391 14281.54 14281.54
+ 396.61 = 14678.1494 14678.150000000002 14678.15
+ 173.86 = 14852.01 14852.010000000002 14852.01
+ 530.15 = 15382.16 15382.160000000002 15382.16
+ 315.51 = 15697.67 15697.670000000002 15697.67
+ 78.32 = 15775.99 15775.990000000002 15775.99
+ 50.18 = 15826.17 15826.170000000002 15826.17
+ 774.53 = 16600.7 16600.7 16600.70
+ 514.21 = 17114.91 17114.91 17114.91
+ 509.96 = 17624.8711 17624.87 17624.87
+ 888.53 = 18513.4 18513.399999999998 18513.40
+ 969.67 = 19483.07 19483.069999999996 19483.07
+ 953.47 = 20436.541 20436.539999999997 20436.54
+ 182.70 = 20619.24 20619.239999999998 20619.24
+ 137.63 = 20756.8711 20756.87 20756.87
+ 569.61 = 21326.48 21326.48 21326.48
+ 404.25 = 21730.73 21730.73 21730.73
+ 443.83 = 22174.56 22174.56 22174.56
+ 765.74 = 22940.3 22940.300000000003 22940.30
+ 185.64 = 23125.9414 23125.940000000002 23125.94
+ 35.75 = 23161.6914 23161.690000000002 23161.69
+ 533.13 = 23694.8223 23694.820000000003 23694.82
+ 671.39 = 24366.2129 24366.210000000003 24366.21
+ 677.42 = 25043.6328 25043.63 25043.63
A little imprecision isn’t the end of the world, obviously, and there are workarounds (rounding to two decimal places). But if you’re cross-checking a precise financial system’s running total against your own imprecise floating-point total, well, you’re going to get false positives every now and then. Choose a precise data type, though, and you won’t have this problem.
Round-trip format is cool
The .NET BCL does some work to protect you against the vagaries of imprecise floating point formats.
In the window above, the first row shows that 1.111f can be represented precisely by a float.
The second row shows that 1f + .111f doesn’t equal 1.111f – the precision of float is screwing us over just a little (0.00000006, to be precise).
The third row shows that ToString() does a little rounding to hide the inaccuracy (that was caused by imprecision) in the least-significant digit.
The fourth row shows that ToString(“R”) gives a string that is as faithful a representation .NET can give of the actual float value.