首页 > 解决方案 > 为什么 0.1 + 0.2 会在 JavaScript 中返回不可预测的浮点结果,而 0.2 + 0.3 不会?

问题描述

0.1 + 0.2
// => 0.30000000000000004

0.2 + 0.2
// => 0.4

0.3 + 0.2
// => 0.5

我知道它与浮点有关,但这里到底发生了什么?

根据@Eric Postpischil 的评论,这不是重复的:

那只涉及为什么“噪音”出现在一个加法中。这个问题问为什么“噪音”出现在一个加法中,而没有出现在另一个加法中。另一个问题没有回答。因此,这不是重复的。实际上,差异的原因不是浮点运算本身,而是由于 ECMAScript 2017 7.1.12.1 step 5

标签: javascriptfloating-pointprecision

解决方案


在 JavaScript 中将 Number 值转换为字符串时,默认使用刚好足够的数字来唯一区分Numbervalue1这意味着当一个数字显示为“0.1”时,并不意味着它恰好是 0.1,只是它比任何其他 Number 值更接近 0.1,所以只显示“0.1”告诉你它是这个唯一的数字值,即 0.1000000000000000055511151231257827021181583404541015625。我们可以用十六进制浮点表示法将其写为 0x1.999999999999ap-4。(p-4将前面的十六进制数字乘以 2 的 -4 次方的方法,因此数学家会将其写为 1.999999999999 16 • 2 -4。)

0.1以下是在源代码中编写、0.2和时产生的值,0.3它们被转换为 JavaScript 的数字格式:

  • 0.1 → 0x1.999999999999ap-4 = 0.1000000000000000055511151231257827021181583404541015625。
  • 0.2 → 0x1.999999999999ap-3 = 0.200000000000000011102230246251565404236316680908203125。
  • .3 → 0x1.3333333333333p-2 = 0.299999999999999988897769753748434595763683319091796875。

当我们评估时0.1 + 0.2,我们添加了 0x1.999999999999ap-4 和 0x1.999999999999ap-3。要手动执行此操作,我们可以首先通过将其有效数(小数部分)乘以 2 并从其指数中减去 1 来调整后者,从而产生 0x3.3333333333334p-4。(您必须以十六进制进行此算术运算。A 16 • 2 = 14 16,所以最后一位是 4,并且携带 1。然后 9 16 • 2 = 12 16,携带的 1 使其成为 13 16. 这会产生一个 3 位数字和一个 1 进位。)现在我们有 0x1.999999999999ap-4 和 0x3.3333333333334p-4,我们可以添加它们。这将产生 4.ccccccccccccep-4。这是精确的数学结果,但是对于数字格式来说它有太多位。我们在有效数字中只能有 53 位。4 (100 2 ) 中有 3 位,后 13 位中的每一个有 4 位,因此总共有 55 位。计算机必须删除 2 位并对结果进行四舍五入。最后一位数字 E 16是 1110 2,所以 10 位必须删除。这些位恰好是前一位的 ½,因此它是向上或向下舍入之间的关系。打破平局的规则是四舍五入,所以最后一位是偶数,所以我们四舍五入使 11 位变为 100。E 16变为 10 16,导致进位到下一位。结果是 4.cccccccccccd0p-4,等于 0.3000000000000000444089209850062616169452667236328125。

现在我们可以看到为什么打印.1 + .2显示“0.30000000000000004”而不是“0.3”。对于数值 0.299999999999999988897769753748434595763683319091796875,JavaScript 显示“0.3”,因为该数值比任何其他数值更接近 0.3。它在小数点后第 17 位与 0.3 相差约 1.1 ,而我们得到的加法结果在第 17与 0.3 相差约 4.4 。所以:

  • 源代码0.3生成 0.299999999999999988897769753748434595763683319091796875 并打印为“0.3”。
  • 源代码0.1 + 0.2生成 0.3000000000000000444089209850062616169452667236328125 并打印为“0.30000000000000004”。

现在考虑0.2 + 0.2。结果是 0.40000000000000002220446049250313080847263336181640625。那是最接近 0.4 的数字,因此 JavaScript 将其打印为“0.4”。

最后,考虑0.3 + 0.2。我们正在添加 0x1.999999999999ap-3 和 0x1.3333333333333p-2。我们再次调整第二个操作数,生成 0x2.6666666666666p-3。然后相加产生 0x4.0000000000000p-3,即 0x1p-1,即 ½ 或 0.5。所以它打印为“0.5”。

另一种看待它的方式:

  • 源代码0.1和的值0.2分别略高于 0.1 和 0.2,将它们相加会产生一个高于 0.3 的数字,并且误差会得到加强,因此总误差大到足以将结果推离 0.3,足以让 JavaScript 显示错误。
  • 添加0.2 + 0.2时,错误再次加强。但是,这种情况下的总误差不足以将结果推离 0.4 太远,以至于 JavaScript 以不同的方式显示它。
  • 源代码的值0.3略低于 0.3。当添加到0.2略高于 0.2 的 时,误差被取消,正好为 0.5。

脚注

1此规则来自 ECMAScript 2017 语言规范第 7.1.12.1 条中的第 5 步。


推荐阅读