Search code examples
perlfloating-pointintegerinteger-arithmetic

perl integer arithmetic giving floating point answer


The below code mimics actual production code. double quotes are used as the actual data comes from an XML file, parsed using XML:Twig:

#!/usr/bin/perl

use strict;
use warnings;
use diagnostics;
use Devel::Peek;

my $linetotalinclusive = "8458.80" * 1_000_000;

$linetotalinclusive = $linetotalinclusive;

my $c = "7980.00" * 1_000_000;

my $data = $linetotalinclusive - $c;

print Dump $c;

print Dump $linetotalinclusive;

print "$linetotalinclusive - $c = $data \n";

Gives the following result:

SV = PVNV(0x22885f0) at 0x21984f8
  REFCNT = 1
  FLAGS = (PADMY,IOK,NOK,pIOK,pNOK)
  IV = 7980000000
  NV = 7980000000
  PV = 0
SV = PVNV(0x2288650) at 0x21984c8
  REFCNT = 1
  FLAGS = (PADMY,NOK,pIOK,pNOK)
  IV = 8458799999
  NV = 8458800000
  PV = 0

8458800000 - 7980000000 = 478799999.999999

When run on my laptop and on our production server. (the above is from my laptop) However, when I run it on another production machine, it works fine. use integer; on the above code makes it work. But I am unable to do that to production code (easily). So, I was would like to know...

  1. Why is the above happening.
  2. What compile option is missing from the perl interpreter that would fix it.

Further info: This is from the broken machine:

This is perl 5, version 18, subversion 1 (v5.18.1) built for x86_64-linux-thread-multi
perl -MPOSIX -le 'print LONG_MAX'
9223372036854775807

perl -V:[in]vsize
ivsize='8';
nvsize='8';

This is from the machine that works:

This is perl, v5.8.9 built for x86_64-linux-ld

perl -MPOSIX -le 'print LONG_MAX'
9223372036854775807

perl -V:[in]vsize
ivsize='8';
nvsize='16';

This gives the expected answer:

Summary of my perl5 (revision 5 version 8 subversion 9) configuration:
  Platform:
    osname=linux, osvers=2.6.32-431.3.1.el6.x86_64, archname=x86_64-linux-ld
    uname='linux 553291-amon-sul2.firstb2b.net 2.6.32-431.3.1.el6.x86_64 #1 smp sat jan 4 02:04:49 est 2014 x86_64 x86_64 x86_64 gnulinux '
    config_args=''
    hint=recommended, useposix=true, d_sigaction=define
    usethreads=undef use5005threads=undef useithreads=undef usemultiplicity=undef
    useperlio=define d_sfio=undef uselargefiles=define usesocks=undef
    use64bitint=define use64bitall=define uselongdouble=define
    usemymalloc=n, bincompat5005=undef
  Compiler:
    cc='cc', ccflags ='-fno-strict-aliasing -pipe -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -I/usr/include/gdbm',
    optimize='-O2',
    cppflags='-fno-strict-aliasing -pipe -I/usr/local/include -I/usr/include/gdbm'
    ccversion='', gccversion='4.4.7 20120313 (Red Hat 4.4.7-4)', gccosandvers=''
    intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=12345678
    d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
    ivtype='long', ivsize=8, nvtype='long double', nvsize=16, Off_t='off_t', lseeksize=8
    alignbytes=16, prototype=define
  Linker and Libraries:
    ld='cc', ldflags =' -L/usr/local/lib'
    libpth=/usr/local/lib /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib64
    libs=-lnsl -lgdbm -ldb -ldl -lm -lcrypt -lutil -lc
    perllibs=-lnsl -ldl -lm -lcrypt -lutil -lc
    libc=, so=so, useshrplib=false, libperl=libperl.a
    gnulibc_version='2.12'
  Dynamic Linking:
    dlsrc=dl_dlopen.xs, dlext=so, d_dlsymun=undef, ccdlflags='-Wl,-E'
    cccdlflags='-fPIC', lddlflags='-shared -O2 -L/usr/local/lib'


Characteristics of this binary (from libperl): 
  Compile-time options: PERL_MALLOC_WRAP USE_64_BIT_ALL USE_64_BIT_INT
                        USE_FAST_STDIO USE_LARGE_FILES USE_LONG_DOUBLE
                        USE_PERLIO

This one doesn't:

Summary of my perl5 (revision 5 version 8 subversion 8) configuration:
  Platform:
    osname=linux, osvers=2.6.18-194.26.1.el5, archname=x86_64-linux-thread-multi
    uname='linux x86-002.build.bos.redhat.com 2.6.18-194.26.1.el5 #1 smp fri oct 29 14:21:16 edt 2010 x86_64 x86_64 x86_64 gnulinux '
    config_args='-des -Doptimize=-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -Dversion=5.8.8 -Dmyhostname=localhost -Dperladmin=root@localhost -Dcc=gcc -Dcf_by=Red Hat, Inc. -Dinstallprefix=/usr -Dprefix=/usr -Dlibpth=/usr/local/lib64 /lib64 /usr/lib64 -Dprivlib=/usr/lib/perl5/5.8.8 -Dsitelib=/usr/lib/perl5/site_perl/5.8.8 -Dvendorlib=/usr/lib/perl5/vendor_perl/5.8.8 -Darchlib=/usr/lib64/perl5/5.8.8/x86_64-linux-thread-multi -Dsitearch=/usr/lib64/perl5/site_perl/5.8.8/x86_64-linux-thread-multi -Dvendorarch=/usr/lib64/perl5/vendor_perl/5.8.8/x86_64-linux-thread-multi -Darchname=x86_64-linux-thread-multi -Dvendorprefix=/usr -Dsiteprefix=/usr -Duseshrplib -Dusethreads -Duseithreads -Duselargefiles -Dd_dosuid -Dd_semctl_semun -Di_db -Ui_ndbm -Di_gdbm -Di_shadow -Di_syslog -Dman3ext=3pm -Duseperlio -Dinstallusrbinperl=n -Ubincompat5005 -Uversiononly -Dpager=/usr/bin/less -isr -Dd_gethostent_r_proto -Ud_endhostent_r_proto -Ud_sethostent_r_proto -Ud_endprotoent_r_proto -Ud_setprotoent_r_proto -Ud_endservent_r_proto -Ud_setservent_r_proto -Dinc_version_list=5.8.7 5.8.6 5.8.5 -Dscriptdir=/usr/bin'
    hint=recommended, useposix=true, d_sigaction=define
    usethreads=define use5005threads=undef useithreads=define usemultiplicity=define
    useperlio=define d_sfio=undef uselargefiles=define usesocks=undef
    use64bitint=define use64bitall=define uselongdouble=undef
    usemymalloc=n, bincompat5005=undef
  Compiler:
    cc='gcc', ccflags ='-D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -Wdeclaration-after-statement -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -I/usr/include/gdbm',
    optimize='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic',
    cppflags='-D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -Wdeclaration-after-statement -I/usr/local/include -I/usr/include/gdbm'
    ccversion='', gccversion='4.1.2 20080704 (Red Hat 4.1.2-50)', gccosandvers=''
    intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=12345678
    d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
    ivtype='long', ivsize=8, nvtype='double', nvsize=8, Off_t='off_t', lseeksize=8
    alignbytes=8, prototype=define
  Linker and Libraries:
    ld='gcc', ldflags =''
    libpth=/usr/local/lib64 /lib64 /usr/lib64
    libs=-lresolv -lnsl -lgdbm -ldb -ldl -lm -lcrypt -lutil -lpthread -lc
    perllibs=-lresolv -lnsl -ldl -lm -lcrypt -lutil -lpthread -lc
    libc=, so=so, useshrplib=true, libperl=libperl.so
    gnulibc_version='2.5'
  Dynamic Linking:
    dlsrc=dl_dlopen.xs, dlext=so, d_dlsymun=undef, ccdlflags='-Wl,-E -Wl,-rpath,/usr/lib64/perl5/5.8.8/x86_64-linux-thread-multi/CORE'
    cccdlflags='-fPIC', lddlflags='-shared -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic'


Characteristics of this binary (from libperl): 
  Compile-time options: MULTIPLICITY PERL_IMPLICIT_CONTEXT
                        PERL_MALLOC_WRAP USE_64_BIT_ALL USE_64_BIT_INT
                        USE_ITHREADS USE_LARGE_FILES USE_PERLIO
                        USE_REENTRANT_API

Solution

  • Simplifying a little, floating point numbers are stored in binary; that is, each number internally is stored as a certain number of bits of mantissa (significant digits) (commonly 53) and an exponent that shows what power of 2 to multiply that mantissa by.

    Most decimal numbers are not exactly representable in this format. For instance, 8458.8 might be represented as 0b10000100001010110011001100110011001100110011001100110 * 2**-39. This is a number that is slightly smaller than 8458.8, but is the closest possible representable number. Because it is smaller, if you multiply it by 100 and then int it, you will get 845879, not 845880.

    You multiplying your input by a large power of ten, which, if numbers were exactly stored, would give an integer. Because you know it should be an integer, you should round it at that point; then numbers represented as either slightly smaller or slightly larger than the exact number will come out correct:

    use strict;
    use warnings;
    
    my $linetotalinclusive = int( "8458.80" * 1_000_000 + .5 );
    
    $linetotalinclusive = $linetotalinclusive;
    
    my $c = int( "7980.00" * 1_000_000 + .5 );
    
    my $data = $linetotalinclusive - $c;
    
    print "$linetotalinclusive - $c = $data \n";
    

    Or alternatively, round after your computations.

    The suggestion to compile perl with uselongdouble will (if your machine supports it) use 64 bits of precision instead of 53. This will affect whether particular numbers are represented as larger or smaller than the exact value, but there will still be some numbers that go each way.