563 字
2 分钟
为什么浮点数计算不精确
>>> 0.1 + 0.20.30000000000000004>>> 0.1 + 0.2 == 0.3False这是计算机科学中最经典的问题之一。本文深入解析浮点数不精确的原因。
一、问题的本质:十进制 vs 二进制
1.1 十进制的困境
人类使用十进制,但计算机使用二进制。
十进制中简单的 0.1:
0.1 (十进制) = 1/10 = 0.1转换为二进制:
0.1 (十进制) = 0.000110011001100110011... (二进制) = 0.0001100110011... (无限循环)关键发现:0.1 在二进制中是无限循环小数!
1.2 为什么 0.1 不能精确表示?
flowchart LR
subgraph 十进制
D1[0.1 = 1/10]
end
subgraph 二进制
B1[0.1 = ?]
B1 -->|除以 2| B2[无限循环]
end
0.1 的二进制展开:
0.1 = 0.0001100110011001100110011001100... ↑____↑____↑____↑____ 0011 重复(无限循环)二、IEEE 754 浮点数标准
2.1 浮点数的结构
IEEE 754 单精度浮点数(32 位):
┌──────────┬─────────────────┬──────────────────┐│ 符号位 │ 指数部分 (8位) │ 尾数部分 (23位) ││ 1 bit │ 8 bits │ 23 bits │└──────────┴─────────────────┴──────────────────┘| 部分 | 作用 | 范围 |
|---|---|---|
| 符号位 | 正负 | 0 = 正, 1 = 负 |
| 指数 | 数量级 | -126 ~ +127 |
| 尾数 | 精度 | 约 7 位十进制 |
2.2 浮点数表示公式
值 = (-1)^符号 × 1.尾数 × 2^(指数 - 127)2.3 0.1 的 IEEE 754 表示
import struct
# 0.1 的 32 位浮点数表示b = struct.pack('>f', 0.1)print(' '.join(f'{x:08b}' for x in b))# 00111101 11001100 11001100 11001101
# 解剖sign = 0exponent = 01111011 = 123 (十进制)mantissa = 10011001100110011001101三、精度损失的过程
3.1 0.1 的存储过程
flowchart LR
A[0.1 十进制] --> B{转换为二进制}
B --> C[0.0001100110011...]
C --> D{舍入到 23 位}
D --> E[0.00011001100110011001101]
E --> F{存储}
F --> G[实际值 ≈ 0.100000001490116]
3.2 精度损失示例
>>> 0.10.1 # 显示的是四舍五入后的值
>>> repr(0.1)'0.1'
>>> format(0.1, '.20f')'0.10000000000000000555'3.3 计算 0.1 + 0.2
sequenceDiagram
participant A as 0.1
participant B as 0.2
participant R as 结果
A->>R: 0.10000000000000000555
B->>R: 0.20000000000000001110
Note over R: 相加得 0.30000000000000004441<br/>显示时四舍五入为 0.3
四、为什么不用十进制浮点数?
4.1 十进制浮点数的成本
flowchart LR
subgraph 二进制
B1[硬件支持] --> B2[高效]
end
subgraph 十进制
T1[软件模拟] --> T2[慢 10-100 倍]
end
style T2 fill:#f96
| 方法 | 速度 | 复杂性 |
|---|---|---|
| 二进制浮点 | 硬件加速,快 | 简单 |
| 十进制浮点 | 软件模拟,慢 | 复杂 |
4.2 IEEE 754 十进制浮点数
IEEE 754-2008 引入了十进制浮点数:
# Python 的 decimal 模块from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')Decimal('0.3')
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')True五、浮点数的陷阱
5.1 等值比较
# 错误if 0.1 + 0.2 == 0.3: # 永远为 False pass
# 正确import mathif math.isclose(0.1 + 0.2, 0.3): # True pass
# 或者使用容差if abs((0.1 + 0.2) - 0.3) < 1e-9: pass5.2 大数吃小数
>>> 1e16 + 110000000000000000.0
>>> 1e16 + 1 == 1e16Trueflowchart LR
A[1e16] --> B[精度损失]
B --> C[1e16 无法表示 +1]
C --> D[结果仍然是 1e16]
5.3 累计误差
# 累计误差示例total = 0.0for i in range(10): total += 0.1
print(total) # 1.0000000000000007,不是 1.0六、解决方案
6.1 使用整数运算
# 所有计算用整数,末尾再除以比例因子# 例如:计算货币,用"分"而非"元"price_in_cents = 100 # 1.00 元tax = 5 # 0.05 元total = price_in_cents + tax # 105 分 = 1.05 元6.2 使用 Decimal
from decimal import Decimal, getcontext
# 设置精度getcontext().prec = 28 # 默认精度
price = Decimal('0.1')total = price + Decimal('0.2')print(total) # 0.36.3 使用分数
from fractions import Fraction
a = Fraction(1, 10) # 1/10b = Fraction(2, 10) # 2/10print(a + b) # 3/10print(float(a + b)) # 0.3七、总结
7.1 浮点数不精确的原因
| 原因 | 说明 |
|---|---|
| 二进制无法精确表示 0.1 | 0.1 是无限循环二进制 |
| 有限的尾数位数 | 单精度 23 位,双精度 52 位 |
| 舍入误差 | 无法精确表示时必须舍入 |
7.2 使用建议
| 场景 | 推荐方案 |
|---|---|
| 货币计算 | 整数运算或 Decimal |
| 科学计算 | 浮点数(可接受误差) |
| 精确比较 | 使用容差或 Fraction |
| 金融应用 | Decimal 或专门库 |
核心原则:理解浮点数的局限性,在需要精确度的场景使用正确的工具。
参考引用
- IEEE 754-2019 Standard — 浮点数标准
- IEEE 754 Calculator — 浮点数可视化工具
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — 经典论文
- Python decimal 模块 — Python 精确计算
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
为什么十进制计算也需要精确
技术科普 深入解析为什么金融计算需要精确的十进制运算,以及 Decimal 类型的实现原理。
2
为什么基础服务不应该高可用
技术科普 深入解析基础服务的高可用设计哲学,什么时候应该追求高可用,什么时候应该放弃。
3
为什么 MySQL 自增主键不单调也不连续
技术科普 深入解析 MySQL InnoDB 自增主键为什么不单调也不连续,以及事务和锁的影响。
4
为什么数据库不应该使用外键
技术科普 深入解析为什么现代互联网应用中不建议使用外键,以及如何替代外键实现数据一致性。
5
为什么 WebSocket 需要握手
技术科普 深入解析 WebSocket 握手协议的设计原理,理解从 HTTP 到 WebSocket 的协议升级机制。






