一、前言
我们常能听到,直接用浮点数做运算得出的结果是不准确的了;或者也能看到涉及到浮点数时,会出现一些奇奇怪怪的问题,比如:
public class DecimalTest {
public static void main(String[] args) {
float f1 = 1.2002f;
float f2 = 100000.2002f;
System.out.println(f1);
System.out.println(f2);
}
}
=============
输出:
1.2002
100000.2
那么这些现象的底层原理是什么呢?这需要从浮点数在计算机中的编码方式说起。
二、IEEE浮点标准
表示方式
IEEE浮点标准是广泛使用的浮点数编码方式,其用下面的方式表示一个数(和10进制的科学计数法是几乎相同的):
- 符号位 s,s = 0为正数,s = 1为负数
- 尾数 M,一个二进制小数,取值范围一般为
- 阶码 E,表示 2 的 E 次幂
储存方式
在实际存储浮点数时,M和E做了些特殊的编码处理,下面以32位的单精度浮点数为例(即编程语言中的float)。
- 31位的 s 部分:符号位 s,s = 0为正数,s = 1为负数
- 30-23位的 exp 部分:由阶码 E 转换而来,转换方式(其中 k 为此部分的 bit 数,即8):
e(编码值) = Bias(偏移量) + E(阶码) = 2^(k-1) - 1 + E = 127 + E - 22-0位的 frac 部分:M 的小数部分,即 M-1
补充说明:
- 上述为常用的“规格化的值”的实现方式,实际上完整的IEEE标准包括 规格化的值、非规格化的值、特殊值3种情况
- 三种情况通过 exp 部分既不全为0也不全为1、全为0、全为1进行区分
- 规格化的值用于表示普通常用的值,非规格化的值用于表示非常接近于0的数,特殊值用于表示无穷、非合法数字等情况
三、开头的问题
为什么 100000.2002f 写入 float 后,发生精度丢失变成了 100000.2?
- 100000.2002 的二进制原码为 11000011010100000.001100110100000001......
- 转成 IEEE 编码,
- 即s=0,exp=127+16=143=10001111,farc=10000110101000000011001(1)=10000110101000000011010(向偶数舍入)。
- 最终的编码为:0-10001111-10000110101000000011010,对应的数值为:1.10000110101000000011010*2^16 = 11000011010100000.0011010 = 10000.203125
四、其他
- 快速判别一个小数是否可被二进制精确表示:该小数是否可表示为以 为分母的分数
- 主要参考资料:《深入理解计算机系统》第二章,2.4