0.1 + 0.2 !== 0.3

有一道很常见的面试题面试

0.1 + 0.2 === 0.3 // true ? false
复制代码

你们应该都知道是 false,可是为毛不相等呢?下面我将从 浮点数表示浮点数精度 两个方面来解释算法

Javascript 浮点数表示

Javascript 中不存在整型和浮点型之分,只有一个类型 Number,它遵循 IEEE 二进制浮点数算术标准(IEEE754),使用 64 位 双精度浮点数(double) 存储。bash

双精度存储是知道了,那双精度浮点数是如何存储数据呢?主要有如下几个关键点spa

1. 双精度浮点数使用 64 位存储设计

  • sign(S):符号位,长度为 1,0 表明数字为正,1 表明数字为负
  • exponent(E):指数位,长度为 11,二进制科学计数法的指数位
  • mantissa(M):尾数位,长度为 52 ,二进制科学计算法的尾数位

数值的计算公式为(二进制):code

value = (-1)^S * 2^E * M

2. 使用二进制科学计数法cdn

举个栗子:
0.5_{10} 转换为二进制表示 0.1_2
0.1_2 转换为二进制科学计数法 1*2^{-1}blog

3. 指数位表示有符号整数接口

由于指数位数值为无符号整数,范围为[0, 2047],在规格化值中取 [1, 2046]。但指数位须要表示的是有符号整数,则[1, 1022] 表示为负数[-1022, -1],1023 表示为 0,[1024, 2046]表示为正数[1, 1023],因此整个指数位表示的范围是[-1022, 1023]ip

由上可推导出公式:

E = e - Bias = e - 1023
  • E:指数位表示的数值
  • e:指数位实际存储的数值

举个栗子:
0.5_{10}表示为二进制科学计数法1*2^{-1},该数 E = -1,e = 1022

tips: 下面会讲什么是规格化值,和其余的值

4. 尾数不表示二进制科学计算的整数部分,即不表示 1

因为规格化的数整数位都是 1,因此在存储时能够节约空间,不表示整数位,从小数位开始表示。因此尾数位其实最大能表示 53 位

由上可推导出公式:

M = 1 + f
  • M:位数位表示的数值
  • f:位数位实际存储的数值

举个栗子:
二进制科学计数法1*2^{-1},该数 M=1,则实际存储 f=0,

由上可将数值计算公式推导为

value = (-1)^S * 2^{e-1023} * (f + 1)

非规格化值 和 特殊值

那么问题来了,若是按公式 M = f + 1,那么如何表示 0 呢?,即便你设 f = 0,e=任意值,按照公式算出,value 不可能为 0。固然第一个公式 value = (-1)^S * 2^E * M 没有问题,问题出在公式的推导,由于推导出的公式只符合规格化数值,下面介绍下规格化数值和其余数值

  • 规格化数值:e 不全为 0,也不全为 1,f 为任意值

  • 非规格化数值:e 所有为 0,f 为任意值。非规格化数值主要用于表示 0,以及接近 0 的数。此时公式为

    E = e = 0,M = f
    value = (-1)^S * f
  • 无穷大:e 所有为 1,f 为 0

  • NaN:e 所有为 1,f 不为 0

因此:当 e = 0 时,f = 0 时,表示数值 0

解释 0.1 + 0.2 !== 0.3

到这里,咱们已经能够解释 0.1 + 0.2 为何不等于 0.3 咯

0.1_{10} 表示为二进制为:0.0001100...1100...1100..._2
转换为二进制科学计数法为:1.1001100...1100...1100...*2^{-4}
计算得:S = 0,E = -4,M = 1.1001100...1100...1100...,则 S = 0,e = 1029,f = 1001100...1100...11010
将截断(舍入)后的数值从新表示为二进制,则 0.1 最终的二进制数值为: 0.00011...0011...001101

同理,表示出 0.2,0.3 的二进制数值

0.1:0.0001100110011001100110011001100110011001100110011001101
0.2:0.001100110011001100110011001100110011001100110011001101
0.3:0.010011001100110011001100110011001100110011001100110011

0.1 + 0.2 和为: // 下面会详情介绍该步骤
     0.0100110011001100110011001100110011001100110011001101
将和与0.3对比,发现并不相等,中间差值为:
     0.000000000000000000000000000000000000000000000000000001
复制代码

从结果值来看,中间差值已经很小很小了,已经能够忽略了不计了。事实上在 ES6 Number 扩展中,增长 Number.EPSILON 属性,表示 1 与大于 1 的最小浮点值差,值为 2^{-52},当值小于 Number.EPSILON 时,通常可忽略不计

浮点数精度

舍入

尾数位只能存储 52 位,可是在 0~1 之间的实数是无穷尽的,这些无穷的数该如何表示呢?既然彻底表示不可能完成,那么只有舍弃掉某些数值,来找出最近的浮点数匹配。那么到底采用哪一种舍入方法呢?下面介绍经常使用的舍入方法

  • 向偶数舍入:也称为向最接近值舍入
  • 向零舍入:舍弃末尾位之外的数值,能够按照 Math.trunc 理解
  • 向正无穷舍入:向较大的数舍入,能够按照 Math.ceil 理解
  • 向负无穷舍入:向较小的数舍入,能够按照 Math.floor 理解

IEEE754 采用的是 向偶数舍入,原则是保证损失精度最小,下面简单介绍一下舍入规则

  1. 对于恰巧中间值的状况,若是保留位数最后一位是偶数则舍弃后续数值,奇数则进位。例如 1.01_2 保留 1/2 为 1.0_21.11_2 保留 1/2 为 1.2_2
  2. 对于向上的值较近,则进位。如 1.01010101 保留 1/2 为1.1_2
  3. 对于向下的值较近,则舍弃。如 1.001 保留 1/2 为 1.0_2

举个栗子:

0.1 => 0.0001100110011001100110011001100110011001100110011001100 | 110011...
// 因为须要保留的最后一位数后为 110011...,舍入时离向上的值较近,应该进位,因此
0.1 => 0.0001100110011001100110011001100110011001100110011001101
复制代码
  • "|" 表示在该处须要舍入

tips:若是你实在没法判断如何舍入,有个简单的办法。先向下舍入,与原数相减;再向上舍入,与原数相减,将两个差值比较,取差值绝对值较小的那个数;若是差值相等,则取末尾为偶数的那个数

计算

为了进一步保证精度,IEEE754 标准,双精度在中间计算时,额外保留三位,分别是 保护位、舍入位、粘贴位

  • 保护位:精度最低位右侧一位(双精度能够理解为尾数的第 53 位)
  • 舍入位:保护位右侧一位
  • 粘贴位:舍入位右侧一位,表明舍入位右侧是否还有数据,若是右侧还有数据,则粘贴位为 1,不然为 0,目的是为了支持目标数值向最近的偶数舍入

在浮点数计算时,经过额外保存三位,来增长计算的正确性,找到浮点数最接近的匹配。

模拟 0.1 + 0.2 计算

讲了那么多,最后仍是简单写一下 0.1 + 0.2 的二进制计算过程。

0.1:S = 0,E = -4,M = 1.1001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010

// 对阶 小阶对大阶
0.1:S = 0,E = -3,M = 0.11001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010

// 将 M 值相加
和为:10.01100110011001100110011001100110011001100110011001110

计算出的0.3:S = 0,E = -3,M = 10.01100110011001100110011001100110011001100110011001110

// 规格化
计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011 | 10

// 计算时,右边多保留两位,此处保护位 = 1,舍入位 = 0,粘贴位 = 0
// 舍入后
计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110100
浮点数的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011

// 转换10进制
计算出的0.3 = 0.30000000000000004
复制代码
  • "|" 表示应该截断,后续数值即为保护位,舍入位,粘贴位

参考

  • 《深刻理解计算机系统》第二章节
  • 《计算机组成与设计 软件/硬件接口》 第三章节
  • 维基百科 IEEE 754
相关文章
相关标签/搜索