http://qs1969.pair.com?node_id=1112957

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

I suppose there is some small thing I am missing in the maze of computers representing floating point numbers, I just can't figure out what it is.
perl -e '$x = 0.0; $x += 20.1 for ( 1 .. 43 ); print $x."\n";'
The code above gives me 864.300000000001 as result on perl v5.14.2.
What is it I'm missing ?

Replies are listed 'Best First'.
Re: floating point addition
by BrowserUk (Patriarch) on Jan 12, 2015 at 16:19 UTC

    May be this will give you a clue:

    [0] Perl> printf "%20.17f\n", 20.1;; 20.10000000000000100

    Then read:Re: Re: Re: Bug? 1+1 != 2.


    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked
      Yeah, it never occurred to me to check that. Thanks.
Re: floating point addition
by choroba (Cardinal) on Jan 12, 2015 at 16:14 UTC
      I think bignum just gives you more precision but does not eliminate the problem.

      To eliminate the problem you could use bigrat and use fractions.

      Compare:

      perl -Mbignum -e ' printf "%20.17f\n", 20.1;' 20.10000000000000142 perl -Mbigrat -e ' printf "%20.17f\n", 20.1;' 20.10000000000000142 perl -Mbigrat -e ' printf "%20.17f\n", 20+1/10; 20.00000000000000000
        You are right about bignum. Your bigrat example, though, doesn't work. One needs
        perl -Mbigrat -le 'print 20/1 + 1/10'
        لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ
      That I've stumbled upon myself, and it seems like an interesting article, however I'm a bit reluctant to read 67 pages of that just to reach the same conclusion I already know, avoid computers touching floating point numbers ... A bit naive of me to think that 20.1 written like that means the same for a computer as it does to me.

        I think I detect a note of intended irony in that last sentence, but basically that's what you get when you try to do base 10 math in a base 2 machine. The sooner you become of aware of it, the sooner you can deal with it.

        This part probably sounds like one of those tirades from old farts that begin "back in my day.." .. but .. back in my day, one of the required courses was "Programming for Numerical Methods" or some such title, and we spent the class trying to predict how poorly our computations would proceed based on understanding the limits of the algorithms. There were two results to each computation, the result and the error estimate, and the second result was more important because it told you if could take the first one seriously or not. I took the class in the 80's and our textbook dated back to the 60's. This has become less important over time because improvements in computational speed and memory allow us to use floating point numbers of sufficiently high resolution that we avoid a lot of these problems .. but here we are in 2015 and 1/10th is still giving us fits.

        Dum Spiro Spero

        As that is an incorrect conclusion I suggest you do yourself a favour and invest your valuable time in reading and understanding those 67 pages.

        Jenda
        Enoch was right!
        Enjoy the last years of Rome.

Re: floating point addition
by davido (Cardinal) on Jan 12, 2015 at 16:55 UTC

    If the floating point number cannot be represented precisely as n/(2^m) then it must be represented imprecisely in a binary form, and when that happens, rounding errors occur. In base ten, the formula is k/((2^n)*(5^m)). We're just more accustomed to the mental leap that 0.33 in base ten usually means 1/3rd, even though this is an imprecise representation of a non-terminating expansion. When we see the same thing in numbers that have lost precision due to being stored in base-2, we panic. ;)

    A few years ago I put together a more thorough explanation: Re: shocking imprecision


    Dave

Re: floating point addition
by hippo (Bishop) on Jan 12, 2015 at 18:20 UTC
Re: floating point addition
by trippledubs (Deacon) on Jan 12, 2015 at 16:56 UTC

    Another exercise to represent fractions in binary is to take the decimal and multiply by two in a column.

    • .625 * 2 = 1.25 
    • .25  * 2 = 0.5 
    • .5   * 2 = 1.0 

    Now taking the whole number and going from bottom to top you get 101, the binary representation, exactly.

    If you take .1 you get

    • .1 * 2 = 0.2
    • .2 * 2 = 0.4
    • .4 * 2 = 0.8
    • .8 * 2 = 1.6
    • .6 * 2 = 1.2
    • .2 * 2 = 0.4

    And from that point you get a repeating sequence in binary similar to .333..3 in decimal

    And the takeaway is instead of testing for equality in floating point you test against an Epsilon (small quantity) and if the two numbers are within that Epsilon close to each other, they are for all intents are purposes, equal. Not doing so introduces bugs.

Re: floating point addition
by LanX (Saint) on Jan 12, 2015 at 18:16 UTC
      What is dual system?
        binary?
        لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ
        There are 10 kind of hackers!

        Those who understand binary and those who don't. :)

        Cheers Rolf

        PS: Je suis Charlie!

        PS and those who call it "dual".

Re: floating point addition
by sundialsvc4 (Abbot) on Jan 12, 2015 at 18:15 UTC

    This is also the reason why processors have a “decimal mode,” in which both digits and the sign of the number are represented using four-bit groups (so-called “binary-coded decimal = BCD.”).   The COBOL language is famous for exposing this feature, and for very obvious reasons:   when dollars-and-cents are involved, accountants (especially) care very much about “cents.”   The arithmetic will be performed in true decimal mode, just like they taught you to do it by-hand in grade school.   (Uhhh-h-h-h, back in my day, anyhow, when to use a pocket calculator was considered “cheating.”)

    Perl does have a Math::Decimal package, which probably exposes the functionality that you might need here.   (It says that it is “implemented mostly in XS,” but I have not taken the time to see whether it uses microprocessor decimal-math.)   A package like this one will not be able to represent 1/3 exactly, simply because “base-10 can’t do that,” but it will be able to represent 1/10 exactly.

    Incidentally, if this is a design consideration, you must also use DECIMAL types in your databases, and be certain that the entire soup-to-nuts handling of the numeric values does not ever stray into float-binary.   (You must also confirm that the underlying representation used by the database system in question is, in fact, decimal.)   Databases may also offer a CURRENCY type that, in most cases, is actually a scaled binary integer.   (Internally, the number is multiplied by 10,000 to give four fixed digits to the right of the decimal point.)   Once again, you must ensure that the entire data production-line never wanders into float-binary -land.

    Perl does have the capability to represent a value internally as a true integer, although the bit-width of that integer may vary by implementation.   Since it is a typeless language that tries its best to be accommodating, you must be mindful of exactly how Perl is representing and manipulating your values at all times ... if it matters.   (As it certainly does, say when you are dealing with the final, double-underlined total at the bottom of a big, long invoice ...)

      It says that it is “implemented mostly in XS,” but I have not taken the time to see whether it uses microprocessor decimal-math

      I don't think it does, but it certainly does decimal (as opposed to binary) arithmetic - and should be more than adequate for most people wanting to perform base 10 arithmetic.

      OTOH, my Math::Decimal64 and Math::Decimal128 modules do make use of the _Decimal64 and _Decimal128 types, and associated operations:
      C:\>perl -MMath::Decimal64 -le "$x=Math::Decimal64->new(0); $x += Math +::Decimal64->new('20.1') for (1..43);print $x;" 8643e-1
      The user interface is a little awkward, mainly because gcc does not provide strtod64 or strtod128 functions, and does not provide any (s)printf formatters for the _Decimal64 and _Decimal128 type.

      I'm working on improving that interface - and the current git version provides for output in floating point format instead of just scientific notation (ie as 864.3 instead of 8643e-1 for the quoted example).
      I must also add overloading of strings - so that the above one liner can be rewritten as:
      perl -MMath::Decimal64 -le "$x=Math::Decimal64->new(0); $x += '20.1' f +or (1..43);print $x;"
      But I don't want to add overloading of NVs as that would defeat the purpose.

      (That takes care of this month's quota of self-promotion ;-)

      Cheers,
      Rob
        But I don't want to add overloading of NVs as that would defeat the purpose.

        How would this defeat the purpose?