浮点数在计算机中的表示和精度误差

先看误差实例

1
2
3
4
5
6
7
8
9
	System.out.println(88.82 + 10.85);
System.out.println(8.82 + 10.85);
double f1 = (3.14 + 10000000000L) - 10000000000L;
double f2 = 3.14 + (10000000000L - 10000000000L);
System.out.println(f1 + "===" + f2);
//output
99.66999999999999
19.67
3.1399993896484375===3.14

浮点数表示

N=S*R^J

S代表尾数,R基数,j阶码

在计算机中,规定浮点数表示尾数用纯小数,并且尾数最高位为1,这样的浮点数精度最高。

image

IEEE 754标准

根据常用计算机浮点数表示标准,以下是各种类型表示占比

image

1
2
3
4
5
	System.out.println(88.82 + 10.85);
System.out.println(8.82 + 10.85);
//output
99.66999999999999
19.67

根据误差实例,直接上浮点数运算,以上都是double长实数,化成二进制表示法:

88.82转二进制数学方法:

参考

image

整数部分的转化,用辗转相除法,倒叙得到88代表的 二进制1011000(上图88多了一个0,错误)

小数部分的转化,用辗转相乘法,正序得到0.82二进制0.1101001110….后面就不再算了

十进制小数化成二进制,具体是乘以二如果整数部分为一,就取出来,一直乘到小数为零

image

十进制转二进制的代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 整数部分 十进制转二进制
* @param n
* @return
*/
public static String fun(int n){
StringBuilder stringBuilder=new StringBuilder();
while(true){
int r=n%2;
stringBuilder.append(r+"");
n=n/2;
if (n==0) {
return stringBuilder.reverse().toString();
}

}
}
/**
* 小数部分 十进制转二进制
* @param n
* @return
*/
public static String fun2(double n){
StringBuilder stringBuilder=new StringBuilder();
stringBuilder.append("0.");
while(true){
if (n==0) {
return stringBuilder.toString();
}
n*=2;
if (n>=1) {
stringBuilder.append("1");
n=n-1;
}else{
stringBuilder.append("0");
}
System.out.println(stringBuilder.toString());

}
}

由于0.82转化成二进制比较长,浮点数存在精度问题,会被舍去部分,通过代码算出0.82二进制

0.11010001111010111000010100011110101110000101000111101

所以88.82化成二进制

1011000.11010001111010111000010100011110101110000101000111101

再由上面的IEEE标准表格,转化成规格化二进制表示法得出

88.82 阶码(1阶符 11阶值)0000 0000 0110

第一个0代表阶符为正数,剩下七位代表6因为规格化后,小数点向左平移6位

88.82尾数(1尾符 尾值)0101 1000 1101 0001 1110 1011 1000 0101 0001 1110 1011 1000 0101

第一个0代表数符,剩下代表精度尾数,从这里看出这个浮点数的精确值受到了舍弃

最后0000 0000 0110 0101 1000 1101 0001 1110 1011 1000 0101 0001 1110 1011 1000 0101

同理可以用代码计算其他浮点数转化成机器数的表示方法

任意实数十进制转二进制
1
2
3
4
5
6
7
8
9
10
11
12

/**
*
* @param n 输入一个实数可以大于1
* @return 输出 二进制
*/
public static String fun3(double n){
//强转取整数部分
String part1=fun((int)n);
String part2=fun2(n-(int)n);
return part1+part2.substring(1,part2.length());
}
双精度浮点数表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 输入是一个二进制字符串
* 输出是一个阶码+尾数的64bit浮点数表示
*
* @param s
* @return
*/
public static String fun4(String s){
//小数点位置
int pointIndex=s.length();
int d=s.indexOf(".");
if (d!=-1) {
pointIndex=d;
}
//找到第一个1位置
int first=s.indexOf("1");
if (first==-1) {
return "0000 0000 0000 0000 0000 0000".replaceAll(" ", "");
}
//阶符
String jf=pointIndex-first>0?"0":"1";
//阶值
int a=pointIndex-first-1;
String jm=fun(a);
//填充 不考虑阶值溢出
while(jm.length()<11){
jm="0"+jm;
}
//阶码部分
String jString=jf+jm;
//数符 不考虑负数
String sf="0";
//尾数
s=s.replace(".", "");
String sm=s.substring(first, Math.min(s.length(), 51+first));
return jString+sf+sm;
}
public static void main(String[] args) {
String string=fun3(8.82);
System.out.println(fun4(string));
}

//output
0000000000110100011010001111010111000010100011110101110000101001

用以上代码得出8.82的浮点数表示

0000000000110100011010001111010111000010100011110101110000101001

88.82

0000000001100101100011010001111010111000010100011110101110000101

10.85

0000000000110101011011001100110011001100110011001100110011001100

浮点数运算

当阶码不同的时候,浮点运算不能直接进行加法运算,第一步需要对阶,然后尾数求和,规格化,舍入

对阶,判断溢出。求出阶差,小阶向大阶看齐。尾数右移,尾数可能会丢失,精度受到影响。

计算88.82+10.85

求两者的补码,因为都是正数,补码相同

88.82补码:

0000 0000 0110; 0101100011010001111010111000010100011110101110000101

10.85补码:

0000 0000 0011; 0101011011001100110011001100110011001100110011001100

对阶(分号前为阶码)

阶差:

0000 0000 0110-0000 0000 0011

等价于(-0000 0000 0011的补码是1111 1111 1101)(注:其实直接减答案也一样,但是如果被减数小于减数就要这种方法)

0000 0000 0110+1111 1111 1101

结果

0000 0000 0011

这是一个正值,说明要把10.85向高阶移动三位,尾数部分除了符号位右移三位,变成:

10.85

0000 0000 0110; 0000101011011001100110011001100110011001100110011001

可以看出,前面补领三位,后面舍去三位,精度损失

尾数求和

计算10.85和88.82的尾数

0000 1010 1101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001

+

0101 1000 1101 0001 1110 1011 1000 0101 0001 1110 1011 1000 0101

=

0110 0011 1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1110

规格化

尾数部分值在0.5到1之间,已经是规格化,无需变化

判断溢出

两个补码相加没有溢出

最终结果

0000 0000 0110;0110 0011 1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1110

99.66999999999999

整数部分:

110 0011->99

小数部分->1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1110

代码判断

1
2
3
4
5
6
7
8
9
	System.out.println("1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1110".replaceAll(" ", ""));
System.out.println(fun3(0.66999999999995).substring(2));
System.out.println(fun3(0.66999999999996).substring(2));
System.out.println(fun3(0.66999999999999).substring(2));
//output
10101011100001010001111010111000010100011110
1010101110000101000111101011100001010001110111010111
10101011100001010001111010111000010100011110000001
10101011100001010001111010111000010100011110100010111

因为我也不知道怎么把1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1110转正十进制小数,所以通过把0.66999999999999转化成二进制来比较答案来看不同。

到这一步,发现精度还是有不对的。内存实际的数值更接近于0.66999999999996

是哪里出错了?

10.85对阶之后,尾数舍去了三位影响了精度,如果舍去采用0舍1入会出现什么结果?

那么10.85舍去之后变成

0000 0000 0110; 0000101011011001100110011001100110011001100110011010

(末尾本来是1,刚好舍去的也是110入一位)

再计算尾数求和

0000 1010 1101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

+

0101 1000 1101 0001 1110 1011 1000 0101 0001 1110 1011 1000 0101

=

0110 0011 1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1111

所以最终结果变成

0000 0000 0110;0110 0011 1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1111

此时代码

1
2
3
4
5
6
7
8
9
10
11
System.out.println("1010 1011 1000 0101 0001 1110 1011 1000 0101 0001 1111".replaceAll(" ", ""));
System.out.println(fun3(0.66999999999995).substring(2));
System.out.println(fun3(0.66999999999996).substring(2));
System.out.println(fun3(0.66999999999999).substring(2));
System.out.println(fun3(0.67).substring(2));
//output
10101011100001010001111010111000010100011111
1010101110000101000111101011100001010001110111010111
10101011100001010001111010111000010100011110000001
10101011100001010001111010111000010100011110100010111
10101011100001010001111010111000010100011110101110001

可以看出内存中的数最接近0.66999999999999

同理计算8.82+10.85

8.82

0000 0000 0011;0100011010001111010111000010100011110101110000101001

10.85

0000 0000 0011;0101011011001100110011001100110011001100110011001100

对阶,发现对阶一致,那么直接尾数相加

0100 0110 1000 1111 0101 1100 0010 1000 1111 0101 1100 0010 1001

+

0101 0110 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100

=

01001 1101 0101 1100 0010 1000 1111 0101 1100 0010 1000 1111 0101

出现了溢出此时,规格化后尾数0舍去1入得到最终

0000 0000 0100;(阶码加1)

0100 1110 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1011

尾数入1

最终值

0000 0000 0100;0100 1110 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1011

1
2
3
4
5
System.out.println("10 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1011".replaceAll(" ", ""));
System.out.println(fun3(0.67).substring(2));
//output
1010101110000101000111101011100001010001111011
10101011100001010001111010111000010100011110101110001

总结

上述内容可能具体还会有错,因为不知道怎么把像

1010101110000101000111101011100001010001111011(52bit)变成0.67.

因为当把0.67化成一个二进制是

10101011100001010001111010111000010100011110101110001(59bit)

但是明白的是,浮点数运算的大致过程,和其中计算过程中的精度损失问题。当两个数相差越大因为要对阶而对齐导致的精度遗失是主要原因。