为什么这个浮点加法只能通过将所有数字乘以 10 来解决?

why this Floating point addition can be solved only by multiply all the figures by ten?

提问人:eric2023 提问时间:7/26/2023 最后编辑:Barmareric2023 更新时间:7/27/2023 访问量:77

问:

由于 Javascript 中的浮点运算,我知道将在 Javascript 中,但我不明白为什么只将数字乘以 10,即 ,然后问题就可以解决了。应该乘以 ,对吧?0.2 + 0.1 = 0.30000000000000004let x = (0.2 * 10 + 0.1 * 10) / 10;10**10

let y = (0.2 * 10 + 0.1 * 10) / 10;
document.getElementById("abc").innerHTML = "0.2 + 0.1 = " + y;
<!DOCTYPE html>
<html>

<body>

  <p id="abc"></p>

</body>

</html>

JavaScript HTML 浮点 精度 固定

评论

0赞 Pointy 7/26/2023
为什么应该是 10**10?我的意思是,你有证据证明 10 是正确的值。
0赞 eric2023 7/26/2023
Javascript 中的“0.1”恰好是 0.00011 0011 0011 0011...0011,要使它变成整数,它应该乘以 10**10 之类的东西,否则,还有剩余的数字。
0赞 jabaa 7/26/2023
从何而来?10**10
2赞 Keith 7/26/2023
Because of Floating point arithmetic in Javascript,请注意,这不是 Javascript 的东西,而是计算机存储数字的方式。IEEE 754 认证。例如。让我们试试 Rust 示例 ->println!("{}", 0.1 + 0.2); = 0.30000000000000004
0赞 eric2023 7/26/2023
不,我已经阅读了上面的链接,因为我读过,所以我问这个问题,Javascript 中的“0.1”正是 0.00011 0011 0011 0011 ...0011,要使它变成整数,它应该乘以 10**10 之类的东西,否则,还有剩余的数字。

答:

0赞 Barmar 7/27/2023 #1

当你乘以 10 时,多余的位会四舍五入,幸运的是,这最终会产生整数。但这并不总是可以保证的,它只是恰好适用于这两个数字。

正确的解决方案是在乘以 10 后使用显式舍入。

let y = (Math.round(0.2 * 10) + Math.round(0.1 * 10)) / 10;
document.getElementById("abc").innerHTML = "0.2 + 0.1 = " + y;
<!DOCTYPE html>
<html>

<body>

  <p id="abc"></p>

</body>

</html>

评论

0赞 eric2023 7/27/2023
非常感谢您的回复和帮助,但您能否说明额外的位是如何四舍五入并幸运地最终产生整数的?0.1 等于 1.10011 *2exponent(-4),对吧?10 等于 1.01 * 2exponent(3)),对吧?最终产生整数有多幸运?非常感谢。
0赞 Barmar 7/27/2023
我真的不确定我能详细解释它。
0赞 Eric Postpischil 7/27/2023
回复“......最终产生整数......恰好对这两个数字有效“:这是一个证明的粗略草稿:设 x 是一个数字,它是某个整数 k 除以 10。将 x 转换为浮点会产生 x+e,表示一些舍入误差 e ≤ 1/2ULP(x)。当我们乘以 10 时,10(x+e) 四舍五入为浮点数。它是 10x+10e,所以 k+10e。 10e ≤ 10•1/2ULP(x) = 1/2ULP(k)。因此,10x+10e 比任何其他浮点值都更接近 k,因此结果是 k。当 x 或 10x 接近双元过渡时,ULP(10x) 实际上可能不是 10 ULP(x),因此需要单独考虑这些,因此需要粗略起草。
0赞 Sam Mason 7/27/2023 #2

您似乎在混合碱基,而没有指定何时使用哪一个碱基。了解IEEE754实现将在每次操作后提供正确的舍入操作也会有所帮助。

正如您所指出的,0.1 不能完全表示为二进制分数。最接近的值(对于 64 位浮点数)为:

0.1000000000000000055511151231257827021181583404541015625

当你说“0.00011 0011 0011 0011...0011“,如果说这些是以 2 为基数的数字,而不是十进制数字,会有所帮助。

当你说 0.1 * 10 时,你实际上是在要求:

10 * 0.1000000000000000055511151231257827021181583404541015625

计算机进行精确的计算,然后将其四舍五入到最接近的可表示浮点数。接近 1.0 的其他值(可以用 64 位二进制浮点数准确表示)是:

0.99999999999999988897769753748434595763683319091796875
1.0000000000000002220446049250313080847263336181640625

它们都与数字相距约 1.7e-16,而与选择 1 相关的误差仅为 ~0.5e-16。因此,FPU 应选择 1。

这同样适用于 0.2,它四舍五入到 2。

不幸的是,我不认为 Javascript 会公开许多有用的原语来查看正在发生的事情,所以我一直在使用 Python,因为它非常适合交互式探索这样的事情。

# arbitrary precision decimal
from decimal import Decimal as D

# see https://en.cppreference.com/w/c/numeric/math/nextafter
from math import inf, nextafter

print(D(0.1))
# => 0.1000000000000000055511151231257827021181583404541015625

print(D(nextafter(1, inf)))
# => 1.0000000000000002220446049250313080847263336181640625

print(D(nextafter(1, -inf)))
# => 0.99999999999999988897769753748434595763683319091796875

print(D(nextafter(1, -inf)) - D(0.1)*10)
# => -1.665334536935156540423631668E-16

希望这能让您对内部发生的事情有所了解!

评论

0赞 Sam Mason 7/27/2023
朱莉娅·埃文斯(Julia Evans)对此有一篇很好的文章,请参阅 jvns.ca/blog/2023/02/08/...
0赞 Monke 11/25/2023 #3

好吧,乘以任何数字,只要两个数字都是整数,就可以进行操作。10 是一个很好的数字,因为只需 10 的幂即可将小数位转换为整数部分。 去掉小数位很有帮助的原因是,二进制可以更好地表示数字,例如 1/3 不表示为 0.333...... 因为它是多余的。二进制发生相同的问题,但由于它使用 2 的幂和其他表示小数的方法,因此数字不同,最终它必须做一些事情。这就是为什么 0.1 + 0.2 = 0.300...4。我不一定确定为什么你会认为应该使用 10^10,它可能与 0.300...4 有关,但浮点答案很可能是错误的,所以这就是为什么方程的项相乘,而不是答案,还可能发生更多的浮点问题,留下更糟糕的结果。这是我的函数,我喜欢用它来解决烦人的算术浮点问题。

//z = 1 or 0 (+ or -)
function float_operator(x, y, z){
    const precision = Math.max((Number.isNaN(x) ? 0 : (x.toString().split(".")[1] || "").length),(Number.isNaN(y) ? 0 : (y.toString().split(".")[1] || "").length)) || 1;
    const multiplier = Math.pow(10, precision);
    const result = z === 1 ? (x * multiplier + y * multiplier) / multiplier : (x * multiplier - y * multiplier) / multiplier;
    return result;
}
//> float_operator(0.1, 0.2, 1)
//> 0.3