12

众所周知,(大多数)浮点数没有精确存储(当使用 IEEE-754 格式时)。所以不应该这样做:

0.3 - 0.2 === 0.1; // very wrong

...因为它将导致false,除非使用了某些特定的任意精度类型/类(Java / Ruby中的 BigDecimal ,PHP 中的BCMath ,Perl 中的Math::BigInt / Math::BigFloat等等)。

然而我想知道为什么当一个人试图打印这个表达式的结果时0.3 - 0.2,脚本语言(PerlPHP)给出0.1了,但是“虚拟机”语言(JavaJavaScriptErlang)给出了更相似的东西0.09999999999999998呢?

为什么它在 Ruby 中也不一致?1.8.6 版(键盘)给出0.11.9.3 版(ideone)给出0.0999...

4

5 回答 5

7

至于 php,输出与精度的 ini 设置有关:

ini_set('precision', 15);
print 0.3 - 0.2; // 0.1

ini_set('precision', 17);
print 0.3 - 0.2; //0.099999999999999978 

这也可能是其他语言的原因

于 2012-12-29T14:16:40.720 回答
4

浮点数的打印方式不同,因为打印是为了不同的目的而进行的,因此对于如何进行打印做出了不同的选择。

打印浮点数是一种转换操作:以内部格式编码的值被转换为十进制数字。但是,对于转换的细节有一些选择。

(A)如果你在做精确的数学运算,想看到内部格式表示的实际值,那么转换必须是精确的:它必须产生一个与输入值完全相同的十进制数字。(每个浮点数只代表一个数字。在 IEEE 754 标准中定义的浮点数不代表一个区间。)有时,这可能需要产生非常多的数字。

(B)如果您不需要确切的值,但确实需要在内部格式和十进制之间来回转换,那么您需要将其精确地(并且准确地)转换为十进制数字,以将其与任何其他结果区分开来。也就是说,您必须生成足够多的数字,以使结果与通过转换内部格式中相邻的数字所获得的结果不同。这可能需要产生大量的数字,但不能多到难以管理。

(C)如果您只想让读者了解数字,而不需要生成确切的值以使您的应用程序按需要运行,那么您只需要生成您的应用程序所需的尽可能多的数字特定的应用。

其中哪些应该转换?

不同的语言有不同的默认值,因为它们是为不同的目的而开发的,或者因为在开发过程中做所有必要的工作以产生准确的结果并不方便,或者出于各种其他原因。

(A) 需要仔细的代码,并且某些语言或它们的实现不提供或不保证提供这种行为。

我相信(B)是Java所要求的。但是,正如我们在最近的一个问题中看到的那样,它可能会有一些意想不到的行为。(65.12打印为“65.12”,因为后者有足够的位数将其与附近的值区分开来,但65.12-2打印为“63.120000000000005”,因为它和 63.12 之间还有另一个浮点值,因此您需要额外的数字来区分它们。 )

(C) 是某些语言默认使用的。从本质上讲,这是错误的,因为对于要打印的位数,没有一个值可以适用于所有应用程序。事实上,几十年来我们已经看到它助长了对浮点的持续误解,主要是通过隐藏所涉及的真实值。然而,它很容易实现,因此对一些实现者很有吸引力。理想情况下,语言应该默认打印浮点数的正确值。如果要显示的位数较少,则应仅由应用程序实施者选择位数,希望包括考虑适当的位数以产生所需的结果。

Worse, some languages, in addition to not displaying the actual value or enough digits to distinguish it, do not even guarantee that the digits produced are correct in some sense (such as being the value you would get by rounding the exact value to the number of digits shown). When programming in an implementation that does not provide a guarantee about this behavior, you are not doing engineering.

于 2012-12-29T17:21:52.643 回答
2

PHP 自动将数字四舍五入到任意精度。

浮点数通常不准确(正如您所指出的),round()如果您只需要比较几位小数,则应该使用特定于语言的函数。否则,取方程的绝对值,并测试它们是否在给定范围内。

来自php.net的 PHP 示例:

$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;
if(abs($a - $b) < $epsilon) {
  echo "true";
}

至于 Ruby 问题,他们似乎使用了不同的版本。Codepad使用1.8.6,而 Ideaone 使用1.9.3,但它更可能与某处的配置有关。

于 2012-12-29T14:17:21.953 回答
2

如果我们想要这个属性

  • 每两个不同的浮点数都有不同的打印表示

或者更强大的对 REPL 有用

  • 印刷表示应重新解释不变

然后我看到了 3 个解决方案,用于将具有基数 2 内部表示的浮点数/双精度数打印到基数 10

  1. 打印确切的表示。
  2. 打印足够的十进制数字(适当的四舍五入)
  3. 打印可以重新解释不变的最短十进制表示

由于以 2 为底,浮点数为 an_integer * 2^an_exponent,其以 10 为底的精确表示具有有限位数。
不幸的是,这可能会导致很长的字符串...例如 1.0e-10 完全表示为 1.0000000000000000364321973154977415791655470655996396089904010295867919921875e-10

解决方案 2 很简单,您使用 IEEE-754 双倍的 17 位 printf...
缺点:它不准确,也不是最短的!如果输入 0.1,则得到 0.100000000000000006

解决方案 3 是 REPL 语言的最佳解决方案,如果您输入 0.1,它会打印 0.1
不幸的是,它在标准库中找不到(很遗憾)。
至少,Scheme、Python 和最近的 Squeak/Pharo Smalltalk 做得对,我认为 Java 也是。

于 2012-12-29T15:27:40.413 回答
0

至于 Javascript,base2 在内部用于计算。

> 0.2 + 0.4
0.6000000000000001

为此,如果生成的 base2 数字不是周期性的,Javascript 只能传递偶数。

0.60.10011 10011 10011 10011 ...在 base2(周期性)中,而0.5没有因此正确打印。

于 2012-12-29T15:23:31.573 回答