浮点型数据的精度问题

如何表示定点数

使用二进制来表示十进制的编码称为 BCD 编码。 这种编码方式把小数点固定在某一位,所以其表示的数称为 定点数

那么 4 个比特最大的表示是十进制数为 9 (1001)。

32 bit 使用右侧的 2 个 0-9 的整数(8 bit),当成小数部分,左侧的6 个 0-9 表示整数部分(24 bit),那么 32 bit 可以表示 0到 999999.99 这样的数组范围。

缺点:

  • 极大的浪费。本来 4 bit 可以表示 0-15,现在只可以表示 0-9。
  • 无法表示很大和很小的数。

如何表示浮点数

  • 32 bit 表示单精度的浮点数,float 或 float32。
  • 64 bit 表示双精度的浮点数,double 或 float64。

以单精度为准看如何表示浮点数

s:符号位 e:指数位 f:有效数位
1 bit 8 bit 23 bit

e:8 bit 组成的指数位,表示整数范围 0254,其中 1254 映射到带符号的整数 -126~127,这样浮点数不仅可以表示很大的数,也可以表示很小的数。

最终浮点数表示为:
(-1)^s×1.f×2^e (e = 指数位对应的整数 -127)

关于 e 为什么要这样计算查看 浮点数的二进制表示–阮一峰 中关于 IEEE-754 的相关内容。

以 0.5 为准:

0.5=(−1)^0×1.0×2^−1=0.5

浮点数的二进制表示

十进制浮点数二进制表示:9.1

9:1001

小数部分转换成二进制使用乘法操作,就是乘以 2,然后看看是否超过 1。如果超过 1,我们就记下 1,并把结果减去 1,进一步循环操作。

1
2
3
4
5
0.1:000110011...(0011 无限循环)

9.1:1001.000110011,使用科学计数法,将小数点向左移三位,表示如下:

1.001000110011..x2^3

匹配浮点数表示的公式:(-1)^s×1.f×2^e

s=0、f=001000110011( 0011 循环至23位)、e=3 (根据指数位计算方式 指数位对于的十进制整数为 +3+127 = 130,对应二进制为 01111110),所以 9.1 在 32bit 下的二进制表示为:

在一位的小数中,只有 0.5 可以精确表示:

其他均为近似表示,这就是为什么浮点数计算出现精度问题的原因。

浮点数的加法与精度丢失

浮点数的加法规则:先对齐,再计算

其中对齐是将两个浮点数的指数位对齐,即通过位移使指数位相同。

1
2
3
4
5
6
7
8
0.5 + 0.125:

0.5:1.0x2^(-1)
0.125: 1.0x2^(-3)=0.01x2^(-1)

1.0x2^(-1) + 0.01x2^(-1) = 1.01x2^(-1)

1.01x2^(-1) 的十进制表示为 0.625

在加法前,浮点型数据的二进制表示就可能会发生精度丢失,如果相加的两个数相差比较大,那么在指数对齐过程中,有可能会丢失有效位,位移就会越大,那么丢失的精度就越大,那么产生的误差也会越大。但是如果丢失的全为 0 ,那么加法的数值不会有精度丢失。

如何避免精度损失

Kahan Summation 算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class KahanSummation {
public static void main(String[] args) {
float sum = 0.0f;
float c = 0.0f;
for (int i = 0; i < 20000000; i++) {
float x = 1.0f;
float y = x - c;
float t = sum + y;
c = (t-sum)-y;
sum = t;
}
System.out.println("sum is " + sum);
}
}

使用 BigDecimal

BigDecimal 是专门为弥补浮点数无法精确计算的缺憾而设计的,它本身提供了加减乘除。

使用整型

如果保留小数点后 2 位,那么将参与运算的值扩大 100 倍 并转换为整型,在展示时再缩小 100 倍。