js 数值精度损耗之 IEEE 754 浮点数

2,246 阅读6分钟

IEEE 754 浮点数

浮点型数据是用来表示具有小数点的实数。为什么在C中把实数称为浮点数呢?在C语言中,实数是以指数形式存放在存储单元中的。一个实数表示为指数可以有不止一种形式,如3.14159可以表示为:3.14159×10e0,0.314159×10e1,0.0314159×10e2,31.4159×10e-1,314.159×10e-2等,它们代表同一个值。可以看出:小数点的位置是可以在314159几个数字之间和之前或之后浮动的,只要在小数点位置浮动的同时改变指数的值,就可以保证它的值不会改变。由于小数点位置可以浮动,所以实数的指数形式称为浮点数。

单精度和双精度的存储结构:

浮点数的存储结构分为单精度和双精度。单精度总共4字节32位,双精度总共8字节64位。

以单精度为例,探讨十进制小数转化二进制过程:

十进制小数需要先转化为二进制小数,再进行存储。

  • 小数先转二进制数
  • 格式化为"尾数+阶码"的形式,即 1.M * 2E127{2^{E-127}}(M为二进制小数,E为阶码,127为偏移量,E - 偏移量 = 实际的二进制指数)
  • 分别存储"符号"、"阶码"、"尾数" 3个纬度的信息

以 123.456 为例子:

/**
 * 十进制转二进制
 * 整数部分:不断除2,取余数,直到商为0
 * 小数部分:不断乘2,取整数,直到积为0
 * 
 * 计算过程
 * 整数
 *  123/2 = 61(1);
 *  61/2 = 30(1);
 *  30/2 = 15(0);
 *  15/2 = 7(1);
 *  7/2 = 3(1);
 *  3/2 = 1(1);
 *  1/2 = 0(1);
 * 逆向取值,所以整数转为二进制:1111011
 * 
 * 小数
 *  0.456*2 = 0.912(0);  
 *  0.912*2 = 1.824(1);
 *  0.824*2 = 1.648(1);
 *  0.648*2 = 1.296(1);
 * 
 *  0.296*2 = 0.592(0);
 *  0.592*2 = 1.184(1);
 *  0.184*2 = 0.368(0);
 *  0.368*2 = 0.736(0);
 *  ... 无限不循环
 * 
 * 正向取值,所以小数转化为二进制:0111 0100 1011 1100 0110 1010 0111 1110 1111 1001 1101 1011
 * 
 * 故该十进制对应的二进制数为:1111011.011101001011110001101010011111101111100111011011
 * 转成"尾数+阶码"的格式为:1.111011011101001011110001101010011111101111100111011011 * 2^6,所以阶码E应该为 6 + 127 = 133
 * 精度损失之后保留23位:1.11101101110100101111000 * 2^6
 * 
 * 符号为正,故是0
 * 阶码为6,而存储阶码时,32位偏移量为127(64位为1023),所以实际应该存 127+6 = 133,转成二进制为 1000 0101
 * 尾数直接取二进制小数 11101101110100101111000
 * 最后转成单精度的值为 0 10000101 11101101110100101111000
 */

以上是十进制转单精度二进制的过程,以下对浮点数进行详细讨论。

浮点数的分类

以双精度为例,阶码位数为 11位,所以阶码E的取值范围 [0, 2047]。

规格化

当阶码不为 0(每一位都为0) 和 2047(每一位都为1) 时,即是规格化的浮点数,表示成二进制小数的格式为 1.M * 2E1023{2^{E-1023}}

非规格化

当阶码为 0(每一位都为0) 时,为非规格化的浮点数,用于表示0或者非常接近0的数。它的尾数不会像规格化数据那样加1,为了平滑从非规格化数据过渡到规格化数据,它的阶码为 1-1023,故非规格化的格式为 0.M * 21022{2^{-1022}}

当 M 全为0时,依据符号位分别表示正负0。

无穷大

当阶码为 2047(每一位都为1),所有尾数位为0,代表无穷大,按符号位分为正无穷大和负无穷大。

NaN

当阶码为 2047(每一位都为1),尾数位非全为0,代表 NaN。

以双精度为例探讨值范围

实数范围

规格数

  • 指数范围:阶码范围为 [1, 2046],所以指数范围为 [-1022, 1023]。

  • 小数范围:双精度中,尾数位数52位,所以尾数的十进制范围在 [0, i=1522i\sum_{i=1}^{52} 2^{-i}],通过等比数列求和可得尾数范围 [0, 1 - 252{2^{-52}}]。而由于浮点数存储时忽略了最开始的1,它对应10进制的值也是1,所以小数实际范围应该是 [1, 2 - 252{2^{-52}}]。注等比数列求和公式:a1anq1q\frac{a_1 - a_nq}{1-q}

所以双精度的最大值即为 小数范围最大值 x 指数范围最大值,故最大值为: 二进制小数 x 21023{2^{1023}} = 十进制小数 x 21023{2^{1023}} = (2 - 252{2^{-52}}) x 21023{2^{1023}} \approx 1.797693135 x 10308{10^{308}} 即双精度的最小负值:-1.797693135 x 10308{10^{308}},对应 js 中的 -Number.MAX_VALUE;最大正值:1.797693135 x 10308{10^{308}},对应 js 中的 Number.MAX_VALUE。

注:【二进制小数 x 2二进制指数{2^{二进制指数}}】 转十进制的方法: 【二进制小数对应的十进制小数 x 2二进制指数{2^{二进制指数}}

非规格数

在非规格数中,最小值的表示为 0 00000000000 0...1,对应的十进制数为 21022{2^{-1022}} x 252{2^{-52}} = 21074{2^{-1074}} = 5 x 10324{10^{-324}},它等于 js 中的 Number.MIN_VALUE。

实数范围总结

结合非规格数和规格数的实数范围,得出总的实数范围 [-1.797693135 x 10308{10^{308}}, -5 x 10324{10^{-324}}] \bigcup 0 \bigcup [5 x 10324{10^{-324}}, 1.797693135 x 10308{10^{308}}]。

以上范围内的实数存储时可能存在精度损耗问题,以下讨论没有损耗的整数范围。

无损整数范围

双精度存储整数时,并非所有整数都能精确存储,只有在某个范围内的整数才能精确存储,超出范围的整数可能存在精度损耗。整数存储时,先转二进制,再移位,最后把小数点后的数存入尾数部分。如十进制10,转二进制为1010,移位之后变为 1.010 * 23{2^3},然后将 010 存入尾数部分。由于双精度的尾数部分只有52位,所以当移位之后的小数部分长度大于52时,就会损失精度。当尾数长度 = 52 时,全部位为1时为最大无损整数,即 1.111...(52位),重新移位回整数再转成的十进制值应该为 253{2^{53}} - 1,即 9007199254740991,对应 js 中的 Number.MAX_SAFE_INTEGER。

综上,所以无损整数范围为 [-9007199254740991, 9007199254740991] === [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]

总结

js 中的数值都是依照 IEEE 754 标准,采用双精度进行存储。所以 js 的数值范围等同于双精度浮点数的范围,即:

  • 实数范围为: [-1.797693135 x 10308{10^{308}}, -5 x 10324{10^{-324}}] \bigcup 0 \bigcup [5 x 10324{10^{-324}}, 1.797693135 x 10308{10^{308}}]
  • 无损整数范围为:[-9007199254740991, 9007199254740991]

它们的值都存储在 js 的 Number 中,即:

  • 实数范围为:[-Number.MAX_VALUE, -Number.MIN_VALUE] \bigcup 0 \bigcup [Number.MIN_VALUE, Number.MAX_VALUE]
  • 无损整数范围为:[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]