Fatal Double (Float) Arithmetic
BlitzMax Forums/BlitzMax Programming/Fatal Double (Float) Arithmetic
| ||
Hey there, I'm working on a program right now, which relais on percise arithmetical operations. However, BlitzMax seems to terrible screw this up. As far as I'm concerned, the following problem has been around for a rather long time now, so I don't get why it hasn't been corrected yet. Now compare the prints. You may also copy out single arithmetic steps to analyse them. I hope you can correct that error, because if you don't, I have to either switch to fixed-point arithmetic, or to another language. Have a nice day - Greetings from germany. - Kernle Edit: If you haven't realised it yet, the result should allways be a straight 0. |
| ||
This is the result on my Mac (bmx 1.38 x86 + my bmk):0.0000000000000000 0.0000000000000000+0.0000000000000000+90.000003576278687-90.000000000000000=3.5762786865234375e-06 0.0000000000000000+0.0000000000000000+90-90.000000000000000=0.0000000000000000 0.0000000000000000+0.0000000000000000+90.000000000000000-90.000000000000000=0.0000000000000000 |
| ||
Arg, sorry, I forgot to remove the ($0000005A) from the first print... Replaced with the original (intended) NextPointX*MaxSpeed. My results: Windows 7 x86 + x64 & Windows XP x86 - BMX 1.36 1.4901160601728732e-006 0.0000000000000000+0.0000000000000000+90.000003576278687-90.000000000000000=3.5762786865234375e-006 0.0000000000000000+0.0000000000000000+90-90.000000000000000=0.0000000000000000 0.0000000000000000+0.0000000000000000+90.000000000000000-90.000000000000000=0.0000000000000000 |
| ||
0.00000000000000000+0.00000000000000000+90.000003576278687-90.000000000000000 still isn't 3.5762786865234375e-006 !!! Why isn't it? 3.5762786865234375e-006 means 3.5762786865234375 * 10^-6. Which is exactly correct. Welcome to the joys of floating point maths. Floating point maths are never 100% accurate. Its nothing to do with Blitz - you'll find the same in any language. I hope you can correct that error, because if you don't, I have to either switch... ...to another language. Well, good luck with that. ;) |
| ||
This is a design choice rather than a bug. A constant has a data type, just like a variable. "Hello" is a string, 7 is an integer etc. I guess because it was intended primarily for game development BiltzMax's default floating point type is single precision. So the constant 1.2 is single precision. To make a constant double precision you must use a type tag, the same as with a variable. You can tell the type of a number when it is Printed, or converted to a string. An integer never has a decimal point. A floating point number always does. A single precision number displays with nine significant digits while double precision has seventeen. By "significant" I mean counting from the first non-zero digit. Notice the use of "scientific notation", the e-006 in the example. This is used to avoid excessively long displays. One final comment. NEVER expect exact results with floating point numbers. Sometimes they will be exact, but usually not. You can see the different types here: Print Print 1.2 ' Single precision Print Double( 1.2 ) ' Single, then converted to double Print Print 1.2! ' Three examples of double precision, to emphasize Print 1.3! ' that you should never expect exact results. Print 1.4! Print x! = 1.2! Print x ' But this particular case was exact, right? x = x - 1.0! ' No, not really. It just looked that way when rounded to Print x ' seventeen digits for display. This is not 0.2, the exact result. Print Print 0.0000000000123! Print 123000000000000000! |
| ||
So why is 90.0 * 1.2 = 90.000003576278687? It should be 90.0, and then the correct result would be 0. My only guess is that BlitzMax doesn't convert the affected values (NextPointX and MaxSpeed) startvalues correctly. But thats another story. I know that floating point arithmetics are not 100% accurate, but still I dont get what's going on with the 90.000003576278687. And yes, I'm familier with that *10^-6 thingy. I just don't know how it's called in english. (Ah yes, scientific notatation...) |
| ||
The issue of 75.0 * 1.2 = 90.000003576278687 is a problem with double precision arithmetic on your cpu, not Blitzmax. I get exactly the same results in plain C. Ironically, single precision floats work better in this case. |
| ||
Just use an arbitrary precision math library... no big deal. |
| ||
...And in case you weren't aware, Brucey just happens to have one of those up for download in his SVN repository. |
| ||
Now this is real crap. But okay, it least there is a solution in form of Brucey's module. Thanks everyone, I'll look into it. |
| ||
Just to restate, if you have use doubles then you will get more than 14 decimal places: Local MaxSpeed:Double = 1.2! Print MaxSpeed-1 but if you use floats, even by mistake (note the missing !), you will end up with only 7 digits, Local MaxSpeed:Double = 1.2 Print MaxSpeed-1 |
| ||
I don't get your point skidracer, besides that both numbers are incorrect, compared to the wanted value, literarely a 1.2000000000000000 -> 0.2000000000000000. The effect is interesting tough. |
| ||
My point was that using the literal 1.2! rather than 1.2 makes your code emulate the solution of your base 10 problem an order of a billion times more accurately. Agreed it's still not perfect but as has been explained, binary computers tend to run out of memory when trying to accurately store the results of a simple decimal division such as one divided by 10. Other computer languages tend to round their output to "nearest decimal number with one less digit" which nicely hides the terrible binary error accumulation that is happening internally. |
| ||
Even the seventeen digit display in BlitzMax can make numbers look exact when they are not. The original example uses 1.2 in calculations. As mentioned, this should have been 1.2! as a double precision constant. The hope is that this is 1 + 2/10, exactly. But it is really 1 + 900719925474099 / (2^52). That works out to about 1.199999999999999955591... ( the first 5 is the 18th digit ). When rounded to seventeen digits it looks like 1.2 exactly even though it is slightly less. After subtracting 1 we really have 0.199999999999999955591... The seventeen digit diplay is then 0.19999999999999996 Print Print 1.2! Print 1.2! - 1 Although approximate, this is good enough for almost any practical problem. For example, it would give the distance travelled by the Earth in one orbit around the sun ( roughly 10^12 meters ) to within a small fraction of a millimeter. |
| ||
Simple rule: never use '=' as comparing operator in conjunction with floats or doubles. Add some 'error-space' and it will work. e.g. if (1.2!-1 >=.1999) and (1.2!-1 <= .2001) then ... |
| ||
So, there is only one question still open to me. As pointed out in the code above, there is still a scenario i couldn't reproduce with that code. Codesnippet: Output: StartPointX: 0.00000000000000000 StartPointY: 0.00000000000000000 NextPointX: 75.000000000000000 NextPointY: 90.000000000000000 MaxSpeed: 1.2000000000000000 CrossX: -1.3877787807814457e-015 (StartPointX*MaxSpeed + StartPointY + NextPointX*MaxSpeed - NextPointY) = -3.3306690738754696e-015 (NextPointX*MaxSpeed - NextPointY) = -3.3306690738754696e-015 (2*MaxSpeed) = 2.3999999999999999 (0.00000000000000000 + 0.00000000000000000 + 90.000000000000000 - 90.000000000000000) / 2.3999999999999999 = -1.3877787807814457e-015 So, what should i think of that? I have not the slightest idea how that code produces that negative value. And as I told, i can only reproduce it with my own code (see snippet above). The problem is indeed the negative value. Usually, negative values apear only if the math-operation is feed with illegal values (e.g. NextPointX < StartPointX), and as such the whole process is aborted... Wicked |
| ||
It is the exact same problem. 0.00000 + 0.0000 + 90.0000 results in a number close to 90. In the same manner, subtracting 90 results in a number close to 0. It seems that in this particular case, a number slightly larger than 90 is subtracted, which results in a very, very small negative number. The difference of the result to 0 is marginal, but still there. This is, too, a problem of lacking accuracy. One has to understand that the results can be smaller or larger than the exact result; if the exact result is zero, floating point operations can also result in negative numbers (but again, only slightly off from the exact result). To prevent wrong branches in limit cases, don't compare to zero (If Foo > 0.0), but a number slightly larger/smaller than zero (If Foo > -0.0001:Double). |
| ||
But why is it a problem? There will be small errors, some positive and some negative. I explained where the error arises. In simplified form it is ' Very close, not exact. Some differences are so small they cannot be seen with 17-digit display. Local speed! = 1.2! ' looks like 1.2, but is really 1.199999999999999955591... ' speed is a little less than 1.2 Print Print speed ' a little less than 1.2, difference too small to see Print 75*speed ' a little less than 90, difference too small to see Print 75*speed - 90 ' difference ( negative ) is now visible Print 90 - 75*speed ' difference ( positive ) is now visible You were hoping for 75*speed = 90. It is really 90 - 0.00000000000000333. Why is that important? Imagine a ball 90mm in diameter, a little bigger than a tennis ball. Well, we hoped for exactly 90mm, but floating point made it 90 - 0.00000000000000333. The error is smaller than an atom. |
| ||
Its a problem cause I didn't knew about it. With that "tolerance" in mind I can work. Just one thing i can't get in my mind... When you type it all into the windows/mac whatever calculator, the result is the excat 0. Why is that, compared to BMax? (I typed it in with all the zeros). |
| ||
The calculator doesn't perform arithmetic on the CPU for this very reason. The calculators priority is accuracy, the CPUs priority is speed. |