mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
563 字
2 分钟
为什么浮点数计算不精确
2023-07-10
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

这是计算机科学中最经典的问题之一。本文深入解析浮点数不精确的原因。

一、问题的本质:十进制 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 = 0
exponent = 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.1
0.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 math
if math.isclose(0.1 + 0.2, 0.3): # True
pass
# 或者使用容差
if abs((0.1 + 0.2) - 0.3) < 1e-9:
pass

5.2 大数吃小数#

>>> 1e16 + 1
10000000000000000.0
>>> 1e16 + 1 == 1e16
True
flowchart LR A[1e16] --> B[精度损失] B --> C[1e16 无法表示 +1] C --> D[结果仍然是 1e16]

5.3 累计误差#

# 累计误差示例
total = 0.0
for 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.3

6.3 使用分数#

from fractions import Fraction
a = Fraction(1, 10) # 1/10
b = Fraction(2, 10) # 2/10
print(a + b) # 3/10
print(float(a + b)) # 0.3

七、总结#

7.1 浮点数不精确的原因#

原因说明
二进制无法精确表示 0.10.1 是无限循环二进制
有限的尾数位数单精度 23 位,双精度 52 位
舍入误差无法精确表示时必须舍入

7.2 使用建议#

场景推荐方案
货币计算整数运算或 Decimal
科学计算浮点数(可接受误差)
精确比较使用容差或 Fraction
金融应用Decimal 或专门库

核心原则:理解浮点数的局限性,在需要精确度的场景使用正确的工具。

参考引用#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么浮点数计算不精确
https://blog.souloss.com/posts/why-the-design/why-floating-point-is-imprecise/
作者
Souloss
发布于
2023-07-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时