Re: what did I just see..?
by haj (Curate) on Mar 21, 2021 at 17:16 UTC

Welcome to the world of floating point arithmetic!
Short version: 0.01 can not be represented exactly as a floating point number, there's a tiny error here. Since you always subtract the same number, this error accumulates to the point where the difference shows up.
Long version: What Every Programmer Should Know About FloatingPoint Arithmetic.
 [reply] 

 [reply] 
Re: what did I just see..?
by AnomalousMonk (Bishop) on Mar 21, 2021 at 17:30 UTC

Win8 Strawberry 5.30.3.1 (64) Sun 03/21/2021 13:20:05
C:\@Work\Perl\monks
>perl
printf "decrement: %0.20f \n", 0.01;
for ($x=0.8; $x > 0.1; $x = 0.01) { printf "%0.20f \n", $x; }
^Z
decrement: 0.01000000000000000021
0.80000000000000004441
0.79000000000000003553
0.78000000000000002665
0.77000000000000001776
0.76000000000000000888
0.75000000000000000000
0.73999999999999999112
0.72999999999999998224
0.71999999999999997335
0.70999999999999996447
0.69999999999999995559
0.68999999999999994671
0.67999999999999993783
0.66999999999999992895
0.65999999999999992006
0.64999999999999991118
0.63999999999999990230
0.62999999999999989342
0.61999999999999988454
0.60999999999999987566
0.59999999999999986677
0.58999999999999985789
0.57999999999999984901
0.56999999999999984013
0.55999999999999983125
0.54999999999999982236
0.53999999999999981348
0.52999999999999980460
0.51999999999999979572
0.50999999999999978684
0.49999999999999977796
0.48999999999999976907
0.47999999999999976019
0.46999999999999975131
0.45999999999999974243
0.44999999999999973355
0.43999999999999972466
0.42999999999999971578
0.41999999999999970690
0.40999999999999969802
0.39999999999999968914
0.38999999999999968026
0.37999999999999967137
0.36999999999999966249
0.35999999999999965361
0.34999999999999964473
0.33999999999999963585
0.32999999999999962697
0.31999999999999961808
0.30999999999999960920
0.29999999999999960032
0.28999999999999959144
0.27999999999999958256
0.26999999999999957367
0.25999999999999956479
0.24999999999999955591
0.23999999999999954703
0.22999999999999953815
0.21999999999999952927
0.20999999999999952038
0.19999999999999951150
0.18999999999999950262
0.17999999999999949374
0.16999999999999948486
0.15999999999999947597
0.14999999999999946709
0.13999999999999945821
0.12999999999999944933
0.11999999999999945433
0.10999999999999945932
Give a man a fish: <%{{{<
 [reply] [d/l] [select] 
Re: what did I just see..?
by hippo (Bishop) on Mar 21, 2021 at 18:38 UTC

And for good measure since you haven't yet read it, here's the relevant FAQ.
 [reply] 

 [reply] 

 [reply] 
Re: what did I just see..?
by haukex (Bishop) on Mar 21, 2021 at 18:45 UTC

Since it hasn't been mentioned yet, note that at the cost of performance, you can get accurate calculations by adding a use bignum; to your code. Note that its scope is lexical, so you can limit the performance impact a bit by only applying it where needed in your code.
 [reply] [d/l] 

... you can get accurate calculations by adding a use bignum; to your code
I think this is the only solution that requires no modification to the original line of code:
for ($x=0.8; $x > 0.1; $x = 0.01) { print "$x\n"; }
It's probably worth pointing out to the OP that it works simply because bignum uses decimal (base 10) arithmetic.
There are other ways to force base 10 arithmetic  eg Math::Decimal, Math::Decimal64 and Math::Decimal128. (The last 2 are a plug, and will work far more quickly than bignum.)
However, they would all require some changes to the given line of code. For example:
use Math::Decimal64;
for ($x = Math::Decimal64>new('0.8'); $x > '0.1'; $x = '0.01') { pri
+nt "$x\n"; }
# or if one is insistent upon receiving 0.xx formatting of the values
+(instead of "xxe2") formatting:
for ($x = Math::Decimal64>new('0.8'); $x > '0.1'; $x = '0.01') { pri
+nt "$x" + 0, "\n"; }
Cheers, Rob  [reply] [d/l] [select] 
Re: what did I just see..?
by LanX (Sage) on Mar 21, 2021 at 17:14 UTC

Hello ishaybas, welcome to the monastery! :)
Standard rounding error phenomenon, you will find this in most languages with binary floats.
More math?
1/10 = 1/(2*5) and you can't represent the prime fraction 1/5 in a binary system without loss.
OTOH 1/2 is no problem, hence any fraction of form m/2**n with m,n in N . All others will have a rounding error.
I'll update links to older discussions...
edit
...like:
There are far are more older threads, sure our brethren will add them soon. :)
Cheers Rolf
_{(addicted to the Perl Programming Language :)
Wikisyntax for the Monastery
}
update
s/n/m/  [reply] [d/l] [select] 
Re: what did I just see..?
by ikegami (Pope) on Mar 22, 2021 at 08:05 UTC

1/10, 8/10 and 1/100 are all periodic numbers in binary just like 1/3 is a periodic number in decimal.
____
0.00011 # 1/10
____
0.1100 # 8/10
____________________
0.0000001010001111010111 # 1/100
As such, they can't be accurately represented as floatingpoint numbers.
$ perl e'printf "%.100g\n", $ARGV[0]' 0.1
0.1000000000000000055511151231257827021181583404541015625
$ perl e'printf "%.100g\n", $ARGV[0]' 0.8
0.8000000000000000444089209850062616169452667236328125
$ perl e'printf "%.100g\n", $ARGV[0]' 0.01
0.01000000000000000020816681711721685132943093776702880859375
See What Every Computer Scientist Should Know About FloatingPoint Arithmetic.
Solution:
for (0..69) (
my $x = ( 80  $_ ) / 100;
say $x;
}
This will prevent error from accumulating, keeping it under the default rounding. You could also perform more forgiving rounding than the default. Or you could avoid floating point numbers entirely (e.g. using rational number libraries).
Seeking work! You can reach me at ikegami@adaelis.com
 [reply] [d/l] [select] 
Re: what did I just see..?
by BillKSmith (Monsignor) on Mar 21, 2021 at 21:58 UTC

In practice, floatingpoint errors are seldom a problem. Look at your output carefully and see how small the error really is. The default print format can be annoying (as you have discovered). You can avoid this by using printf to round to your required precision.
 [reply] 

> In practice, floatingpoint errors are seldom a problem
Sorry, I must disagree.
I know of a case where fiscal authorities rejected a calculation because it was 1 cent off. Which led to a missed deadline and penalty payments, IIRC.
If you need cent accuracy then calculate in cents. Never floats!
Cheers Rolf
_{(addicted to the Perl Programming Language :)
Wikisyntax for the Monastery
}
PS: And if you do accounting, inform yourself about the required precision and rounding rules.
 [reply] 

 [reply] 




All your suggestions are true. My word 'seldom' probably does not apply to financial calculations. It may not be possible to exactly duplicate the calculations on the statement you receive from your financial institution. The printed decimal numbers on that statement are not exactly the same as the binary numbers in the bank's computer. It simply is not possible to recover every bit of those numbers. Any calculation that you do is flawed to start with. I have noticed that even Quicken's calculation of price/share never exactly agrees with my statement.
 [reply] 

Re: what did I just see..?
by sectokia (Scribe) on Mar 22, 2021 at 01:58 UTC

No one seems to have actually told you how to fix this. Basically if you are subtracting two floating point numbers (which you are) and then rounding (which is what print does  downward) then the upper bound of the error in the result will be twice the machines epsilon. So to fix this your code need to be this:
use Machine::Epsilon;
for (my $x=0.8; $x > 0.1; $x = 0.01) {
print "".($x+(2*machine_epsilon()))."\n";
}
 [reply] [d/l] 

Sorry, sectokia, but I don't think that adding twice the machine_epsilon is the cureall you really think it is.
Per the documentation, machine_epsilon is the maximum relative error ("Machine::Epsilon  The maximum relative error while rounding a floating point number"), but you are using it in an absolute fashion, which means that you are not properly scaling the error. The error possible is different for 1_000_000 vs 1 vs 0.000_001... it's even different for 2 vs 1 vs 1/2. So that won't help you "correct" the cumulative floating point errors. Since you are using values from 0.8 down to 0.1, you will be in the ranges [0.5,1.0), [0.25,0.50), [0.125,0.25), and [0.0625,0.125), which actually have four different ULP sizes.
Besides that, the cumulative errors will keep getting bigger, until your x+2*epsilon is no longer enough to increase it. Starting at 0.8 and decreasing by 0.01 each loop, by 70 iterations, the stringification of the "real" number and the stringification of your x+2*epsilon will not be matching each other.
The example in the spoiler shows both of those issues in more detail (where "diff" represents the real number, start  n*step, "x" represents the multiple individual subtractions, and "sectokia" represents the x+2epsilon).
Further, your x+2*epsilon assumes that your subtraction step size is slightly bigger than the real value; that is true for a step of 0.1 (1.00000000000000006e01) or 0.01 (1.00000000000000002e02), but for a step of 0.03 (2.99999999999999989e02), the step size is smaller, so now your adjustment takes the string in the wrong direction. (Not shown in the spoiler code.)
 [reply] [d/l] [select] 

For the range 0 to 1, then epsilon will always be greater than the error (as it would only scale smaller), but of course you are correct in that it should be scaled both up and down for a normalized solution.
You are also right about needing to apply it to each subtraction operation.
I don't agree with the bit around it being in the wrong direction if the step happens to be just under the desired ideal value. Print rounds down. If the float is +/ epsilon from ideal, then adding an epsiol brings it into range of 0 to +2 epsilon from ideal, which will round down to ideal. It doesn't matter if you started +ve or ve from ideal.
 [reply] 





> No one seems to have actually told you how to fix this.
I did, I said calculate in cents if you want that precision in the end
Even when using one single division to a float in the final output, it'll print correctly.
DB<3> for ($x=80; $x > 10; $x = 1) { say $x/100; }
0.8
0.79
0.78
0.77
0.76
0.75
0.74
0.73
0.72
0.71
0.7
... yadda
0.21
0.2
0.19
0.18
0.17
0.16
0.15
0.14
0.13
0.12
0.11
DB<4>
Cheers Rolf
_{(addicted to the Perl Programming Language :)
Wikisyntax for the Monastery
}
 [reply] [d/l] 

Thats not a solution, its sticking your head in the sand.
You'll end up coming unstuck at certain values. If are really are going to use cents, use it, and do integer division.
 [reply] 
