0.1+0.2为什么不等于0.3

0.1 + 0.2 = 0.30000000000000004

IEEE 754 标准

IEEE 754 是 IEEE 二进制浮点数算术标准的简称. 也是目前业界通用的浮点数运算标准. 其常用的浮点数值表示方法分为: 单精度(32 位) 和 双精度(64 位).

JavaScript 中不论小数还是整数都只用一种数据类型来表示, 即 Number 类型. 其遵循 IEEE 754 标准, 使用双精度浮点数(double)64(8 字节)来存储一个浮点数. 我们也只介绍 64bits 的数值存储.

64Bits 双精度是怎么存储数据的

在 IEEE 754 标准中, 任意一个二进制浮点数 V 可以通过以下形式表示:

  • (-1)^S 表示符号位, 当 S = 0, V 为正数; 当 S = 1, V 为负数
  • M 表示有效数字, 大于等于 1, 小于 2
  • 2 ^ E 表示为指数

64Bits 分为以下 3 个部分

  • sign bit (S, 符号): 用来表示正负号, 0 为正 1 为负 (1 bit)
  • exponent (E, 指数): 用来表示次方数 (11 bits)
  • mantissa (M, 尾数): 用来表示小数部分 (52 bit)

符号 S

用来存储正负号, 0 表示正数 (+), 1 表示负数 (-)

1 的二进制为 0000 0001

-1 的二进制为 1000 0001

尾数 M

IEEE 754 规定, 在计算机内部保存 M 时, 1 <= M < 2, 所以默认这个数的第一位总是 1, 因此可以被舍去, 只保存后面部分. 所以对于双精度 64 位浮点数, M 为 52 位, 但是可以保存的有效数字为 52 + 1 = 53 位.

所以 JavaScript 的最大安全数为 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1

1
2
3
Math.pow(2, 53) === Math.pow(2, 53) + 1; // true
Math.pow(2, 53); // 9007199254740992
Math.pow(2, 53) + 1; // 9007199254740992

尾数 E

E 为一个无符号整数, 在双精度浮点数中 E 为 11 位, 取值范围: 0 ~ 2047.

但是科学计数法中的 E 是可以出现负数的, 所以 IEEE 754 规定, E 的真实值必须再减去一个中间数. 对于 8 位的 E, 中间数是 127; 对于 11 位的 E, 中间数是 1023.

指数 E 还可以分成三种情况:

  1. E 不全为 0 或不全为 1, 这时, 浮点数计算方法是, 指数 E 的计算值减去 127 ( 或 1023), 得到真实值, 再将有效数字 M 前加上第一位的 1
  2. E 全为 0, 浮点数的指数 E 等于 1 - 127 ( 或 1 - 1023), 有效数字 M 不再加上第一位的 1, 而是还原为 0.xxxxx 的小数. 这样做是为了表示 ±0, 以及接近于 0 的很小的数字.
  3. E 全为 1, 如果有效数字 M 全为 0, 表示 ± 无穷大(正负取决于符号位 S), 如果有效数字 M 不全为 0, 表示这个数不是一个数 ( NAN )

十进制小数如何转二进制

转换

十进制小数转二进制, 小数部分 乘 2 取整数. 若乘之后的小数部分不为 0, 继续乘 2 直到小数部分为 0, 将取出的整数正向排序.

如: 0.1 转二进制

1
2
3
4
5
6
7
8
9
10
0.1 * 2 = 0.2 ---------------- 取整数 0, 小数 0.2
0.2 * 2 = 0.4 ---------------- 取整数 0, 小数 0.4
0.4 * 2 = 0.8 ---------------- 取整数 0, 小数 0.8
0.8 * 2 = 1.6 ---------------- 取整数 1, 小数 0.6
0.6 * 2 = 1.2 ---------------- 取整数 1, 小数 0.2
0.2 * 2 = 0.4 ---------------- 取整数 0, 小数 0.4
0.4 * 2 = 0.8 ---------------- 取整数 0, 小数 0.8
0.8 * 2 = 1.6 ---------------- 取整数 1, 小数 0.6
0.6 * 2 = 1.2 ---------------- 取整数 1, 小数 0.2
...

最终 0.1 的二进制表示为

1
0.000110011001100110011(0011); // 0011 将会无限循环

IEEE 754 存储

0.1 的二进制表示如下所示

符号位

由于 0.1 为整数, 所以符号位 S = 0

指数位

E = -4, 实际存储为 -4 + 1023 = 1019, 二进制为 1111111011, E 为 11 位, 最终值为 01111111011

尾数位

在 IEEE 754 中, 循环位不能无限循环下去, 在双精度 64 位下最多存储的有效整数位为 52 位, 采用就近舍入模式(进一舍零) 进行存储

1
2
3
11001100110011001100110011001100110011001100110011001; // M 舍去首位的 1,得到如下
1001100110011001100110011001100110011001100110011001; // 0 舍 1 入,得到如下
1001100110011001100110011001100110011001100110011010; // 最终存储

得到最终结果:

1
0	01111111011	1001100110011001100110011001100110011001100110011010

0.1 + 0.2 等于多少?

通过同样的方法计算出 0.2 的存储值, 我们得到以下结果

1
2
3
S  E            M
0 01111111011 1001100110011001100110011001100110011001100110011010 // 0.1
0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2

浮点数运算步骤

对阶

浮点数加减首先判断两数的指数位是否相同(小数点位置是否对齐), 若两数指数位不同, 需要对阶保证指数位相同.

对阶时遵守小阶向大阶看齐原则, 尾数向右移位, 每移动一位, 指数位加 1, 直到指数位相同, 即完成对阶

本示例, 0.1 的阶码为 -4 小于 0.2 的阶码 -3, 故对 0.1 做移码操作

1
2
3
4
5
6
// 0.1 移动之前
0 01111111011 1001100110011001100110011001100110011001100110011010
// 0.1 右移 1 位之后 尾数最高位空出一位 (0舍1入, 舍去末尾0)
0 01111111100 100110011001100110011001100110011001100110011001101(0)
// 0.1 右移 1 位完成
0 01111111100 1100110011001100110011001100110011001100110011001101

尾数右移 1 位之后最高位空出来了,如何填补呢?涉及两个概念:

  • 逻辑右移:最高位永远补 0
  • 算术右移:不改变最高位值,是 1 补 1,是 0 补 0

尾数求和

两个尾数直接求和

1
2
3
4
5
0  01111111100   1100110011001100110011001100110011001100110011001101 // 0.1
+
0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2
=
0 01111111100 100110011001100110011001100110011001100110011001100111 // 产生进位,待处理

规格化

由于产生进位,阶码需要 + 1,对应的十进制为 1021,此时阶码为 1021 - 1023(64 位中间值)= -2,此时符号位、指数位如下所示

1
2
S  E
0 01111111101

尾部进位 2 位,去除最高位默认的 1,因最低位为 1 需进行舍入操作(在二进制中是以 0 结尾的),舍入的方法就是在最低有效位上加 1,若为 0 则直接舍去,若为 1 继续加 1

1
100110011001100110011001100110011001100110011001100111 = 00110011001100110011001100110011001100110011001101000 = 00110011001100110011001100110011001100110011001101000 = 0011001100110011001100110011001100110011001100110100; // + 1 // 去除最高位默认的 1 // 最后一位 0 舍去 // 尾数最后结果

IEEE 754 中最终存储如下:

1
0  01111111101 0011001100110011001100110011001100110011001100110100

最高位为 1,得到的二进制数如下所示:

1
2 ^ (-2 * 1.00110011001100110011001100110011001100110011001101);

转换为十进制如下所示:

1
0.30000000000000004

如何解决精度问题

数字转成整数

1
2
3
4
5
6
function add(num1, num2) {
const num1Digits = (num1.toString().split(".")[1] || "").length;
const num2Digits = (num2.toString().split(".")[1] || "").length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}

但是这种方法对数值较大的数支持依旧不好

用第三方库

总结

由于计算机底层存储都是基于二进制的,需要事先由十进制转换为二进制存储与运算,这整个转换过程中,类似于 0.1、0.2 这样的数是无穷尽的,无法用二进制数精确表示。JavaScript 采用的是 IEEE 754 双精确度标准,能够有效存储的位数为 52 位,所以就需要做舍入操作,这无可避免的会引起精度丢失。另外我们在 0.1 与 0.2 相加做对阶、求和、舍入过程中也会产生精度的丢失。