最近项目过程中,为了节省存储空间,使用float而不是double来存储浮点数,结果遇到了float溢出的问题。当时还觉得奇怪,float表示范围挺大的啊,有没有超范围,怎么还会溢出?后来试验并查证了float的存储结构,有了答案,原来是float的有效位丢失了。
先来看试验bug复盘
1 |
|
本来预期程序会输出220000000.0+8.0=220000008.0, 结果大相径庭。
程序输出
220000000.000000 + 8.000000 = 220000000.000000
结果说明,加不加8.0的结果不变,这种隐藏的bug真的是难以排除。查阅了相关资料之后原来是自己才疏学浅。
float的存储结构
任何数据在内存中都是以二进制的形式存储的, 包括float。 浮点数在二进制科学表示法中的表示为S=M*2^N, 由三部分组成,符号位+阶码(N)+尾数(M)。float 二进制32位,其中符号位1位,阶码8位,尾数23位。
| 31 | 30-23 | 22-0 | |
|---|---|---|---|
| float | 符号位 | 阶码 | 尾数 |
符号位,0表示正,1表示负
阶码:8位阶码N=[-127,127], 因为指数可正可负,所以指数部分的存储采用移位存储,存储的数据为元数据+127
尾数:有效数字位,即部分二进制位(小数点后面的二进制位),因为规定M的整数部分恒为1,所以这个1就不进行存储了,即S=1.xxx*2^N
举例:
125.5的float标准浮点格式,用二进制表示为1111101.1=1.1111011*2^6, 则阶码为6,加上127为133,则表示为10000101,而对于尾数将整数部分1去掉,为1111011,在其后面补0使其位数达到23位,则为11110110000000000000000
所以125.5的float存储二进制为0 10000101 11110110000000000000000
反过来若要根据二进制形式求算浮点数如0 10000101 11110110000000000000000
由于符号为为0,则为正数。阶码为133-127=6,尾数为11110110000000000000000,则其真实尾数为1.1111011。所以其大小为1.1111011*2^6,将小数点右移6位,得到1111101.1,而1111101的十进制为125,0.1的十进制为1*2^(-1)=0.5,所以其大小为125.5
由上分析可知float型数据最大表示范围为1.11111111111111111111111*2^127=3.4*10^38
float有效位丢失
因为尾数为23+1=24位,如果数据尾数多于24位,就会造成精度丢失。
以220000000.0为例, 220000000.0=1101 0001 1100 1110 1111 0000 0000.0=1.101 0001 1100 1110 1111*2^27
其中阶码为27,移位码为27+127=154=10011010 ,尾数为101 0001 1100 1110 1111,该数字的二进制表示为 0 10011010 10100011100111011110000
同理 220000000.0+8.0=220000008.0 的阶码的移位码为10011010,尾数为101 0001 1100 1110 1111 0000 1000,由于尾数为27位,有效位只有前23位,所以有效尾数为101 0001 1100 1110 1111 0000
所以220000008.0的二进制编码为0 10011010 10100011100111011110000, 跟220000000.0 是一样的,符合结果预期。
再做一个实验证实一下,220000000.0+16.0=2200000016.0 在float下应该是正确的
1 |
|
输出结果符合预期
220000000.000000 + 16.000000 = 220000016.000000
具体原因可以自己推导一下
PS: float会有精度损失问题,比如本来存的float f1=220000008,实际真实值为220000000