浮点数的奥秘

 

浮点数在计算机中的存储格式以及精度问题

浮点数在计算机中的存储格式以及精度问题

浮点数的存储格式

在计算机中,一切内容最终都是以二进制保存的,浮点数也不例外,对于一个十进制整数,可以通过除二取余的方式来计算对应的二进制数

十进制数:10
10 / 2 = 5, 余0 => 0
5 / 2 = 2, 余1 => 10
2 / 2 = 1, 余0 => 010
1 / 2 = 0, 余1 => 1010

对于十进制的小数,要分成整数部分和小数部分,整数部分计算相同,小数部分通过乘二取整的方法

十进制数:10.625
小数部分:
0.625 * 2 = 1.25,整数部分为1,小数部分0.25 => 1
0.25 * 2 = 0.5,整数部分为0,小数部分0.5 => 10
0.5 * 2 = 1.0,整数部分为1,小数部分为0 => 101
所以二进制10.625为 1010.101

10.625可以通过有限的计算使得小数位为0,所以表示起来很简单,但对于10.33这种小数,二进制的小数部分会有无限位,而计算机能够存储的数据位数是有限的,所以就会存在舍入,带来精度损失

计算机中存储float类型通常有32位或64位,表示方法是V=(-1)^s * M * 2^E,其中,s是符号位,M是尾数(它代表了能够表示小数的精度),E是阶码

实际存储方式如下(E由k计算出,M由f计算出):

2gwJSJ.png

比如10.33的二进制表示:

32位:
0 10000010    01001010100011110101110
0 130         2443182
64位:
0 10000000010 0100101010001111010111000010100011110101110000101001
0 1026        1311673391471657

对于10.33这种可以规格化表示的数字,E = k - (2 ^ (kn-1) - 1)M = 1 + f/(2^fn),kn是k的位数,fn是f的位数

规格化与非规格化的区别可以参考《深入理解计算机系统》2.4.2

所以可以计算

公式:V = (-1)^s * M * 2^E
32位:
V = (-1)^0 * (1+2443182/2^23) * 2^(130 - 2^(8-1) - 1) = 10.32999992370605472

64位:
V = (-1)^0 * (1+1311673391471657/2^52) * 2^(1026 - 2^(11-1) - 1) = 10.33

可以看出64位表示法的精度更高

常见错误

因为存储会损失精度,所以在使用浮点数时要注意一些情况,比如在go语言中

fmt.Println(float32(1) == float32(0.99999999))

输出的结果是true,这显然是不对的

出现上面的错误的原因就是由于计算机中存储位数有限,所以可能造成两个不相等的数却有相等的二进制存储

对于32位浮点数,k有23位,2^23=8,388,608,所以最多能够保证6~7位的十进制精度

对于64位浮点数,k有52位,2^52=4,503,599,627,370,496,所以最多能够保证15~16位的十进制精度

测试一下:

fmt.Println(float32(1) == float32(0.9999999))           // 小数点后7位,false
fmt.Println(float32(1) == float32(0.99999999))          // 小数点后8位,true
fmt.Println(float64(1) == float64(0.9999999999999999))  // 小数点后16位,false
fmt.Println(float64(1) == float64(0.99999999999999999)) // 小数点后17位,true

所以在程序中一般不要使用浮点数进行相等比较