如何消除Perl的四舍五入误差

3

请考虑以下程序:

$x=12345678901.234567000;
$y=($x-int($x))*1000000000;
printf("%f:%f\n",$x,$y);

以下是打印的内容:

12345678901.234568:234567642.211914

我期望的是:

12345678901.234567:234567000

这似乎是Perl中的某种舍入问题。
我该如何更改才能得到234567000
我做错了什么吗?

7个回答

6

这是一个经常被问到的问题。

Why am I getting long decimals (eg, 19.9499999999999) instead of the numbers I should be getting (eg, 19.95)?

Internally, your computer represents floating-point numbers in binary. Digital (as in powers of two) computers cannot store all numbers exactly. Some real numbers lose precision in the process. This is a problem with how computers store numbers and affects all computer languages, not just Perl.

perlnumber shows the gory details of number representations and conversions. To limit the number of decimal places in your numbers, you can use the printf or sprintf function. See the Floating Point Arithmetic for more details.

printf "%.2f", 10/3;
my $number = sprintf "%.2f", 10/3;

5

将"use bignum;"作为您程序的第一行。

其他答案解释了使用浮点算术时可以预期的结果——一些数字在末尾并不是真正的答案。这是为了使计算在合理的时间和空间内完成。如果您愿意花费无限的时间和空间来处理数字,那么您可以使用任意精度的数字和数学,这就是"use bignum"所实现的。它速度较慢,占用更多内存,但它的工作方式类似于您在小学学到的数学。

通常,在将程序转换为任意精度数学之前,最好了解更多关于浮点数学的工作原理。只有在非常奇怪的情况下才需要使用它。


4
“只有在非常奇怪的情况下才需要”这句话是指钱是一种非常奇怪的情况吗? - Evan Plaice
4
@Evan Plaice:钱并不奇怪;它通常是以分为单位的整数。如果您在使用小数点值作为货币单位,那么您可能做错了。 - Greg Hewgill
4
不需要,钱的计算使用整数,因为钱是"固定精度"。 - hobbs
4
@Greg,@hobbs - Evan完全正确。我猜你们俩都不在金融界工作吧?与金钱有关的各种事物都是分数的——从应计利息到费用、优惠券再到外汇兑换等等。你在银行对账单上看到的美好的整分舍入只是一种(不太得体的)虚构。 - DVK
1
@Greg,@hobbs - ……这就是为什么你永远不应该将钱存储为整数(例如,以美分为单位的数量),而应该始终以浮点数存储。此外,如果您想要全面了解有关小数货币和编程的技术参考,请去租赁《超人》或《办公室空间》 :) - DVK
显示剩余10条评论

3
整个浮点精度问题已经得到解决,但是尽管使用了 bignum,您仍然看到了问题。为什么?罪魁祸首是 printf。bignum 只是一个浅层的 pragma。它只影响变量和数学运算中数字的表示方式。即使 bignum 让 Perl 做正确的数学计算,printf 仍然是用 C 实现的。%f 会将您精确的数字转换回不精确的浮点数。
使用 print 输出您的数字,它们应该很好地工作。您将需要手动格式化它们。
另一件事是重新编译 Perl,使用 -Duse64bitint -Duselongdouble 强制 Perl 内部使用 64 位整数和 long double 浮点数。这将给您更多的准确性,更一致,并且几乎没有性能损失(对于数学密集型代码,bignum 有点效率低下)。它不像 bignum 那样 100% 准确,但它会影响 printf 等内容。但是,以这种方式重新编译 Perl 会使其与二进制不兼容,因此您需要重新编译所有扩展。如果您这样做,建议在不同位置安装新的 Perl(例如 /usr/local/perl/64bit),而不是尝试管理共享同一库的并行 Perl 安装。

1
printf "%s"也可以通过强制大数转换为字符串来完成任务。虽然在这里没有特别的需要使用printf,但了解这一点也是值得的。 - hobbs

2

你的作业(Google作业?):计算机如何表示浮点数?

计算机只能精确表示有限数量的数字,超出这个范围的部分只是从基数转换(二进制到十进制)中产生的噪声。这也是为什么你的$x的最后一位看起来是8

$x - (int($x)0.23456linenoise,它也是一个浮点数。乘以1000000000,就会得到另一个浮点数,其中包含更多从基数不可约的性质中提取出的随机数字。


1

Perl不支持其内置浮点类型的任意精度算术运算。因此,您的初始变量$x是一个近似值。您可以通过执行以下操作来查看:

$ perl -e 'printf "%.10f", 12345678901.234567000'
12345678901.2345676422

1
Perl默认情况下不执行任意精度算术运算。 - ysth
如果默认未开启,我该如何打开它? - User1

0

这个答案在我的x64平台上有效,通过“适应”错误的规模

sub safe_eq {
  my($var1,$var2)=@_;
  return 1 if($var1==$var2);
  my $dust;
  if($var2==0) { $dust=abs($var1); }
  else { $dust= abs(($var1/$var2)-1); }
  return 0 if($dust>5.32907051820076e-15 ); # 5.32907051820075e-15
  return 1;
}

你可以在上述基础上解决大部分问题。

如果可能的话,避免使用bignum - 它非常慢 - 而且如果你需要将数字存储在数据库或JSON等地方,它也无法解决任何问题。


0

这与计算机执行的(有限的)浮点数计算的准确性有关。一般来说,当比较浮点数时,应该使用适当的epsilon进行比较:

$value1 == $value2 or warn;

在大多数情况下,这不会按预期工作。你应该这样做

use constant EPSILON => 1.0e-10;
abs($value1 - $value2) < EPSILON or warn;

应该选择 EPSILON,以便考虑到 valueX 的计算复杂度。大量的计算可能会导致更大的 EPSILON。

另一个选项是,正如其他人建议的那样:

sprintf("%.5f", value1) eq sprintf("%.5f", value2) or warn;

或者使用任意精度数学库。


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