Fatal Double (Float) Arithmetic

BlitzMax Forums/BlitzMax Programming/Fatal Double (Float) Arithmetic

Kernle 32DLL(Posted 2010) [#1]
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.


Brucey(Posted 2010) [#2]
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



Kernle 32DLL(Posted 2010) [#3]
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



GfK(Posted 2010) [#4]
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. ;)


Floyd(Posted 2010) [#5]
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!



Kernle 32DLL(Posted 2010) [#6]
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...)


matibee(Posted 2010) [#7]
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.


Brucey(Posted 2010) [#8]
Just use an arbitrary precision math library... no big deal.


xlsior(Posted 2010) [#9]
...And in case you weren't aware, Brucey just happens to have one of those up for download in his SVN repository.


Kernle 32DLL(Posted 2010) [#10]
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.


skidracer(Posted 2010) [#11]
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


Kernle 32DLL(Posted 2010) [#12]
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.


skidracer(Posted 2010) [#13]
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.


Floyd(Posted 2010) [#14]
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.


BladeRunner(Posted 2010) [#15]
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 ...


Kernle 32DLL(Posted 2010) [#16]
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


Noobody(Posted 2010) [#17]
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).


Floyd(Posted 2010) [#18]
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.


Kernle 32DLL(Posted 2010) [#19]
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).


matibee(Posted 2010) [#20]
The calculator doesn't perform arithmetic on the CPU for this very reason. The calculators priority is accuracy, the CPUs priority is speed.