Целочисленная арифметика perl, дающая ответ с плавающей запятой
Приведенный ниже код имитирует фактический производственный код. двойные кавычки используются, так как фактические данные поступают из файла XML, анализируется с использованием 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";
Дает следующий результат:
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
При запуске на моем ноутбуке и на нашем производственном сервере. (выше это с моего ноутбука) Однако, когда я запускаю его на другом компьютере, он работает нормально.use integer;
на приведенном выше коде заставляет его работать. Но я не могу сделать это с производственным кодом (легко). Итак, мне хотелось бы узнать...
- Почему вышесказанное происходит.
- Какая опция компиляции отсутствует в интерпретаторе perl, которая исправит это.
Дополнительная информация: Это со сломанной машины:
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 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';
Это дает ожидаемый ответ:
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
Этот не:
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
3 ответа
Упрощенно, числа с плавающей точкой хранятся в двоичном виде; то есть каждое число внутренне сохраняется как определенное количество бит мантиссы (значащих цифр) (обычно 53) и показатель степени, который показывает, на какую степень 2 умножить эту мантиссу.
Большинство десятичных чисел не могут быть точно представлены в этом формате. Например, 8458.8 можно представить как 0b10000100001010110011001100110011001100110011001100110 * 2**-39. Это число, которое немного меньше 8458,8, но является наиболее близким представимым числом. Поскольку он меньше, если вы умножите его на 100, а затем на int, вы получите 845879, а не 845880.
Вы умножаете свой ввод на большую степень десяти, которая, если бы числа были точно сохранены, дала бы целое число. Поскольку вы знаете, что это должно быть целое число, вы должны округлить его в этой точке; тогда числа, представленные либо немного меньшими, либо чуть большими, чем точное число, получатся правильными:
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";
Или, альтернативно, круглая после ваших вычислений.
Предложение компилировать Perl с помощью uselongdouble будет (если ваша машина поддерживает это) использовать 64-битную точность вместо 53. Это будет влиять на то, будут ли конкретные числа представлены как большие или меньшие, чем точное значение, но все равно будут некоторые числа, которые идут каждый путь.
Это не целочисленная арифметика. Оба уравнения содержат числа с плавающей точкой.
my $linetotalinclusive = "8458.80" * 1_000_000;
^^^^^^^^^
my $c = "7980.00" * 1_000_000;
^^^^^^^^^
Иногда Perl становится умным и замечает, что число с плавающей запятой может быть сохранено как целое число, но, похоже, возникают проблемы при преобразовании строки.
Dump 7980.00 + 1_000_000_000_000;
Dump "7980.00" + 1_000_000_000_000;
SV = IV(0x7fd55401c8e0) at 0x7fd55401c8f0
REFCNT = 1
FLAGS = (PADTMP,IOK,READONLY,pIOK)
IV = 1000000007980
SV = NV(0x7fd553831200) at 0x7fd553844990
REFCNT = 1
FLAGS = (PADTMP,NOK,READONLY,pNOK)
NV = 1000000007980
Происходят другие вещи: постоянное сворачивание. Если выражение не содержит ничего, кроме констант, Perl часто выполняет математику во время компиляции. Если вы запустите свой код через B:: Deparse, который восстанавливает код из скомпилированных кодов операций, вы увидите, что ваши уравнения были превращены в константы.
my $linetotalinclusive = 8458799999.9999990463;
my $c = 7980000000;
Первый пострадал от ошибки с плавающей запятой. Второго нет.
Если вы можете собрать свой Perl из исходного кода, параметр для передачи в скрипт конфигурации: -Duselongdouble
$ ./Configure -des -Duselongdouble
...
$ make
...
$ ./perl -Ilib -V:[in]vsize
ivsize='8';
nvsize='16';