为什么不能使用"=="运算符判断两个浮点数是否相等

在大多数编程语言中,使用"=="运算符判断两个浮点数是否相等的结果都是难以确定的,并且几乎总是无意义的,在编程实践中应该完全禁止使用"=="运算符去判断两个浮点数是否相等。

一个问题及其分析

如若不信,可以先试着运行下面这段js代码,看看它的输出结果:

console.log(0.1 + 1 - 1 == 0.1);

在Chrome Console中,上面这句代码的输出是false,意味着(0.1+1-1)这个数不等于0.1! 看上去加减法互为逆运算的这一基本运算法则都被颠覆了,这究竟是什么原因呢?

计算机科学作为应用数学的一个分支,和纯数学相比有一个显著的特点,那就是无法摆脱物理规律限制。浮点数的计算精度及其带来的一系列问题正是这一特点在存储字长受限上的体现。在详细解读这句js代码之前,我们不妨先来看看另外一个问题:

先将有理数1/7 [无限循环小数 0.1(428571)] 以小数形式写在一张最多只能容纳9个数字的小纸条上,得到0.14285714;再将这个数加上10并把结果写到第二张同样长的纸条上,最后一个数字4由于纸条太短写不下,只好舍去,得到10.1428571;然后将第二张纸条上的数字减去10的结果写到第三张纸条上,得到0.1428571。此时,如果认为第一张纸条上的数字是1/7, 第三张纸条上的数字是(1/7 + 10 - 10), 就会得出(1/7 + 10 - 10)不等于1/7的荒谬结论!然而,不论是第一张还是第三张纸条上的数字,都不是1/7的精确数值,而是1/7的近似值,并且近似的精度还不一样,所以它们不相等。

尽管浮点数在计算机器中的表示比上面所举的纸条示例要更复杂,但Javascript里的(0.1+1-1)这个数之所以和0.1不相等,与上面例子中(1/7 + 10 - 10)之所以不等于1/7并无本质上的不同:两者都使用了有限空间来存储无限小数,必然只能存储一个近似值,而不同的计算过程影响了近似值的精度。显然,如果使用"=="运算符对两个有着不同精度的近似值逐位进行比较,有可能会得到false的判断结果。

下面具体分析在Javascript中浮点数0.1经过先加1再减1的操作过程后,最终如何得到了一个不同于0.1的数。

JavaScript的Number类型都是64位浮点数[1],按照IEEE 754标准,0.1的存储模式为:

  • 最高位(第63位)为符号位,0,表示是正数
  • 第52-62这11位为指数部分,01111111011,转成十进制数为1019,表示指数值为1019-1023=-4
  • 第0-51这52位为小数部分,1001100110011001100110011001100110011001100110011010 [十进制数0.1的二进制表示是无限循环小数 0.(1001), 第50-51位的10是由精确值的第50-52位011舍入(rounding)第52位上的1进位后得到], 这样,小数值为二进制数1. 1001100110011001100110011001100110011001100110011010 [根据标准,小数部分的最高位总为1,于是被省去,这里需要加回],将其转成十进制是一个非常接近1.6的数。最终,1.6*2^(-4)=0.1

1的存储模式为:

  • 符号位0
  • 指数值为0
  • 小数值为1. 最终,1*2^(0)=1

0.1+1的处理过程为: 将0.1的小数部分右移4位(可见部分的最高3位以及隐藏的最高位1),得到小数部分为0001100110011001100110011001100110011001100110011010 (第50-51位的10由精确值的第50-52位011舍入第52位上的1进位后得到),同时将指数值加上4变为0,让它与1对齐;将指数值设为0,再将小数部分相加,即为0.1+1的最后结果1.1 (这里由于1的小数部分仅有最高隐藏位为1,相加后正好被隐去,于是结果的小数部分无变化)。

1.1-1的处理过程为: 两者的指数值都为0,无需移位对齐;两者的小数部分的隐藏最高位相减后被抵消,1的小数部分全为0,所以其余位无变化;此时的小数部分的最高位不是1,需要进行规约化(normalize)使最高位为1,具体操作为左移4位,同时将指数值减去4变为-4,并在最低4位补0。最后结果的小数部分为1001100110011001100110011001100110011001100110100000。

可见,(1.1-1)的最低6位和0.1是不一样的,这就是(0.1 + 1 - 1 == 0.1)被判断为false的原因。

正确的比较方式

function fpeq(a, b) {
    let eps = 1e-08;
    return Math.abs(a-b) < eps;
}

console.log(fqeq(0.1 + 1 - 1, 0.1));

延伸阅读

  • 浮点数的二进制表示
  • "4.2.1 Single-Precision Calculations" in The Art of Computer Programming, Vol 2: Seminumerical Algorithms, Third Editioin, by D. E. Knuth, 1998
  • "2.4 Floating Point" in Computer Systems: A Programmer's Perspective, by R. E. Bryant and D. R. O'Hallaron, 2003

[1] http://www.w3schools.com/js/js_numbers.asp

上一篇:《编写高质量代码:改善c程序代码的125个建议》——建议16-1:尽量使用复合赋值运算符


下一篇:python中switch语句用法