float有效位丢失

 

最近项目过程中,为了节省存储空间,使用float而不是double来存储浮点数,结果遇到了float溢出的问题。当时还觉得奇怪,float表示范围挺大的啊,有没有超范围,怎么还会溢出?后来试验并查证了float的存储结构,有了答案,原来是float的有效位丢失了。

先来看试验bug复盘

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
float f1 = 220000000, f2 = 8.0;
printf("%f + %f = %f\n", f1, f2, f1 + f2);
return 0;
}

本来预期程序会输出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
2
3
4
5
6
7
#include <stdio.h>
int main()
{
float f1 = 220000000, f2 = 16.0;
printf("%f + %f = %f\n", f1, f2, f1 + f2);
return 0;
}

输出结果符合预期

220000000.000000 + 16.000000 = 220000016.000000

具体原因可以自己推导一下

PS: float会有精度损失问题,比如本来存的float f1=220000008,实际真实值为220000000

 上一頁

算法