ishaybas has asked for the wisdom of the Perl Monks concerning the following question:

Hello, this seems very strange.
for ($x=0.8; $x > 0.1; $x -= 0.01) { print "$x\n"; }
result (look at the end):
0.8
0.79
0.78
0.77
0.76
0.75
0.74
0.73
0.72
0.71
0.7
0.69
0.68
0.67
0.66
0.65
0.64
0.63
0.62
0.61
0.6
0.59
0.58
0.57
0.56
0.55
0.54
0.53
0.52
0.51
0.5
0.49
0.48
0.47
0.46
0.45
0.44
0.43
0.42
0.41
0.4
0.39
0.38
0.37
0.36
0.35
0.34
0.33
0.32
0.31
0.3
0.29
0.28
0.27
0.26
0.25
0.24
0.23
0.22
0.21
0.2
0.19
0.179999999999999
0.169999999999999
0.159999999999999
0.149999999999999
0.139999999999999
0.129999999999999
0.119999999999999
0.109999999999999
can someone explain this please..?

This is perl 5, version 30, subversion 0 (v5.30.0) built for x86_64-linux-gnu-thread-multi
Ubuntu 20.04

Replies are listed 'Best First'.
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 Floating-Point Arithmetic.

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

    Just to pound this tent peg completely into the ground:

    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:  <%-{-{-{-<

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.


    🦛

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.

      ... 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 "xxe-2") formatting: for ($x = Math::Decimal64->new('0.8'); $x > '0.1'; $x -= '0.01') { pri +nt "$x" + 0, "\n"; }
      Cheers,
      Rob
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/

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 floating-point 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 Floating-Point 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

Re: what did I just see..?
by BillKSmith (Monsignor) on Mar 21, 2021 at 21:58 UTC
    In practice, floating-point 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.
    Bill
      > In practice, floating-point 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.

        I got a threatening letter from American Express for my Amazon.com corporate card—which I hadn’t used in a full year—telling me to pay up on my outstanding balance or else! The amount I owed: $0.00. :P

        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.
        Bill
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"; }
      Sorry, sectokia, but I don't think that adding twice the machine_epsilon is the cure-all 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.00000000000000006e-01) or 0.01 (1.00000000000000002e-02), but for a step of 0.03 (2.99999999999999989e-02), the step size is smaller, so now your adjustment takes the string in the wrong direction. (Not shown in the spoiler code.)

        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.

      > 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

        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.