浮点数
2021 javascript os在 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(尾数)
同时还规范:
- 尾数的第一位总是 1,因此这个 1 可以省略不写,它是个隐藏位
- 指数是个无符号整数,指数可以负的,规定指数在原本的值加中间数。8 bit 的中间数是 127(0111 1111),11 bit 的中间数是 1023(011 1111 1111)
- 当存储空间无法存储完整的无限循环小数,IEEE 754 采用 round to nearest, tie to even(舍入到最接近可以表示的值,优先取偶数) 的舍入模式
我们试着将 25.125 转换成单精度的浮点数
- 整数部分:25(D) = 11001(B)
- 小数部分:0.125(D) = 0.001(B)
- 二进制科学计数法表示:11001.001(B) = 1.1001001 * 2^4(B)
- 符号位为 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), 根据上面的计算方法等到双精度
- 整数部分为 0(D) = 0(B)
- 小数部分为 0.2(D) = 0.00110011…(B)
- 二进制科学计数法表示:0.00110011…(B) = 1.100110011…(B) * 2^-3(B)
- 符号位为 0,指数 -3 + 127 = 124(D) = 011 1111 1100(B),尾数去掉隐藏位 1 后为 100110011…(B)
- 由于其舍入方式,尾数 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