为什么浮点数的打印方式如此不同?

IT技术 php javascript ruby floating-point ieee-754
2021-03-16 21:28:04

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

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

......因为它会导致false,除非一些特定的任意精度的类型/类使用(BigDecimal的中的Java / Ruby的bcmath时在PHP中,数学:: BigInt有/数学:: BigFloat在Perl,仅举几例)来代替。

然而我想知道为什么当人们试图打印这个表达式的结果时0.3 - 0.2,脚本语言(PerlPHP)给出0.1,而“虚拟机”语言(JavaJavaScriptErlang)给出的结果更类似于0.09999999999999998

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

5个回答

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

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

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

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

我的天……转换为 FP 数字的字符串是由 INI 设置控制的吗?他们抽什么烟?
2021-04-15 21:28:04
让用户能够在一个地方进行操作而不是round / ceil / floor / number_format每次想要显示 FP 数字时都使用它有什么问题?
2021-04-18 21:28:04
局部问题的全局设置=至少无用,最坏的情况是灾难。如果是未知设置,则编写代码时会盲目假设默认值,并且只要有人更改它就会中断。相反,如果它在不同的安装中经常设置为不同的值,那么您的代码将不得不在需要不同的东西时处理这些设置(magic_quotes_gpc有人吗?);所以,它只会使代码复杂化。解决这个问题的正常方法是有一个固定的默认值(由语言规范保证)并提供一些方法来本地调整它。
2021-04-27 21:28:04
@Matteo Italia:有人可能会说整个 PHP 的事情是巧合而不是设计:)。尽管如此,如果您想要一致的结果,依赖于浮点数到字符串的任何隐式转换可能不是一个好主意它们在设计上是近似的。这就是 BigDecimal & Co. 首先存在的原因。
2021-05-11 21:28:04
@Hiroto:然后您的脚本使用的库将停止工作,因为它预计几乎未知的设置是默认设置。解决方案?在每次调用库之前,您必须将设置恢复为默认值 - 或者首先避免自定义它,因为它比收益更痛苦。同样,语言特性的全局状态几乎总是一个坏主意,我已经被这些东西咬了很多次了。
2021-05-12 21:28:04

浮点数的打印方式不同,因为打印的目的不同,因此对如何进行打印有不同的选择。

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

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

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

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

转换应该执行哪些操作?

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

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

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

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

更糟糕的是,有些语言除了不显示实际值或足够多的数字来区分它外,甚至不保证产生的数字在某种意义上是正确的(例如通过将精确值四舍五入到数字而得到的值显示的数字)。在不提供有关此行为的保证的实现中进行编程时,您不是在进行工程设计。

我会稍微更改案例 (C),或添加案例 (D),将最终结果交付给最终用户。输出应仅限于预期在应用程序上下文中正确和有用的数字。显然,这不能作为默认设置,因为它取决于输入的精度和计算的数值属性,以及数据的预期用途。
2021-04-16 21:28:04
@PatriciaShanahan:我愿意改变或分叉案例 (C),但我不清楚您想要做出的区分。最初的列表描述了这些情况,(C) 只产生了一些数字。我认为您可能正在讨论第二个列表,该列表讨论了案例的进一步属性或目的,并且可能会区分猜测要使用的大量数字的语言和计划使用大量数字的特定应用程序开发人员. 你能澄清一下吗?
2021-04-26 21:28:04
我主要考虑的是限制打印数字的目的。选择向最终用户提供结果的位数不仅仅是让读者了解数字的问题。它正在挑选将实际使用的数字,这些数字是整个计算的最终目的。我觉得对 C 的描述对于这种极端重要的东西来说似乎太随意了。
2021-05-01 21:28:04

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

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

来自php.net 的PHP 示例

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

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

如果我们想要这个属性

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

或者更强大的对 REPL 有用的

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

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

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

由于在基数 2 中,浮点数是 an_integer * 2^an_exponent,因此它的基数 10 精确表示具有有限位数。
不幸的是,这会导致很长的字符串……例如 1.0e-10 完全表示为 1.0000000000000000364321973154977415791655470655599639608999040102958678700000000000912

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

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

解决方案3是Java中float和double的默认字符串转换。
2021-04-21 21:28:04

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

> 0.2 + 0.4
0.6000000000000001

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

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