在 JS 中学习 0.1 + 0.2 == 0.30000000000000004 的问题时候,引申出来浮点数的学习。

什么是浮点数

浮点数的意思是指,小数点的位置是漂浮不定的。浮点数是采用科学计数法表示的。如十进制的小数 1.234 用不同的科学计数法表示:

  • 1.234 = 1.234 * 10^0
  • 1.234 = 12.34 * 10^-1
  • 1.234 = 123.4 * 10^-2

格式可以写成:V = (-1)^S * M * R^E

  • S: 符号位,取值 0 或 1,决定数字的符号,0 表示正,1 表示负
  • M: 尾数,用小数表示,如 1.234 * 10^0,1.234 就是尾数
  • R: 基数,表示十进制时,基数 R 就是 10,表示二进制的时,基数 R 就是 2
  • E: 指数,用整数表示,例如 10^-2,-2 就是指数

IEEE 754 编码规范

IEEE 754 提供了 4 个精度级别的浮点数定义:单精度,双精度,扩展精度和可扩展精度。JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。

  • 单精度浮点数:32 位,符号位占 1 位,指数占 8 位,尾数占 23 位
  • 双精度浮点数:64 位,符号位占 1 位,指数占 11 位,尾数占 52 位

每个浮点数只有一种二进制交换格式的编码。

IEEE 754 的二进制编码由 3 部分组成,分别是:

  • sign(符号位)0 表示正,1 表示负,占 1bit。
  • based exponent(基于偏移的阶码域)
  • fraction(尾数)

float_point_format

同时还规范:

  1. 尾数的第一位总是 1,因此这个 1 可以省略不写,它是个隐藏位
  2. 指数是个无符号整数,指数可以负的,规定指数在原本的值加中间数。8 bit 的中间数是 127(0111 1111),11 bit 的中间数是 1023(011 1111 1111)
  3. 当存储空间无法存储完整的无限循环小数,IEEE 754 采用 round to nearest, tie to even(舍入到最接近可以表示的值,优先取偶数) 的舍入模式

我们试着将 25.125 转换成单精度的浮点数

  1. 整数部分:25(D) = 11001(B)
  2. 小数部分:0.125(D) = 0.001(B)
  3. 二进制科学计数法表示:11001.001(B) = 1.1001001 * 2^4(B)
  4. 符号位为 0,指数 4 + 127 = 131(D) = 1000 0011(B),尾数去掉隐藏位 1 后为 1001001

最后的结果为 0|10000011|10010010000000000000000

浮点数的运算

浮点数的加减运算一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。

对阶

对阶是指将两个运算的数的阶码对齐的操作,对阶是为了让两个浮点数的尾数能够进行加减运算。

对阶的主要方法:修改小的阶码使其与大的阶码相等,并将对应的尾数右移相应的位数。

尾数运算

对阶完成之后,我们将尾数进行相加减得到最后的尾数。

规范化

相加减之后得到的结果,尾数可能非规范化的形式,我们需要将其进行规范化。

舍入方式

舍入方式参考:IEEE754规范的舍入方案怎么理解呢?

根据 Boss呱呱 的总结大致:

  • ”最近“原则其实是”损失精度最小“原则
  • “偶数”原则其实是”舍入后保留的最低有效位是偶数(二进制表示则是0)“原则

具体例子和讲解可以参考原回答。我们这里也以下面的 0.2 的尾数 10011…0011001(10011) 作为例子

  • 首先我们的有效位为 10011…0011001,舍入部分为 (10011)
  • 向上舍入的 10011…0011010,精度损失为 0.000…0(01101),向下舍入为 10011…0011001,精度损失 0.000…0(10011)
  • 向上舍入的精度更小,所以我们向上舍入,结果为 10011…0011010

第二个原则则是当损失精度一样时,我们优先选择偶数。如 1.01101 舍入到 4 位,向上舍入 1.0111,损失精度 0.00001,向下舍入为 1.0110,损失精度也是 0.00001,这时优先选择偶数,采用向下舍入,结果为 1.0110

溢出判断

将最终的结果进行溢出判断,判断是否超过浮点数所能表示的最大数指。

0.1 + 0.2

让我们回到最初的 0.1 + 0.2 的问题上,主要原因是因为 0.1 和 0.2 在转成 IEEE 754 标准浮点数的二进制上会有精度损失。

0.2 转换为二进制的过程是不断乘以 2,直到不存在小数为止。

0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
...(开始循环)

所以我们得到 0.2(D) = 0.00110011…(B), 根据上面的计算方法等到双精度

  1. 整数部分为 0(D) = 0(B)
  2. 小数部分为 0.2(D) = 0.00110011…(B)
  3. 二进制科学计数法表示:0.00110011…(B) = 1.100110011…(B) * 2^-3(B)
  4. 符号位为 0,指数 -3 + 127 = 124(D) = 011 1111 1100(B),尾数去掉隐藏位 1 后为 100110011…(B)
  5. 由于其舍入方式,尾数 10011…0011001(10011) 成 10011…0011010

最后的结果为 0|011 1111 1100|10011...0011010

同理得到 0.1 的结果为 0|011 1111 1011|10011...0011010

实际存储的位模式作为操作数进行浮点数加法,得到 0|011 1111 1101|00110..0110100, 得到结果为 0.30000000000000004

参考链接