<!-- title: 0.1算不准：它在二进制里是无限循环小数 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=dQhj5RGtag0 -->

# 0.1算不准：它在二进制里是无限循环小数

## 一、为什么这个视频值得你花18分钟

每个程序员第一次写 `print(0.1 + 0.2)` 时，都会经历一次小型信仰崩塌——屏幕上跳出的不是干干净净的 `0.3`，而是 `0.30000000000000004`。这不是 bug，也不是语言的锅，Python、JavaScript、Java、C、Rust，几乎地球上所有通用语言都会给你同样的答案。因为它们背后用的是同一套硬件标准：**IEEE 754 浮点数**。

jan Misali 这支播放量 45 万、时长近 18 分钟的《how floating point works》，是我见过把"为什么小数会算错"讲得最通透的视频之一。他没有停留在"浮点数不精确"这句正确的废话上，而是从**二进制的数学根**出发，让你亲眼看到 0.1 为什么在计算机里根本无法被精确表示，再一层层揭开浮点数是"科学记数法而非小数"、数轴上疏密不均、甚至连加法结合律都会失效的真相。

如果你写过任何涉及金额、坐标、物理模拟或机器学习的代码，这 18 分钟能帮你少踩一类最隐蔽、最难复现的 bug。本文在视频基础上补全了 IEEE 754 的工程细节和真实灾难案例。

## 二、视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | how floating point works |
| 频道 | jan Misali |
| 时长 | 17 分 48 秒 |
| 播放量 | 约 45 万（截至 2026 年） |
| 频道订阅 | 约 35.1 万 |
| 核心主题 | IEEE 754 浮点数：为什么小数算不准，以及它是怎么表示的 |
| 适合人群 | 所有写过小数运算的程序员、计算机专业学生 |

## 三、核心内容拆解

### 1. 一切祸根：0.1 在二进制里是无限循环小数

视频开篇就直击要害。我们觉得 0.1 是个"干净"的数，那只是**十进制**给我们的幻觉。十进制之所以能精确表示 0.1，是因为分母 10 = 2×5，而 0.1 = 1/10。一个分数能被某进制精确表示，**当且仅当它的分母（约分后）只含该进制基数的质因子**。十进制的基数是 10，质因子是 2 和 5，所以 1/10、1/5、1/2、1/4、1/20 都能精确表示。

但计算机是二进制，基数是 2，唯一的质因子就是 2。于是 1/2 = 0.1₂、1/4 = 0.01₂、1/8 = 0.001₂ 都能精确表示，可一旦分母里掺进一个 5，比如 1/10、1/5、1/20，在二进制下就变成了**无限循环小数**。

具体地，0.1 转成二进制是 `0.0001100110011001100…`，"1100"这四位永远循环下去，和十进制里 1/3 = 0.333… 本质完全一样。既然它无限长，而存储空间（double 是 52 位尾数）有限，计算机就只能**截断**它，于是存进去的 0.1 实际上是 0.1000000000000000055511151231257827…。两个这样的数加起来，再舍入，就得到了那个经典的 `0.30000000000000004`。

> **延伸**：这不是"实现得不好"，而是数学的必然。任何有限位二进制小数都无法精确表示 0.1，就像有限位十进制无法精确表示 1/3。

### 2. 浮点数根本不是"小数"，是科学记数法

视频接着纠正一个根深蒂固的误解：很多人以为浮点数是"带小数点的整数"，其实它更像是**二进制版的科学记数法**。一个 IEEE 754 数的本质形式是：

$$(-1)^{\text{sign}} \times 1.\text{mantissa} \times 2^{\text{exponent}}$$

一个 64 位 double（`float64`）被切成三段：1 位符号位、11 位指数、52 位尾数（mantissa，也叫 significand）。float32 则是 1 + 8 + 23。这个结构决定了它最核心的权衡——**它追求的是"范围"，而不是"精度"**。一个 double 能表示从约 $1.8\times10^{308}$ 到约 $5\times10^{-324}$ 的天文级跨度，代价是在这个范围内的数只能离散地、近似地取到。

这也解释了名字的由来：**浮点（floating point）**——小数点的位置会随着指数"浮动"，固定小数位数的叫**定点数（fixed point）**。浮点数用指数让有效数字自动对齐，从而在小数和大数上都能分配合理的有效位。

### 3. 两个聪明的工程把戏：隐含的前导 1 与指数偏移

视频展示了 IEEE 754 设计里两个精打细算的细节。

**隐含的前导 1（hidden leading bit）**：二进制科学记数法规范化的结果，小数点前永远是 1（形如 1.xxxx × 2ⁿ），既然这个 1 一定存在，存它就浪费一个 bit。于是标准规定：这个前导 1 **不存**，只在运算时由硬件补回。这一招凭空给 float64 多抠出了 1 位精度（53 位有效位），堪称"省一毛是一毛"的典范。

**指数偏移（exponent bias）**：11 位指数本身是无符号的（能表示 0–2047），但我们既要表示很大的数（正指数），也要表示很小的数（负指数）。标准没用补码，而是约定：**真实指数 = 存储值 − 1023**。这样存储值 1023 对应指数 0，存储值 0 和 2047 被保留给特殊用途（见第 6 节）。float32 的偏移量则是 127。

这两个设计让浮点运算能用相对简单的硬件高速实现，是 IEEE 754 在 1985 年能一统江湖的关键。

### 4. 数轴上的浮点数根本不均匀——越靠近 0 越密

这是视频里最反直觉、也最重要的一点。我们直觉上以为浮点数在数轴上像整数一样均匀分布，**完全不是**。

由于指数的存在，相邻两个可表示数之间的间距（称为 **ULP，unit in the last place**）会随着数的增大而指数级膨胀：在 1 附近，相邻数差约 $2.2\times10^{-16}$（这就是机器 epsilon）；在 1000 附近，差约 $10^{-13}$；而到了 $2^{53} \approx 9\times10^{15}$ 附近，相邻数之间已经差了整整 2——**这意味着 9 000 000 000 000 001 在 double 里根本存不进去，它会被舍入成 9 000 000 000 000 000**。

推论极其重要：**当一个小数加到一个足够大的数上时，小数会被"吃掉"。** 试一试 `1e16 + 1.0 == 1e16`，结果是 `True`。那个 `+1` 在这个量级的 ULP 面前，就像往太平洋里倒一杯水，水面纹丝不动。这种丢失叫**吸收（absorption）**，是浮点运算最常见的隐形杀手之一。

### 5. 最致命的一点：浮点加法没有结合律

视频后半段触及了浮点数最让人脊背发凉的性质——**它破坏了我们从小学就奉为真理的算术定律**。

由于每次运算都要舍入，浮点加法**不满足结合律**：

$$(a + b) + c \neq a + (b + c)$$

一个经典反例（float32）：`(1e8 + 1.0) + 1.0` 与 `1e8 + (1.0 + 1.0)` 结果可能不同，因为前者那个 1.0 在第一次加法时就被吸收了。更隐蔽的是**灾难性抵消（catastrophic cancellation）**：当两个非常接近的数相减时，高位的有效数字全部消掉，剩下的全是噪声位，相对误差瞬间爆炸。比如算两点距离 $\sqrt{x^2+y^2}$，当 x、y 量级悬殊时，平方项会放大到极大、再做差抵消，精度几乎全军覆没。

这对工程实践的含义是颠覆性的：**你不能假定同一个公式、只是换了计算顺序，还能得到一样的结果。** 并行归约求和、SIMD 向量化、不同编译器优化级别，都可能让同一段代码给出不同的最后几位，从而让"数值复现"变成一门单独的学问。这也是为什么数值线性代数库（LAPACK、BLAS）要精心设计运算顺序。

### 6. 特殊值的设计哲学：NaN 不等于自己

视频还讲了 IEEE 754 那一票"特殊值"，它们的设计其实极其优雅。

- **±0**：正零和负零都存在，虽然比较时 `+0 == -0` 为真，但 `1/+0 = +Inf`、`1/-0 = -Inf`，方向信息被保留，在复变和极限运算里有用。
- **±Inf**：指数全 1、尾数全 0。溢出不再崩溃，而是变成无穷大，让运算可以"传染"地继续下去。
- **NaN（Not a Number）**：指数全 1、尾数非 0。最酷的是 **NaN ≠ NaN 永远为真**——这可不是 bug，而是神来之笔：因为它不自等，你只要写 `if (x != x)` 就能一行检测出 NaN。Python 的 `math.isnan` 底层就是这么干的。
- **次正规数（subnormal/denormal）**：填补 0 和最小正规数之间的缝隙，实现"渐进下溢"，避免数值在极小处突然跳到 0。代价是运算较慢，有些游戏引擎会开启 FTZ（flush-to-zero）把它关掉换性能。

### 7. 这不是学术问题——它真的杀过人

视频点到为止的地方，我用两个真实案例把它补全，因为这才是浮点数该被严肃对待的理由。

**爱国者导弹灾难（1991，达兰兵营）**：爱国者系统的内部时钟用"十分之一秒"为单位计数，再乘以 0.1 换算回秒。而 0.1 在二进制里正是上面说的无限循环小数，每次换算都产生约 $9.5\times10^{-8}$ 秒的微小误差。系统连续运行 100 小时不重启，误差累积到约 0.34 秒。一枚来袭的飞毛腿导弹每秒飞约 1676 米，0.34 秒就是约 **570 米**的偏差——拦截窗口完全错开。当夜 28 名美军丧生。这是软件工程史上被引用最多的浮点数案例。

**温哥华证券交易所（1982–1983）**：交易所指数每次更新都把结果**截断**（而不是四舍五入）到 3 位小数，每次都朝下偏那么一点点。22 个月累积下来，这个本应反映大盘走势的指数，**凭空蒸发了约 50%**，明明市场是在涨的。修复方法是改成"用整数累加、只显示时再除"。

> 这两个案例的共同教训：**当浮点误差被放在一个"循环累加"的回路里，它就会从可忽略的噪声，变成系统性的、单向的、致命的偏移。**

## 四、知识结构图

```mermaid
flowchart TB
    A["0.1 + 0.2 ≠ 0.3<br/>浮点数为什么算不准"] --> B{"根因：分母含质因子 5<br/>0.1 在二进制是无限循环小数"}
    B --> C["有限位存储只能截断<br/>存进去的 0.1 已有微误差"]

    A --> D["本质：科学记数法 (-1)^s × 1.m × 2^e"]
    D --> D1["1 位 符号位"]
    D --> D2["11 位 指数（偏移 1023）<br/>float32 为 8 位 偏移 127"]
    D --> D3["52 位 尾数<br/>前导 1 隐含不存 → 实为 53 位"]

    D2 --> E["特殊值：指数全0/全1"]
    E --> E1["±0、±Inf"]
    E --> E2["NaN（NaN≠NaN）"]
    E --> E3["次正规数 subnormal<br/>渐进下溢"]

    D3 --> F["数轴分布不均匀<br/>ULP 随指数翻倍"]
    F --> F1["靠近 0 越密"]
    F --> F2["大数 + 小数 → 吸收<br/>1e16 + 1 == 1e16"]

    F2 --> G["加法失去结合律<br/>(a+b)+c ≠ a+(b+c)"]
    G --> G1["灾难性抵消<br/>相近数相减丢精度"]
    G --> G2["累加误差单向漂移"]

    G2 --> H["真实灾难"]
    H --> H1["爱国者导弹 0.34 秒<br/>28 人丧生"]
    H --> H2["温哥华指数 22 月<br/>蒸发 50%"]
```

## 五、实战启示：怎么和浮点数和平共处

明白了原理，工程对策其实很清晰，核心思想是**"该精确的地方，绝不用浮点"**。

**钱永远别用 float/double。** 金融系统计算必须用整数（以分为单位）或任意精度的十进制类型——Python 的 `decimal.Decimal`、Java 的 `BigDecimal`、C# 的 `decimal`。0.1 元存成整数 10 分，加减乘除全程在整数域，最后再格式化。这一条能消灭一类最常见的"账对不上"bug。

**永远不要用 `==` 比较浮点数。** 正确做法是判断差的绝对值是否小于一个阈值（epsilon）：`abs(a - b) < 1e-9`。更稳健的是基于 ULP 的比较（如 C++ 的 `std::abs(a-b) <= epsilon * max(abs(a),abs(b))`，或直接用 Google Test 的 `EXPECT_NEAR`）。

**累加时用 Kahan 求和（补偿求和）。** 它用一个额外的"补偿项"把每次舍入丢掉的尾巴记下来，下轮补回去，能把 N 次求和的误差从 $O(N)$ 降到近乎 $O(1)$。Python 3.12+ 的 `math.fsum` 和 NumPy 的 `np.sum` 在关键路径上用的就是这类技术。如果只能用朴素求和，**先排序、从小到大加**，能显著减少吸收。

**对顺序敏感的代码，要锁定浮点模型。** CUDA 的 `-ftz`、`--fmad`、编译器的 `-ffast-math`、不同 BLAS 实现都会改变结果。如果你的科学计算/模型推理要求可复现，要么关掉这些激进优化，要么记录浮点环境，否则换台机器就"对不上"了。

**物理与图形：接受误差，但控制量级。** 游戏引擎和物理模拟普遍用 float32 换性能，但要警惕"远离原点"的坐标（如大世界坐标）让 ULP 暴涨。做法是用**相对坐标/分层坐标**把物体拉回原点附近，或者对超大世界用 double 做位置、float 做旋转。Far Lands（Minecraft 早期著名的地图崩坏）就是 float32 坐标溢出的经典反面教材。

## 六、延伸学习路径

1. **IEEE 754-2019 标准** —— 想吃透精确语义（各种舍入模式、异常处理）的唯一权威，配合 [floating-point-gui.de](https://floating-point-gui.de/) 这个科普小站一起看，后者用最少的话讲清了程序员最该知道的事。
2. **《What Every Computer Scientist Should Know About Floating-Point Arithmetic》**（David Goldberg, 1991）—— 浮点数领域的"九阴真经"，灾难性抵消、ULP 误差分析的系统化推导都在这里，ACM 经典长青文。
3. **Computerphile 的 Floating Point Numbers（PZRI1IfStY0）** —— 英国诺丁汉大学的经典短片（2.5M 播放），用粉笔画把 IEEE 754 的位结构讲得极清楚，和 jan Misali 这支互为补充。
4. **fabiensanglard/wikipedia 上的"单精度浮点数例子"** —— 配合 [float.exposed](https://float.exposed/) 这个交互式工具，拖动每一位二进制，实时看浮点数值如何变化，是建立直觉的最佳方式。
5. **Bruce Dawson 的博客系列 "Random ASCII"** —— 微软浮点数专家，专门讲浮点陷阱、NaN 传播、浮点确定性问题，案例硬核、实战性极强。

## 七、一句话总结

> 浮点数不是"不精确的小数"，而是一套**用有限位数逼近极大范围**的科学记数法——它用"0.1 永远存不准""加法没有结合律""数轴疏密不均"这些代价，换来了在芯片上高速处理天文级数字的能力，看懂这笔交易，你就再也不会被 `0.1 + 0.2` 吓到。

## 参考来源

1. [how floating point works — jan Misali](https://www.youtube.com/watch?v=dQhj5RGtag0) — 本文分析的核心视频
2. [Floating Point Numbers — Computerphile](https://www.youtube.com/watch?v=PZRI1IfStY0) — IEEE 754 位结构经典讲解
3. [What Every Computer Scientist Should Know About Floating-Point Arithmetic — David Goldberg / ACM](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) — 浮点误差分析权威文献
4. [The Floating-Point Guide — floating-point-gui.de](https://floating-point-gui.de/) — 程序员视角的浮点对策速查
5. [float.exposed — 交互式浮点位可视化工具](https://float.exposed/) — 建立直觉的最佳实践工具

---
> 本文由「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)
