为什么0.1加0.2不一定等于0.3:浮点数精度原理与正确用法
在很多编程语言中执行下面的代码:
print(0.1 + 0.2)
得到的结果可能是:
0.30000000000000004
继续比较:
print(0.1 + 0.2 == 0.3)
结果可能是:
False
这并不是计算机连简单加法都算不对,也不是某种编程语言存在缺陷,而是浮点数表示方式带来的正常现象。
计算机通常使用有限位二进制保存小数,而许多十进制小数无法被有限位二进制精确表示。程序只能保存一个非常接近原值的近似数,计算后误差就可能显示出来。
一、十进制小数为什么能精确表示0.1
在十进制中:
0.1 = 1 ÷ 10
它只需要一位小数就能准确表示。
但不是所有分数都能用有限位十进制表示。例如:
1 ÷ 3 = 0.333333……
小数部分会无限循环。
一个最简分数能否在十进制中有限结束,取决于其分母是否只包含2和5这两类质因数。
例如:
1 ÷ 2 = 0.5
1 ÷ 4 = 0.25
1 ÷ 5 = 0.2
1 ÷ 10 = 0.1
这些分母最终都可以分解成2和5的乘积,因此能用有限位十进制表示。
而:
1 ÷ 3
1 ÷ 7
不能用有限位十进制精确表示。
二、二进制也有无法有限表示的小数
计算机通常使用二进制保存浮点数。
二进制小数每一位表示的是:
1/2、1/4、1/8、1/16……
例如:
0.1₂ = 1/2
0.01₂ = 1/4
0.001₂ = 1/8
十进制的0.5可以精确表示为:
0.1₂
十进制的0.25可以表示为:
0.01₂
十进制的0.125可以表示为:
0.001₂
但十进制0.1对应分数:
1/10
分母包含质因数5,而二进制有限小数只能精确表示分母为2的幂的分数。
因此,十进制0.1转换成二进制后会成为无限循环小数,只能截取或舍入到有限位:
0.00011001100110011……
这和十进制无法有限表示三分之一是同一种问题,只是使用的进位制不同。
三、计算机实际保存的并不是0.1
当程序写下:
value = 0.1
如果使用常见二进制浮点类型,计算机保存的通常不是数学意义上精确的0.1,而是最接近0.1的可表示值。
可以把它理解成:
希望保存:0.1
实际保存:0.100000000000000005……
同样,0.2和0.3也可能分别被保存成略大或略小的近似值。
执行加法时,计算机相加的是这些近似值,而不是纸面上的精确十进制数。
因此:
0.1的近似值
+ 0.2的近似值
= 0.30000000000000004附近
而单独写下的0.3又会被转换成另一个最接近0.3的浮点数。两边的最终位模式不完全相同,直接比较就可能得到不相等。
四、浮点数为什么叫“浮点”
定点数会预先约定小数点位置。
例如,使用整数保存金额,并规定最后两位表示小数:
1999 → 19.99元
小数点位置固定,所以称为定点。
浮点数则类似科学计数法:
1.2345 × 10³
数字由有效部分和指数共同表示,小数点可以随指数移动,因此称为浮点数。
在二进制中,可以表示为:
有效数字 × 2的指数次方
这种方式能够同时表示非常大和非常小的数,但只能保留有限数量的有效数字。
五、IEEE 754浮点数的基本结构
现代计算机通常采用IEEE 754浮点标准。
一个二进制浮点数主要由三部分组成:
符号位
指数
尾数或有效数字部分
对于常见的32位单精度浮点数:
1位符号
8位指数
23位小数部分
对于常见的64位双精度浮点数:
1位符号
11位指数
52位小数部分
由于规格化表示通常隐含一个最高有效位,双精度浮点数实际上拥有约53位二进制有效精度。
它大约能够提供15到16位十进制有效数字,但这并不意味着小数点后固定有15位可靠数字。
精度描述的是整体有效数字数量,而不是固定的小数位数。
六、float和double有什么区别
许多语言提供两种常见浮点类型。
float
通常是32位单精度浮点数。
它大约提供:
6到7位十进制有效数字
适合:
- 图形渲染;
- 对精度要求不高的传感器数据;
- 大规模数组;
- 需要节省内存的计算。
double
通常是64位双精度浮点数。
它大约提供:
15到16位十进制有效数字
适合:
- 通用科学计算;
- 统计分析;
- 大多数工程计算;
- 需要更高数值稳定性的场景。
double精度更高,但仍然是有限精度的二进制浮点数,依然不能精确表示0.1这类十进制小数。
七、显示正常不代表内部精确
程序执行:
print(0.1)
通常只显示:
0.1
这是因为输出函数会选择一种更适合人阅读的格式,并隐藏无意义的多余数字。
内部保存值仍然可能是:
0.100000000000000005……
如果使用更高精度格式输出,就可能看到近似值:
print(format(0.1, ".20f"))
可能显示:
0.10000000000000000555
因此:
显示为0.1
并不代表:
内存中保存的是数学上精确的0.1
格式化只能改变显示方式,不能提高已经存储的数据精度。
八、浮点数为什么不满足结合律
在数学中:
(a + b) + c = a + (b + c)
但浮点计算每一步都可能发生舍入,所以不同计算顺序可能得到不同结果。
假设:
a = 一个非常大的数
b = 一个较小的数
c = a的相反数
计算:
(a + b) + c
时,b可能因为相对于a太小而在第一次加法中被舍弃,结果接近0。
而计算:
a + (b + c)
时,中间值和舍入路径不同,结果可能保留b的一部分。
因此,在并行计算中,即使输入相同,只要求和顺序发生变化,最后几位也可能不同。
这也是为什么科学计算中不能总是假设不同硬件、线程数和执行顺序会产生完全一致的最后一位结果。
九、什么是灾难性消减
两个非常接近的浮点数相减时,前面的有效数字会大量抵消,剩余结果可能只保留原误差。
例如:
1.23456789
- 1.23456788
理论结果很小。
如果两个输入本身已经包含舍入误差,相减后,有效信息可能大幅减少。这种现象称为灾难性消减。
它常见于:
- 数值微分;
- 统计方差计算;
- 求解方程;
- 几何运算;
- 信号处理;
- 大数之间的小差值。
解决这类问题通常需要重新组织公式,而不只是换成更多小数位输出。
十、浮点误差会不会不断累积
会,但具体程度取决于算法。
例如,连续一百万次累加一个不能精确表示的小数,舍入误差可能逐步积累:
total = 0.0
for _ in range(1000000):
total += 0.1
结果可能与理论值存在微小差异。
改善方法包括:
- 使用更高精度类型;
- 改变求和顺序;
- 先累加绝对值较小的数;
- 使用补偿求和算法;
- 使用定点或十进制类型;
- 减少不必要的重复运算。
误差是否可以接受,需要结合业务量级和允许偏差判断。
十一、为什么不能直接用等号比较浮点数
下面的比较可能失败:
if 0.1 + 0.2 == 0.3:
print("相等")
因为两边的实际浮点表示可能不同。
更常见的方式是判断差值是否足够小:
a = 0.1 + 0.2
b = 0.3
if abs(a - b) < 1e-9:
print("近似相等")
这里比较的不是绝对相等,而是:
两者误差是否处于业务允许范围内
十二、为什么只用固定绝对误差还不够
假设统一使用:
允许误差 = 0.000001
比较两个接近零的数时,这个误差可能太大;比较两个数十亿量级的数时,它又可能太小。
更加稳妥的方式是同时考虑绝对误差和相对误差:
|a - b| ≤ 最大值(
绝对误差,
相对误差 × max(|a|, |b|)
)
其中:
- 绝对误差适合处理接近零的情况;
- 相对误差根据数值规模调整容忍范围。
Python可以使用:
import math
math.isclose(
a,
b,
rel_tol=1e-9,
abs_tol=1e-12
)
容差不应随意复制,而应该根据数据范围、测量精度和业务要求确定。
十三、钱为什么不适合直接使用二进制浮点数
金额通常要求明确的小数位和舍入规则。
例如:
19.90元
如果用二进制浮点数保存,19.90可能只是一个近似值。大量金额相加、税费计算和折扣计算后,可能出现分币误差。
金融和账务系统通常采用以下方式。
方法一:使用最小货币单位的整数
例如,用“分”保存金额:
19.90元 → 1990分
加减运算全部使用整数:
1990 + 500 = 2490分
整数运算在范围允许时是精确的。
不过,需要提前明确:
- 最小货币单位;
- 是否存在比分更小的计算单位;
- 汇率计算精度;
- 税费中间值保留规则;
- 最终舍入方式。
方法二:使用十进制定点类型
数据库中的DECIMAL以及编程语言中的高精度十进制类型,可以按照十进制规则保存和计算金额。
它们更符合人类账务习惯,但运算速度和存储成本可能高于硬件原生浮点数。
金额计算首先需要正确,其次才是追求微小性能差异。
十四、创建十进制数时也可能引入浮点误差
即使使用十进制类型,如果先经过二进制浮点数,误差也可能已经进入。
例如:
from decimal import Decimal
value = Decimal(0.1)
这里的0.1可能先被解释为二进制浮点数,再转换成十进制对象,因此可能保留原来的近似值。
更合适的方式是从字符串创建:
value = Decimal("0.1")
字符串准确表达了十进制文本“0.1”,不会先经过二进制浮点转换。
十五、数据库中的FLOAT和DECIMAL有什么区别
FLOAT或REAL类类型
通常使用二进制浮点表示。
适合:
- 科学测量;
- 统计数据;
- 允许近似的数据;
- 数值范围很大的数据。
DECIMAL或NUMERIC类类型
通常按照指定十进制精度保存。
例如:
DECIMAL(10, 2)
可以用于最多10位有效数字,其中2位位于小数点后。
适合:
- 金额;
- 税率;
- 账务数据;
- 需要精确十进制舍入的数据。
不要因为字段中“有小数”,就默认使用浮点类型。应该先判断业务需要近似计算还是精确十进制计算。
十六、整数也有精度上限
使用浮点数保存整数时,同样存在限制。
双精度浮点数能够精确表示一定范围内的所有整数,但超过某个范围后,相邻整数之间可能没有对应的浮点值。
对于常见双精度浮点数,整数在绝对值不超过:
2⁵³
时通常可以精确表示。
超过这个范围后,可能出现:
一个整数
和:
下一个整数
被转换成同一个浮点值。
因此,大型整数编号、时间戳、订单号和数据库主键不应随意转换为浮点数。
一些使用双精度浮点数表示普通数字的运行环境,在处理超大整数编号时尤其需要注意。可以使用字符串或专门的大整数类型传递。
十七、NaN是什么
浮点标准还定义了一种特殊值:
NaN
表示“不是一个数”。
它可能来自:
- 无效数学运算;
- 未定义结果;
- 缺失或异常数值;
- 某些计算函数失败。
NaN具有一个反直觉特性:
NaN通常不等于它自己
因此,不能总是使用:
value == value
之外的普通相等逻辑判断NaN。更清晰的方式是使用语言提供的专门检测函数。
NaN进入统计数据后,可能让整个计算结果也变成NaN,因此需要在数据清洗阶段明确处理。
十八、正无穷和负无穷
浮点数还可以表示:
正无穷
负无穷
它们可能由数值溢出或某些除法产生。
无穷不一定立即导致程序崩溃,反而可能继续参与后续计算,使错误传播得更远。
对于关键计算,应检查结果是否为有限数,而不能只判断程序有没有抛出异常。
十九、负零是什么
浮点数通常区分:
+0
-0
它们在大多数相等比较中被视为相等,但在某些运算中可能产生不同结果。
例如,它们可能携带数值从正方向还是负方向趋近于零的信息。
普通业务程序很少需要主动处理负零,但在数值分析、复数计算和底层数学库中,它可能具有意义。
二十、舍入方式为什么重要
现实业务中经常需要将结果保留到固定小数位,例如金额保留两位。
但“保留两位”仍然需要说明具体舍入规则:
- 四舍五入;
- 向零截断;
- 向上取整;
- 向下取整;
- 五舍六入;
- 遇到正中间值时取偶数;
- 针对正负数采用不同规则。
不同语言的默认round函数可能采用不同策略,或者受到二进制近似值影响。
例如,看似正好处于中间位置的十进制数,在浮点表示中可能略高或略低于中点,因此结果与人工直觉不同。
财务计算必须明确舍入规则,并在所有系统中保持一致。
二十一、传感器数据是否需要精确相等
测量数据本身通常包含:
- 传感器误差;
- ADC量化误差;
- 温度漂移;
- 电源噪声;
- 标定误差;
- 浮点计算误差。
因此,判断电压是否等于某个值时,不应写成:
if (voltage == 5.0) {
// ...
}
更合理的是根据硬件精度设置容差范围:
if (voltage >= 4.95 && voltage <= 5.05) {
// 认为处于5V允许范围
}
这里的容差应该来自:
- 规格要求;
- 测量系统精度;
- 元器件误差;
- 控制系统稳定性;
- 安全余量。
浮点误差通常只是整体误差预算中的一部分。
二十二、控制系统中为什么需要滞回
如果控制逻辑以单一阈值判断:
电压高于10V:打开
电压低于10V:关闭
当测量值在10V附近波动时,状态可能频繁切换:
9.999
10.001
9.998
10.002
这不仅与浮点误差有关,也与实际信号噪声有关。
可以使用滞回区间:
高于10.1V时打开
低于9.9V时关闭
这样,状态不会因为阈值附近的微小波动反复变化。
因此,工程控制中不能只关注浮点表示,还要结合测量噪声和系统动态设计阈值。
二十三、如何降低求和误差
1. 使用更高精度类型
从单精度改为双精度,可以显著降低舍入误差,但不能完全消除。
2. 从小到大求和
把绝对值较小的数先相加,可以减少小数被大数“吞掉”的情况。
3. 使用补偿求和
补偿求和算法会额外记录部分舍入损失,并在后续运算中进行修正。
4. 分块求和
将大量数据分成多个小块,分别求和后再合并,通常比从头到尾顺序累加更稳定。
5. 使用整数或十进制类型
如果业务数据本质上具有固定小数位,定点表示可能比二进制浮点更加合适。
6. 重新设计公式
一些数学上等价的公式,在浮点计算中的稳定性可能完全不同。避免相近大数相减,常常比增加输出小数位更有效。
二十四、什么时候可以直接比较浮点数
并不是所有浮点数都不能使用==。
以下情况可能是合理的:
- 与自己刚刚赋值的同一浮点值比较;
- 比较约定的特殊离散值;
- 参与计算的数都能被二进制精确表示;
- 判断某个变量是否被设置为确定的状态值;
- 底层算法明确要求比较位级结果。
例如:
0.5
0.25
0.125
都能被二进制有限表示,一些简单计算可以得到精确结果。
问题不在于==这个操作本身,而在于开发者是否理解参与比较的数据来源和误差范围。
二十五、浮点数适合什么场景
浮点数非常适合:
- 科学计算;
- 机器学习;
- 图像处理;
- 三维图形;
- 信号处理;
- 物理仿真;
- 统计分析;
- 传感器数据;
- 大范围近似数值。
这些场景通常更关注:
- 数值范围;
- 计算速度;
- 相对精度;
- 误差是否可控。
浮点数不是“不准确所以不能用”,而是在有限存储和高速计算之间做出的工程选择。
二十六、不适合直接使用二进制浮点的场景
需要谨慎的场景包括:
- 金额;
- 税费;
- 账户余额;
- 精确计数;
- 超大整数编号;
- 密码学整数运算;
- 法律或合同规定的小数舍入;
- 需要逐位完全一致的跨系统结果。
这些场景更适合:
- 整数;
- 定点数;
- 十进制类型;
- 大整数;
- 专门的高精度数值库。
二十七、常见误区
1. 计算结果出现长小数就是程序错误
这通常只是浮点近似值被完整显示出来。
2. double可以精确表示所有小数
double比float精度高,但仍然不能精确表示大量十进制小数。
3. 多保留几位小数就能解决误差
格式化只改变显示结果,不会修复内部计算误差。
4. 所有浮点比较都不能使用等号
是否能够直接比较取决于数据来源和算法语义。
5. 使用一个固定极小值比较所有浮点数
不同数量级需要结合绝对误差和相对误差。
6. 金额使用double后最后统一四舍五入就一定安全
中间步骤、税费分摊和累计误差仍然可能造成不一致。
7. 运算顺序不会影响结果
浮点加法和乘法在实际计算中不一定满足数学上的结合律。
二十八、浮点计算检查清单
设计数值计算时,可以检查以下问题:
- 数据需要精确值还是近似值;
- 数据范围有多大;
- 允许误差是多少;
- 应该使用float还是double;
- 是否更适合使用整数或十进制类型;
- 是否直接比较了计算结果;
- 比较容差是否考虑数值规模;
- 是否存在大量重复累加;
- 是否有相近大数相减;
- 运算顺序是否影响结果;
- 是否明确舍入规则;
- 数据库字段类型是否合适;
- 超大整数是否被转换成浮点数;
- 是否正确处理NaN和无穷;
- 传感器阈值是否考虑测量误差和滞回;
- 单元测试是否覆盖边界值和极端值。
结语
0.1 + 0.2不一定等于0.3,根本原因是:
十进制0.1和0.2
→ 无法用有限位二进制精确表示
→ 只能保存邻近值
→ 运算过程中再次舍入
→ 最终结果出现微小误差
浮点数不是错误的数字类型,而是一种能够用有限空间表示巨大数值范围的近似计算工具。
正确使用浮点数的关键,不是追求绝对没有误差,而是:
- 知道误差从哪里产生;
- 根据业务定义允许误差;
- 避免直接比较不稳定结果;
- 为金额选择整数或十进制类型;
- 为科学与工程计算选择数值稳定的算法;
- 明确精度、范围和性能之间的取舍。
计算机并没有算错,它只是按照有限位二进制所能表达的最近数值完成了计算。
- 点赞
- 收藏
- 关注作者
评论(0)