<!-- title: 0.1+0.2≠0.3：浮点为何算不准 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=NETMcVdSVkE -->

# 0.1+0.2≠0.3：浮点为何算不准

每一个写过几年代码的人，几乎都被同一个"幽灵"咬过一次：你在 JavaScript 里写下 `0.1 + 0.2`，期待它老老实实等于 `0.3`，结果控制台冷冷地吐出 `0.30000000000000004`。第一次见到的人会以为是语言 bug，第二次以为是编译器抽风，第三次才隐隐意识到——这背后藏着一个比所有编程语言都更古老的、写在硅片里的"原罪"。YouTube 上这支《Why 0.1 + 0.2 ≠ 0.3 — Floating Point, Explained Visually》之所以值得反复看，就在于它没有用一句"浮点精度问题"糊弄过去，而是把那串令人发指的小数，一笔一画拆成了你能看见的比特。它解决的不是某个具体 bug，而是程序员和数字之间长达六十年的认知鸿沟。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Why 0.1 + 0.2 ≠ 0.3 — Floating Point, Explained Visually |
| 平台 | YouTube |
| 核心主题 | IEEE 754 浮点数的存储原理与精度丢失根因 |
| 内容形式 | 可视化逐比特拆解 |
| 知识增量 | 从"现象复现"到"二进制原罪"的完整因果链 |
| 适配人群 | 任何写代码、做计算、碰过钱或坐标的开发者 |

## 核心内容拆解

### 一、真正的元凶：小数在二进制里根本写不完

视频最关键的一步，是把你从"算错了"的错觉里拽出来。问题不出在加法，出在表示。我们在十进制下觉得 `0.1` 是个干净利落的小数，但换到二进制里，`0.1` 是一个无限循环小数：`0.00011001100110011…`，"1100"四位一组无限重复。这就好比十进制里的 `1/3 = 0.3333…`——你永远写不完，只能截断。

而这个截断，就是一切误差的起点。`0.1` 和 `0.2` 各自被存成 64 个比特时，都已经被"削"成了无限循环小数的近似值：`0.1` 实际存的是略大于 0.1 的 `0.10000000000000000555…`，`0.2` 存的是略大的 `0.2000000000000000111…`。两个"偏大"的数相加，结果自然偏大，于是出现了那个著名的 `0.30000000000000004`。视频的功劳在于——它让"无限循环"这件事变得可视，而不是一句抽象的"精度有限"。

### 二、IEEE 754 的三段式：符号、指数、尾数

要理解误差从哪一比特渗进来，必须看懂 `double` 的内部布局。视频把 64 个比特分成了三段，这是 IEEE 754 标准的铁律：

- **1 位符号位（sign）**：0 正 1 负，和整数一样。
- **11 位指数（exponent）**，采用"偏移（bias）"编码，偏移量 = 1023。实际指数 = 存储值 − 1023。这样负指数也能用无符号整数表示，比较时直接比大小即可。
- **52 位尾数（fraction）**，但隐藏了一个"幽灵 1"——规格化数字默认形式是 `1.m × 2^e`，那个整数位的 `1` 永远存在、不必存储，等于免费白送了 1 比特精度。

合起来，一个 `double` 能精确表达的，只有 `52 + 1 = 53` 个有效二进制位，折合约 **15~17 位十进制有效数字**。这就是天花板：超出这个位数的"小数位"，在二进制层面根本不存在，只能四舍五入。`float`（single）更惨，只有 1+8+23 结构，约 **6~7 位十进制**有效数字。视频反复强调的"精度预算"概念，指的就是这 53 位/24 位的稀缺资源。

### 三、精度不是均匀的：越靠近零越密，越往无穷越稀

这是很多人忽略、却极重要的一个特性，视频用刻度尺做了直观演示。浮点数在数轴上不是均匀分布的，而是**指数级稀疏化**：在 0 附近，相邻两个可表示的数之间间隔极小（小到 2 的负一千多次方）；而到了 10¹⁵、10¹⁶ 这个量级，相邻两个 `double` 之间已经相差 1 甚至 2——也就是说，在很大的数附近，"+1"这种操作会直接"被吞掉"，因为结果落进了两个可表示数之间的缝隙，四舍五入后又回到原数。

一个经典实验：从某个大数开始不断 `+1`，循环会变成死循环，因为加 1 等于没加。这解释了为什么游戏里地图坐标一放大就穿模、为什么大额财务计算必须远离浮点。**浮点的精度是浮动的**，这一点比"0.1 算不准"更致命，因为前者还会报警，后者会沉默地出错。

### 四、三个隐藏的杀手：抵消、溢出、下溢

视频之外，我想补充三个工程上最常踩的坑，它们都源于同一种"有限比特装无限实数"的矛盾：

1. **灾难性抵消（catastrophic cancellation）**：两个非常接近的数相减，有效数字会成片丢失。比如 `1.0000001 - 1.0`，原本的高精度瞬间蒸发，相对误差爆炸。这是数值算法里最隐蔽的陷阱。
2. **溢出（overflow）**：指数超过上限就变成 `±∞`。Ariane 5 火箭的悲剧本质上和它同源——超出了表示范围。
3. **下溢（underflow）**：结果太小、逼近零时，会掉进"次正规数（subnormal）"区间，精度骤降，最终塌缩成 0。很多概率连乘的算法，算着算着就变成 0，根因往往在此。

### 五、灾难现场：误差真的会杀人

把上面的理论放进真实世界，浮点误差的代价是血淋淋的，这也是这篇文章比视频多走的一步——

**海湾战争·爱国者导弹（1991 年 2 月 25 日，沙特宰赫兰）**：导弹的内部时钟以"十分之一秒"为步长累计时间，再换算。问题在于 `0.1` 秒在二进制里无法精确表示，每累计一次都残留一丁点误差。系统连续运行约 100 小时后，累计漂移达到 **0.34 秒**。来袭的飞毛腿导弹每秒飞行约 600 米，0.34 秒意味着目标已经偏离了 200 多米——拦截窗口彻底算错，导弹直接命中兵营，**28 名美军士兵丧生**。

**阿里亚娜 5 号火箭（1996 年 6 月 4 日首飞）**：把一个表示水平速度的 64 位浮点数，转换成 16 位有符号整数。阿里娜 5 比它的前辈阿里娜 4 快得多，这个速度值轻松超出 16 位整数上限，溢出抛出异常，飞控计算机崩溃，火箭在升空 40 秒后自毁，**5 亿美元载荷化为火球**。一行类型转换代码，葬送了一枚火箭。

**温哥华证券交易所（1982 年起）**：指数每次交易后都做"截断"而非"四舍五入"，每次都往小里偏一点点。一年多后，本该是 1098 点的指数，被算成了 **520 点**——几乎腰斩。一个舍入策略的选择错误，让整个市场的"温度计"长期失灵。

这三个案例的共同点是：**单次误差小到肉眼看不见，累积起来却能摧毁一座兵营、一枚火箭、一个交易所。** 浮点的问题从来不是"算错一个数"，而是"错误在沉默中复利"。

### 六、特殊值不是 bug，是设计：±0、±∞、NaN

视频顺带解释了 IEEE 754 几个常被当成"奇怪"的特殊值，它们其实都是精心设计的兜底机制：**+0 与 −0**（区分"从正无穷趋近"和"从负无穷趋近"）、**±∞**（明确表示溢出方向）、以及 **NaN（Not a Number）**——用来承载 `0/0`、`∞−∞`、`√(−1)` 这类"数学上无定义"的运算结果。

其中最反直觉的设计是 `NaN ≠ NaN` 永远成立。很多人觉得这是 bug，但视频点破：这正是 IEEE 754 架构师 William Kahan 的神来之笔。NaN 表示"一个未知且无法比较的值"，两个"未知"本就不该判等，于是 `x === x` 这个恒等式成了**判断一个数是否"正常"的最快方法**——`if (x !== x)` 就能精确捕获 NaN，这在所有语言里通用。把看似的缺陷变成检测手段，这是顶级标准才会有的品味。

### 七、工程师的对策：比较、定点、补偿求和

知道了病因，对策也就清晰了。视频和业界沉淀出几条铁律：

- **永远不要用 `==` 比较浮点**。改成判断差值是否小于容差（epsilon）：`abs(a − b) < 1e-9`。注意容差要按量级缩放，不是写死一个魔法数。
- **涉及钱的，别用浮点**。改用十进制定点类型——Python 的 `decimal`、Java 的 `BigDecimal`、C# 的 `decimal`、数据库的 `DECIMAL`——或者干脆以"分"为单位存整数。银行系统永远在整数上算钱。
- **大量求和时用 Kahan 求和（补偿求和法）**：用一个"补偿变量"把每次舍入丢掉的那一点尾数记下来，下次补回去。这个算法能把累积误差压到接近"无限精度再舍入"的水平，而发明者正是 IEEE 754 的奠基人 Kahan 本人。
- **关注数值算法的稳定性**，避免相近数相减、避免先乘大数再减、必要时取对数把乘法变加法。

## 知识结构图

```mermaid
flowchart TD
    A["0.1 + 0.2 ≠ 0.3 现象"] --> B{"根因在哪？"}
    B -->|"表示层"| C["二进制写不完小数<br/>0.1 = 0.000110011… 循环"]
    B -->|"运算层"| D["两个偏大的近似值相加<br/>四舍五入到 53 位"]

    C --> E["IEEE 754 double 布局<br/>符号1 + 指数11 + 尾数52"]
    E --> F["隐藏的幽灵 1<br/>规格化: 1.m × 2^e"]
    E --> G["bias = 1023<br/>偏移编码"]

    F --> H["精度天花板<br/>仅 15~17 位十进制"]
    H --> I["精度分布不均<br/>靠近0密集 / 靠近∞稀疏"]

    I --> J["三大陷阱"]
    J --> J1["灾难性抵消"]
    J --> J2["溢出 → ±∞"]
    J --> J3["下溢 → 次正规数→0"]

    J1 & J2 & J3 --> K["真实灾难<br/>爱国者导弹 / 阿里亚娜5 / 温哥华证交所"]

    D --> L["工程对策"]
    L --> L1["epsilon 容差比较"]
    L --> L2["定点数 / decimal 存钱"]
    L --> L3["Kahan 补偿求和"]
    L --> L4["NaN 靠 x≠x 检测"]

    K --> L
```

## 实战启示

看完这个视频和上面的拆解，最该带走的是一套"浮点卫生"的工作习惯。

**第一，建立"浮点不是实数"的直觉。** 在你的大脑里，把 `double` 理解成一个布满小格子的尺子，而不是一条光滑的连续直线。有些数恰好落在格子上（精确），大多数数只能找到最近的格子（近似）。当你写下一个带小数点的字面量时，它就已经不等于你心里那个数了——误差在赋值那一刻就诞生了，运算只是把它放大。带着这个滤镜看代码，很多"诡异"的 bug 会立刻现形：为什么这个累加器越加越偏？为什么这个比例永远到不了 1.0？为什么这个坐标在高数值区会"粘住"？

**第二，给不同场景配不同的数字类型。** 算钱、算坐标、算计量值——任何涉及"必须精确"的场景，浮点都不是答案。学会在第一时间问自己：这个数算错了会怎样？如果答案是"财务对不上账""坐标穿模""剂量算错"，那就换成整数（以最小单位计）或十进制定点。把 `double` 留给那些天然带噪声、容许近似的地方：物理仿真、机器学习、统计、图像。这不是偷懒，是分工。

**第三，把"比较浮点"写进肌肉记忆。** 凡是看到 `if (a == b)` 且 `a`、`b` 是浮点，条件反射般改成容差比较。再进一步，凡是看到累加浮点上万次的循环，考虑 Kahan 求和。这些不是炫技，而是把你从"99% 时间正确、生产环境偶发翻车"的泥潭里捞出来。

**第四，敬畏累积。** 爱国者导弹的 0.1 秒、温哥华证交所的逐笔截断，本质上都是"单次无害、累积致命"。当你写任何循环、任何长时间运行的累加、任何自增计数器时，问一句：误差会复利吗？如果是，就得主动管理它。浮点最危险的地方，从来不是它算错，而是它**安静地、持续地**算错。

## 延伸学习路径

1. **《What Every Computer Scientist Should Know About Floating-Point Arithmetic》**（David Goldberg 著）——浮点领域的"圣经级"文献，看完你会对所有舍入行为有数学级的掌控。
2. **IEEE 754 在线可视化工具**（如 DevToolbox 的 IEEE754 Inspector / DigiSim）——把你输入的任意小数实时翻译成 64 比特，亲手验证 `0.1` 的真实存储值。
3. **《Floating Point — Numberphile》**——另一个经典科普视角，用更轻松的方式讲同一件事，适合搭配本文视频一起看。
4. **Python `decimal` 模块官方文档**——从工程角度落地"用十进制定点算钱"，里面有大量可复制的财务计算范式。
5. **《An Interview with the Old Man of Floating-Point》**（William Kahan 访谈）——听 IEEE 754 架构师本人讲标准背后的取舍，理解那些"奇怪"设计从何而来。

## 一句话总结

**浮点数从来不是"算错了"，而是用 53 个比特去装一个无限的小数——它教会程序员的，是永远不要相信光滑的数轴，要用容差、定点和补偿，去管理那个注定存在的缝隙。**

## 参考来源

1. [Why 0.1 + 0.2 ≠ 0.3 — Floating Point, Explained Visually](https://www.youtube.com/watch?v=NETMcVdSVkE) — 本文深度解读的核心视频
2. [Disasters due to rounding error — University of Texas at Austin](https://web.ma.utexas.edu/users/arbogast/misc/disasters.html) — 爱国者导弹、温哥华证交所等真实灾难的技术归因
3. [How one line of code brought down a €500M rocket launch — Jam Blog](https://jam.dev/blog/famous-bugs-rocket-launch/) — 阿里亚娜 5 号浮点转整数溢出事故详解
4. [IEEE Standard 754 Floating Point Numbers — GeeksforGeeks](https://www.geeksforgeeks.org/computer-organization-architecture/ieee-standard-754-floating-point-numbers/) — 符号/指数/尾数三段式与偏移编码的规范说明
5. [Real-World Floating-Point Bugs — DevToolbox](https://www.dev-toolbox.tech/tools/ieee754-inspector/examples/real-world-float-bugs) — 经典浮点 bug 案例库与可视化检查工具

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
