浮点数加减法计算导致精度损失的问题
示例
来看一段简单的java代码:
public class Solution {
public static void main(String[] args) {
double a = 398.92;
double b = 1282.04;
double c = 2167.38;
double d = 3284.94;
double sum = a+b+c+d;
System.out.println(sum);
}
}
猜一猜计算的结果是什么?
7133.280000000001 7133.280000000001 7133.280000000001
很震惊, 为什么小数点后两位的小数相加会得到小数点后多位的小数呢? 按理说无论多少个小数点后两位的小数相加最后的和都不会产生多于小数点后两位的小数
这是编程语言导致的问题吗?
再来看一下在python语言中的结果
a = 398.92
a += 1282.04
a += 2167.38
a += 3284.94
print(a)
从运行结果来看, 和Java运行的结果是一毛一样。
其实浮点数的精度损失问题并不是编程语言的特有问题,而是由于浮点数的二进制表示方式和计算机硬件设备的局限性所导致的。无论使用什么编程语言,都无法完全避免浮点数的精度损失问题,只能采用一些方法来减少误差或提高精度。
分析
那问题出在了哪里呢?
就拿Python中浮点数加减法来说吧,Python中的浮点数类型float,是按IEEE 754标准来存储的。该标准对浮点数的精度做了规定:
- 使用二进制来表示一个浮点数,包括符号位、指数位和小数位(尾数位)
- 尾数位数是有限的,一般为53位(64位系统可达64位)
- 指数部分也有位数限制,一般为11位(64位系统可达15位)
IEEE 754浮点数标准定义的浮点数格式:
通常在CPU和浮点数运算单元中,会定义二进制小数的位数和范围。比如:
- 单精度float 32:
- 总位数:32位
- 小数位数:23位
- 双精度doublet 64:
- 总位数:64位
- 小数位数:52位
这就决定了float类型在存储大数值时会损失精度。当进行float加法时,其实是两个二进制浮点数相加,然后将结果重新规格化到浮点数表示范围内。这个转化过程会造成精度损失。
举个栗子:
0.1 + 0.2
二进制表示大约为:
0.1 -> 0.000110011001100110011010
0.2 -> 0.001100110011001100110010
在计算机系统中,考虑到精度和存储空间,我们往往会选择截断一定位数的二进制数字来代表0.1:
- float类型通常截断为24位二进制数字
- double类型通常截断为52位二进制数字
这足够满足大多数计算的精度需求。比如双精度float64使用64位(1位符号位,11位指数位,52位小数位)。但从数学意义上,它们的二进制表示应该是无限位数的。
0.000110011001100110011010 + 0.001100110011001100110010
相加结果为:
0.010011001100110011010010
转换回十进制后大约为:
0.30000000000000004
所以 0.1 + 0.2 ! = 0.3 0.1 + 0.2 != 0.3 0.1+0.2!=0.3 , 减法也存在类似的精度问题。
这就是Python浮点数加减法的底层原理导致的精度问题。需要使用定点数或保留规定位数来避免。
为什么要限定二进制小数的位数呢?
主要原因有:
- 简化浮点数存储设计,按照固定位数分配存储空间
- 提高计算效率,按照固定位数进行浮点数计算
- 减少资源占用,避免使用过多存储和计算资源
- 符合计算机系统的字长限制,如32位或64位
位数的限制也会导致一定的计算误差,这就是浮点数计算中"舍入误差"的根源。但这是为了在有限的计算机系统中进行浮点数运算时需要做的必要折衷。
解决方案
怎么避免浮点数的计算精度丢失呢?
- 使用更高精度的浮点数格式
例如,双精度float64相比单精度float32有更多的小数位数,可以减少精度损失。
- 避免进行可能导致精度损失的运算
如避免递增计算、避免除后再乘法等。
- 使用定点数代替浮点数
定点数存储方式可以完全避免舍入误差。定点数(Fixed Point Number)是一种用于表示实数的数字表示方法。
-
定点数的特点是小数点的位置是固定的,这与浮点数不同,浮点数中的小数点位置是可变的。
-
小数点的位置是预先指定的,并且固定不变。
-
整数位数和小数位数是预先指定的。
-
每一位数字都代表一个绝对值,不会根据小数点位置改变。
-
定点数的数值范围和精度是有限的。
-
运算过程中不需要浮点运算的规格化和舍入操作。
-
存储空间需求固定,不需要动态调整。
-
运算速度快,但是表示范围和精度有限。
-
相比浮点数,定点数计算结果完全精确,不会有舍入误差,但范围和精度受限
- 使用专门的高精度计算库
比如:python中使用Numpy中的decimal模块可以进行高精度浮点数运算。
- 余数定理保留精度
对运算结果取余数,可以消除舍入误差的影响。
-
检查运算结果,如果检测到精度损失再增加精度重新计算。
-
输出结果时舍入位数少一些,隐藏部分精度损失。
那又回到开头, 怎样才能在代码中避免浮点数的精度丢失呢?
为了避免这个代码中的浮点数相加出现精度丢失,我们可以这样修改:
- 在Java中使用
BigDecimal
类来表示浮点数,它可以任意精度地表示浮点数,不会有精度损失。 - 使用字符串来定义
BigDecimal
,避免从double转换时出现误差。 - 使用
BigDecimal
的add方法进行相加。
import java.math.BigDecimal;
public class Solution {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("398.92");
BigDecimal b = new BigDecimal("1282.04");
BigDecimal c = new BigDecimal("2167.38");
BigDecimal d = new BigDecimal("3284.94");
BigDecimal sum = a.add(b).add(c).add(d);
System.out.println(sum);
}
}
这样通过使用BigDecimal来进行浮点数运算,可以避免精度丢失问题,得到完全准确的结果。
其他语言也有对应的解决方案
Python语言也不例外:
在Python中,可以通过使用decimal
模块来避免浮点数运算的精度损失:
from decimal import Decimal
a = Decimal('398.92')
a += Decimal('1282.04')
a += Decimal('2167.38')
a += Decimal('3284.94')
print(a)
Decimal类可以对浮点数进行任意精度的运算,内部采用的是定点数的表示方法,没有丢失精度的问题。
关键是要使用Decimal类初始化浮点数,然后进行算术运算的时候也要用Decimal的方法。正常的float类是存在精度问题的,decimal模块的Decimal类可以很好地解决这个问题。
另外,也可以指定Decimal的精度和四舍五入模式,来控制运算的精确程度。使用decimal可以大大提高Python中重要浮点数运算的准确性。