你怎么知道 JavaScript 浮点数何时不会被舍入



我知道二进制浮点的属性,计算机将无法将它们计算成四舍五入的数字。我想知道是否有任何"逻辑"来知道哪些浮标会被四舍五入,哪些不会?

例如,当我在控制台中运行 0.1 + 0.2 时,它会返回 0.30000000000000004. 然而,当我运行 0.1 + 0.3 时,它正确返回 0.4。

是否有任何逻辑可以确定哪些特定浮点数不会"正确"舍入?

paul23 的回答涉及一般原则。这个答案分析了问题中的具体案例。

对于表示十进制数的每个字符串,舍入到最接近将产生特定的 64 位二进制IEEE754数。以下是问题中数字的映射:

0.1 0.1000000000000000055511151231257827021181583404541015625
0.2 0.200000000000000011102230246251565404236316680908203125
0.3 0.299999999999999988897769753748434595763683319091796875
0.30000000000000004 0.3000000000000000444089209850062616169452667236328125
0.4 0.40000000000000002220446049250313080847263336181640625

转换为浮点数时,0.1 和 0.2 都向上舍入,因此它们的总和将大于 0.3。另一方面,0.3 向下舍入,因此总和大于最接近浮点数 0.3。任一方向的舍入误差为 2.77555756156289135105907917022705078125E-17,但舍入到偶数规则会导致向上舍入。

当添加 0.1 和 0.3 时,输入上的舍入误差方向相反。确切的金额是 0.39999999999999999944488848768742172978818416595458984375,正好是可表示数字之间的一半 0.399999999999999999966693309261245303787291049957275390625 和 0.4000000000000000002220446049250313080847263336181640625。无论哪种方式,舍入误差为 2.77555756156289135105907917022705078125E-17。

较大位模式的十六进制表示是 3fd999999999999a,它是偶数,所以这就是舍入的方式。碰巧的是,这也是最接近 0.4 的浮点数。

除非您将自己限制在可以用 64 位二进制浮点数精确表示的数字上进行算术,否则很难预测哪些计算将获得最接近预期十进制计算的浮点数,哪些不会。如果这很重要,则打印输出时小数位数过多,或者需要不同的数据类型。

浮点舍入基本上取决于数学。它是数论的一部分。

我将首先用十进制解释一下它,然后展示它是如何用二进制工作的:

像 0.12 这样的数字基本上是"零 + 1 乘以 1/10 + 2 乘以 1/10^2",即 12/100。这是一个所谓的"有理数",一个可以写成两个整数之间的比例的数字(1/10 = 0.12,1/4 = 0.25,1/2 = 0.5,都是有理数)。任何非有理数都不能写成十进制(或任何编号系统)的分数,无理数就像"pi"e"或 2 的平方根。

现在任何有理数都可以写成终止分数吗?

我们也知道十进制不是这种情况:1/3 不能,1/7 也不能。但有些人可以,事实证明这背后有逻辑: 分母的素因数与将在其中写入数的基数的素因数相同的任何有理数都可以写为有限浮点数。10的质因数是2和5。因此,任何素因数仅为 2 和 5 的有理数都可以写成以 10 为底的全数 - 换句话说,x/(2^p * 5^q)后面的任何数字(或这些数字的任何总和):

3/8 = 3/(2^3) = 0.375
1/80 = 1/(2^4 * 5^1) = 0.0125

但不是:

1/65 = 1/(5^1 * 13^1) = 0.0153846153846...

现在回到计算机上的浮点数:浮点单元以二进制工作,这是一个以 2 为基数的系统。该系统的质因数是简单的"2"。

因此,任何可以写成x/(2^a)的数字都可以写在浮点单元中而不会损失准确性,而任何不是这种形式的数字都不能在不损失准确性的情况下写入

然而,有一个警告:浮点单元的精度大小也有限,这进一步限制了数字的范围。IEEE 754-2008 注意到双精度数的最大精度"尾数"为 52 位,因为二进制数无论如何只有一个素因数,这限制了高于公式的a <= 52

哪些

数字将四舍五入或不四舍五入

有限数可以用常见的 IEEE-754 双精度格式表示,当且仅当它等于某些整数 M 和 e 的 M•2 e 使得 -2 53 <<em>M<253和 -1074 ≤e≤ 971。

从十进制转换或由其他运算产生的每个其他有限数将四舍五入。

(这是 JavaScript 使用的格式,因为它符合 ECMA-262,ECMA-262 表示使用 IEEE-754 64 位二进制浮点格式。上面的有效数M通常表示为 1 到 2 之间的值,在基数点之后有一定数量的位,但我将其缩放为整数以便于分析,并且调整指数边界以匹配。

问题中的所有数字都四舍五入

这意味着示例中的所有数字都将四舍五入:

  • 没有办法将 0.1 缩放 2 的幂来为M做一个整数。当我们重复将 0.1 乘以 2 时,我们得到 0.1、0.2、0.4、0.8、1.6、3.2、6.4,我们可以看到分数部分永远重复 .2、.4、.8、.6,...所以它永远不会达到 .0。由于 0.1 不能表示为M•2e,因此必须四舍五入。
  • 同样,0.2
  • 、0.3 和 0.4 也不能按 2 的任何幂缩放以构成M的整数。
  • 当这些数字 0.1、0.2、0.3 和 0.4 转换为 JavaScript 的Number格式时,结果为: 0.100000000000000000055511151231257827021181583404541015625
    • 0.20000000000000000011102230246251565404236316680908203125.
    • 0.299999999999999988897769753748434595763683319091796875.
    • 0.4000000000000000002220446049250313080847263336181640625.
  • 更正式地考虑数学,0.1 是 1/10。它永远不能等于M•2e,因为这样我们就会得到M•2 e = 1/10,所以 2•5•M•2e= 1。由于M 是整数,因此 2•5•M是整数,因此 2e必须抵消 5。但即使对于负e,2的幂也不能抵消 2 以外的质因数。

相比之下,数字 0.25 或 0.375 是可表示的。当我们将 0.25 乘以 2 时,我们得到 0.5,然后是 1,所以 0.25 = 1•2−2,这与上面的格式相匹配。0.375 产生 0.75、1.5 和 3,因此 0.375 = 3•2−3,这也与格式匹配。

为什么会出现一些数字没有四舍五入

两个令人困惑的问题造成了某些操作是精确的错觉

  1. JavaScript 对值的默认显示仅使用足够的十进制数字来唯一区分Number值。这来自 ECMAScript 2017 语言规范第 7.1.12.1 条中的步骤 5。 因此,例如,对于 0.10000000000000000055511151231257827021181583404541015625,JavaScript 将其
    • 显示为"0.1",因为这已经足够了 - 将"0.1"转换为浮点会产生相同的值,因此不需要更多数字。
    • 这将隐藏舍入,因为对于最多 15 位有效数字的任何十进制数字,将其转换为Number然后显示它会产生相同的数字。例如,我们在"0.12345"0.12345000000000000004174438572590588591992855072021484375"→0.12345→。默认格式规则使任何最多 15 位数字都是通过显示由该数字生成的Number值生成的数字。
  2. 有时在计算十进制数字abca + b == c时,a + b的舍入恰好与c的舍入一致。 有时它不会。
    • 0.1 + 0.3 == 0.4中,添加了 0.10000000000000000055511151231257827021181583404541015625 和 0.299999999999999988897769753748434595763683319091796875,舍入结果为 0.400000000000000002220446049250313080847263336181640625。这与0.4的结果相同,因此即使存在舍入误差,评估报告也为真。
    • 0.1 + 0.2 == 0.3中,将添加 0.10000000000000000055511151231257827021181583404541015625 和 0.20000000000000000001110223024625156540423631668090908203125,舍入结果为 0.30000000000000000000444089209850062616169452667236328125。这与.3的结果不同,后者为 0.29999999999999999988897769753748434595763683319091796875。所以评估报告是错误的。

后一个结果向我们展示了为什么显示0.1 + 0.2的结果会产生"0.30000000000000004"。它接近 0.3,但

0.2999999999999999988897769753748434595763683319091796875 更接近,因此,为了唯一区分 0.300000000000000000444089209850062616169452667236328125 与该更近的值,JavaScript 必须使用更多数字 - 它产生零,直到到达第一个非零数字,导致"0.3000000000000004"。我们可以问a + b == c什么时候会评估为真?数学绝对决定了这一点;abc分别转换为最接近的可表示值,执行加法并将其结果舍入到最接近的可表示值,然后如果左右结果相等,则表达式为 true。但这没有简单的模式。这取决于十进制数字以二进制形式形成的模式。你可以在这里和那里找到各种模式。但是,总的来说,它们实际上是随机的。

最新更新