Perl整数运算返回浮点数答案的问题

4
下面的代码模拟实际的生产代码。由于数据来自使用XML:Twig解析的XML文件,因此使用双引号表示实际数据:
#!/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;会使它正常工作。但我无法轻易地将其应用于生产代码。所以,我想知道...

  1. 为什么会出现上述情况。
  2. 缺少哪种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

2
启示:perl -e '$x=1;printf"%.20f\n",8458.8*($x*=10) for 1..8' - mob
perl -e '$x=1;printf"%.20f\n",8458.8*($x*=10) for 1..8'84588.00000000000000000000 845880.00000000000000000000 8458800.00000000000000000000 84588000.00000000000000000000 845880000.00000000000000000000 8458800000.00000000000000000000 84588000000.00000000000000000000 845880000000.00000000000000000000 - ftumsh
84588.00000000000000000000 845879.99999999988358467817 8458800.00000000000000000000 84588000.00000000000000000000 845879999.99999988079071044922 8458799999.99999904632568359375 84588000000.00000000000000000000 845879999999.99987792968750000000 - ftumsh
1
这是你必须发表的“Perl已经十岁了”的评论。 :) - Schwern
3个回答

6

这不是整数算术。两个方程都包含浮点数。

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;

第一篇遭受了浮点误差的影响,而第二篇则没有。

以上代码复制了生产代码中发生的情况。引号用于近似表示来自 XML 解析的值。 - ftumsh
B::Deparse很有趣。谢谢。 - ftumsh

4
简单来说,浮点数是以二进制形式存储的。每个数字内部都存储了一定数量(通常为53位)的尾数(有效数字),以及一个指数,显示应将该尾数乘以2的几次幂。
大多数十进制数字在这种格式下无法被精确表示。例如,8458.8可能被表示为0b10000100001010110011001100110011001100110011001100110 * 2 ** -39。这是一个比8458.8略小的数字,但是它是可表示的最接近的数字。因为它比原数小,如果你把它乘以100再取整,你会得到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";

或者,你可以在计算后四舍五入。
建议使用uselongdouble编译perl(如果您的机器支持),将使用64位精度而不是53位。这将影响某些数字是否表示为比确切值更大或更小的数字,但仍然会有一些数字朝每个方向走。

谢谢。非常准确! - ftumsh

1
如果你能够从源代码中构建perl,那么传递给配置脚本的参数是 -Duselongdouble
$ ./Configure -des -Duselongdouble
...
$ make
...
$ ./perl -Ilib -V:[in]vsize
ivsize='8';
nvsize='16';

那不会解决问题。这可能会改变以.999...999...结尾的数字,并将其转换为int类型“不正确”。 - ysth
不是,但它确实“修复”了这个“问题”。 - mob
我已经在问题中添加了更多信息。@ysth,当您说它改变数字时,您是否意味着它仍然会发生,但是发生在更大的值上?如果是这样,在perl“有效”的情况下,这些值可能是多少? - ftumsh
Perl是Redhat的默认安装。看来我得建立一个新的服务器了,真是烦死了。 - ftumsh
谢谢大家的时间。 - ftumsh
我添加了我认为更好的答案。 - ysth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接