浮点数相等性测试引起的困惑?

3

数字6.35无法精确表示:

alert( 6.35.toFixed(20) ); // 6.34999999999999964473

但是为什么6.35 * 10 == 63.5是正确的呢?

6.35不够精确,10是精确的,而63.5是精确的。我无法理解为什么(不精确的* 精确的)等于精确的。


JavaScript中的数字精度为16或17位数字 - 您要求21位(包括6...也许这个问题可以帮助您。 - Bravo
2
舍入系统旨在最小化舍入误差,而不是保证它总是会影响比较结果。您得出的结论只意味着最接近 6.35 的数乘以 10 并四舍五入至最近的数字后是 63.5。 - Patricia Shanahan
2个回答

3
这是逻辑错误:最后一个操作6.35 * 10.0不准确而不是准确的。
只有可能连续发生多个“舍入误差”抵消,也可能累积它们。
635/100的最接近双精度浮点数是 635/100-1/2,814,749,767,106,560 ,或者如果您愿意,可以使用 635/100-1 /(10 * 2 ^ 48)。因此,准确的* 10 操作应该回答 635/10-1/(2 ^ 48)。但是,这个数量不能表示为双精度浮点数(请参见下文)......因此,最后一个操作是不准确的。
这两个相邻的数是63.5(正好是 635/10 )和它的前任 635/10-1/(2 ^ 47)。一个有趣的精确连接情况:精确数量与两个可表示的双倍邻居相同的距离, 默认舍入模式是四舍五入,连接到偶数,因此FPU将选择具有偶数有效位数的double,即 635/10
这是IEEE 754算术的好运还是一个好属性? 如果我在Squeak / Pharo Smalltalk中评估此代码片段(具有精确分数和精确算术值的比较):
(1 to: 10000) count: [:x | (x/10.0) = (x/10) and: [(x/100.0) ~= (x/100)]].

我得到了 1600 个案例,其中 x/10 能够被准确地表示为双精度浮点数,而 x/100 则不能。

如果我选择这 1600 个案例,并验证舍入误差是否被消除:

((1 to: 10000) select: [:x | (x/10.0) = (x/10) and: [(x/100.0) ~= (x/100)]])
    count: [:x | (x/100.0*10) = (x/10)]

我发现在1600个案例中,误差被抵消了,这是IEEE754算法的一个好特性。但这仍然是运气。

如果我尝试将其除以1000.0再乘以100,我会得到一个错误答案:

((1 to: 10000) select: [:x | (x/10.0) = (x/10) and: [(x/1000.0) ~= (x/1000)]])
    allSatisfy: [:x | (x/1000.0*100) = (x/10)]

答案对于1920个案例中的1649个是正确的,这已经是一个很好的分数了。

0

Javascript使用IEEE 754浮点标准,其中所有数字都无法准确表示。

数字表示为2的幂的倍数,包括负二次幂,分母不是2的幂的数字无法准确表示。

出于同样的原因,我们得到0.1 + 0.2 == 0.3 //false

这是二进制浮点数最著名的副作用,对于所有使用IEEE 754格式表示数字的语言(不仅仅是Javascript),这一点仍然成立。

因此,在处理小数值时,有些情况需要更加小心。

最常接受的做法是在进行比较时使用微小的舍入误差。这个值被称为机器epsilon,它是2 ^ 52

从ES6开始,Number.EPSILON预定义了这个容差值。

function numbersCloseEnoughToEqual(n1,n2) {
  return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 6.35.toFixed(20);
var b = 6.35;

console.log(numbersCloseEnoughToEqual( a, b ));// true


这并没有回答所提出的问题。 - Eric Postpischil
我试图解释浮点数的工作原理以及javascript如何通过IEEE 754标准(Number.EPSILON)来最小化副作用。我尝试提到用户在处理小数值时应该做什么以避免副作用。这就是用户询问“我不明白为什么(不准确*准确)等于准确”的意图所在。 - Akshay Bande
当然,你试图解释浮点数的工作原理以及JavaScript如何最小化浮点算术的不良影响。然而,这并没有回答问题:为什么将6.3510相乘会得到恰好63.5,即使6.35并不完全等于6.35。无论你在这个答案中尝试做什么,这个答案都没有回答被问出的问题。 - Eric Postpischil
此外:(a)“数字表示为2的整数次幂”是不正确的。这可能主要是语言问题;“数字表示为2的幂的倍数,包括负2的幂”才是正确的。(b)建议使用容差进行比较通常是错误的建议。(c)建议容差为绝对值Number.EPSILON是错误的。使用浮点结果计算时产生的误差从0到无穷大,也可以是NaN,因此没有单个容差可以使用。 - Eric Postpischil
对于点(a),我已经更新了我的答案,对于点(b),是的,你绝对是正确的,没有单一的公差可以适用。但是在这里,我试图比较两个浮点值,其中一个可以是操作的结果,另一个可以是期望的,例如0.1 + 0.3将得到不同的结果,但期望的结果是0.3,因此您可以使用numbersCloseEnoughToEqual来获得正确的结果。 - Akshay Bande

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接