首页 › programming › 0.1+0.2为何不等于0.3

0.1 + 0.2 ≠ 0.3:一条计算机科学的经典神课深度解读

概述

在所有让程序员"世界观崩塌"的瞬间里,0.1 + 0.2 === 0.30000000000000004 大概率排名第一。它在 Stack Overflow 上是史上访问量最大的问题之一,几乎每个开发者在职业生涯早期都会撞上它,然后大部分人选择耸耸肩绕过去——知道现象,却不明白机理。本期我们深度解读 Computerphile 频道讲解浮点数(Floating Point)的经典视频,把这个问题从二进制的地基上彻底讲透:计算机如何在有限比特里表示无穷多的实数、IEEE 754 标准的精妙设计、历史上真实发生过的浮点灾难,以及到今天仍在发生的工程陷阱。在 AI 大量替人写代码的 2026 年,这个老话题反而更重要了——因为 AI 生成的代码同样会犯浮点错误,而审查代码的你必须有能力看出来。

一、视频基本信息

项目 内容
视频标题 Floating Point Numbers(浮点数)
频道 Computerphile
讲解方式 白板手绘推演,面向工程师的直观讲解
发布时间 2014 年前后(据频道页面;本次未能实时抓取精确日期)
播放量 数百万级,长期占据 "floating point" 主题搜索头部位置(据频道页面)
视频地址 见文末参考来源

说明:本次制作时视频页数据抓取受限,以上热度信息为频道长期公开数据之约数,精确数字请以频道页面实时显示为准。

Computerphile 是英语世界最著名的计算机科普频道之一,与 Numberphile 同源,常驻讲解者多为诺丁汉大学的计算机学者。它的特点是不用炫酷动画,就靠一块白板和一支马克笔,把计算机科学里最"硬"的概念讲到工程师真正能听懂。浮点数这期正是这种风格的代表作:十几分钟,从"计算机只有 0 和 1"推到"为什么 0.1 存不进去",全程没有一句废话。

二、核心主张拆解:视频到底讲了什么

要点一:无限的实数,有限的比特

视频的起点是一个根本性矛盾:实数在数轴上是稠密且无限的——任意两个实数之间还有无穷多个实数。而计算机里一个 float 只有 32 位,一个 double 只有 64 位。32 位最多表达 2³² ≈ 43 亿种状态,你不可能用有限个格子去装无穷个数。

所以浮点数的本质不是"精确表示",而是"在数轴上撒一把有限且精心挑选的点",所有真实数值都被就近吸附(舍入)到这些点上。理解了这一点,后面所有"怪事"都不怪了:误差不是 bug,是这个模型与生俱来的属性。

视频先讲了直觉上最容易想到的方案——定点数:固定小数点位置,比如 16 位整数部分 + 16 位小数部分。问题立刻暴露:定点数要么覆盖范围小,要么精度粗,二者不可兼得。想同时表示"原子的直径"和"星系的直径",定点数在固定比特数下无能为力。这就引出了真正的答案。

要点二:把科学计数法搬进二进制

人类在纸上处理极大或极小的数,用的是科学计数法:光速写成 2.998 × 10⁸ m/s。我们只关心三样东西:正负号、指数、有效数字。视频的核心动作,就是把这套思想翻译成二进制,这就是浮点数(floating point)名字的由来——小数点的位置不再固定,而是"浮"着,由指数字段动态决定。

IEEE 754 单精度(32 位)的内存布局是视频反复画、也是最值得记住的一张图:

| 1 bit 符号 | 8 bit 指数 | 23 bit 尾数 |

双精度(64 位)则是 1 + 11 + 52。三项的含义:

这套设计让 32 位浮点数既能表示约 10³⁸ 的巨大数,也能表示约 10⁻³⁸ 的微小数,动态范围碾压同样比特数的定点数。

要点三:0.1 的"死刑判决"——二进制里的无限循环

这是全片最关键的一幕。十进制的 0.1 写成二进制是 0.000110011001100…,1100 无限循环。原因和 1/3 在十进制里是 0.333… 完全同构:1/3 在十进制循环,1/10 恰好含因子 5、与二进制互质,于是必然无限循环。

而尾数只有 23 位(双精度 52 位),存储时必须在某处截断并舍入。所以:

>>> 0.1
0.1000000000000000055511151231257827

你在任何主流语言里写下的字面量 0.1,从来就不是 0.1,而是比 0.1 略大(或略小)的最近的那个可表示数。0.1 + 0.2 !== 0.3 的完整因果链是:0.1 被舍入过一次,0.2 被舍入过一次,两者相加的结果再次舍入,三个"最近邻"的误差叠加后,落在的格子与 0.3 的最近邻格子不同——于是 0.30000000000000004 诞生了。每一步都完全确定、可以逐位复现,没有任何随机性。

要点四:精度不是均匀的——格子会越变越大

视频里另一个容易颠覆直觉的点:浮点数的"刻度"在数轴上不均匀。指数每增加 1,可表示数之间的间隔(术语叫 ULP,Unit in the Last Place)就翻一倍。

一句话总结:浮点数的精度是相对的,不是绝对的。它保证的是"有效数字位数"大致恒定,而不是"小数点后多少位恒定"。这也是为什么金额计算用浮点数是错的——从 0.01 元到 10 亿元,用户要求的是绝对精度,而浮点只能给相对精度。

要点五:那些设计好的"逃生口"

视频收尾提到了标准里的特殊值设计:指数位全 1 配尾数全 0 表示 ±Infinity;指数全 1 配尾数非 0 表示 NaN(NaN 连自己都不等于自己:NaN !== NaN);指数全 0 的非规格化数(denormals)负责填补 0 附近的精度真空,实现向 0 的渐进下溢。这些"格子"是 IEEE 754 特意留的逃生口,让硬件在不抛异常的情况下继续计算并携带异常状态——这是当年标准设计者深思熟虑的结果,也为后面讲设计史埋下伏笔。

三、知识与理论溯源:这一切从哪来

IEEE 754 与 William Kahan

视频讲述的整套规范就是 IEEE 754 标准,1985 年正式发布,此后成为几乎所有 CPU 浮点单元(FPU)的事实基础。标准背后的灵魂人物是伯克利大学的 William Kahan 教授,人称"浮点数之父",他主导的设计在 1989 年拿下图灵奖。Kahan 团队当年最激进的主张是:不同厂商的浮点运算必须有可移植的、确定性的结果——今天你在 x86 和 ARM 上算 0.1 + 0.2 得到完全相同的位模式,就是他留下的遗产。

值得一提的是,Kahan 至今仍在维护名为 "Prof. Kahan's papers" 的个人页面,持续抨击各语言编译器为性能牺牲浮点严格性(如允许 FMA 重排、x87 80 位中间精度)的行为。视频里"这标准比很多人想的严谨得多"的潜台词,出处就在这里。

Goldberg 的传世论文

要往深处走,绕不开 1991 年发表、至今仍被无数大学课程列为必读的论文 《What Every Computer Scientist Should Know About Floating-Point Arithmetic》(David Goldberg,Sun 微系统)。论文用严格的误差分析证明:IEEE 754 在规格化范围内的运算满足"半 ULP 舍入误差界",并系统阐述了为何"用浮点做数值算法"在数学上完全可控。本文批判性审查一节的很多结论,标准出处就是它。

三场真实的浮点灾难

视频本身偏原理,但理解其价值最好的方式是看历史代价:

  1. 爱国者导弹拦截失败(1991 年,宰赫兰)。系统以 0.1 秒为节拍计时,而 0.1 在二进制里无限循环,系统用 24 位定点数截断存储,每次产生约 0.0000001 秒的舍入误差。连续运行约 100 小时后,累计时钟漂移约 0.34 秒,足够让雷达预测的目标位置偏差近 700 米,拦截弹从假目标旁飞过,来袭飞毛腿命中兵营,28 名士兵死亡。事故调查报告(GAO/IMTEC-92-26)明确点名浮点/定点表示误差是根因——正是本文主题的第一滴血。
  2. 阿丽亚娜 5 号首飞爆炸(1996 年)。火箭升空 37 秒后自毁,损失约 3.7 亿美元。调查结论:惯性导航代码直接沿用阿丽亚娜 4 的模块,将一个 64 位浮点数转换为 16 位整数时溢出,未被捕获的异常触发火箭整体自毁。这是"浮点与整数表示边界"最昂贵的教训——一个在旧火箭数值范围内永远安全的转换,在新火箭更快的加速度下爆了。
  3. 温哥华证券交易所指数(1982 年)。指数计算用截断而非四舍五入处理小数,每次交易损失不到千分之一的"零头",但几十万次交易累积下来,指数在约一年半内被低估近一半:真实约 1000 点的指数显示为 500 多点。它是"每次误差极小 × 海量迭代 = 巨大灾难"的教科书案例。

语言层的救赎与代价

正因二进制浮点对"十进制人类"天生不友好,主流语言都提供了十进制浮点/定点方案:Python 的 decimal.Decimal、Java 的 BigDecimal、C# 的 decimal(128 位十进制浮点)、数据库的 NUMERIC 类型。它们的代价是:运算通常比硬件浮点慢一个数量级以上(软件模拟 vs 专用电路)。Kahan 还发明了 Kahan 求和算法(compensated summation),用一个补偿变量把累加误差从 O(n·ε) 压到接近 O(ε),Python 标准库的 math.fsum 就是其精密版实现。

四、批判性审查:哪些是真知,哪些被夸大

证据充分的部分

"0.1 无法被二进制精确表示"是可被逐位验证的数学事实,任何语言、任何硬件上一致复现,这部分毫无争议。IEEE 754 的内存布局、舍入规则(默认 round-to-nearest-even,银行家舍入)、特殊值语义,都有标准文本和四十年硬件实践背书。可以说这是计算机科学里验证成本最低的知识之一——你现在就可以打开终端亲眼看。

容易被带偏的部分

其一,"浮点数是随机的/不可靠的"是常见过度引申。恰恰相反,IEEE 754 的最大成就就是让浮点运算完全确定:同样的操作序列在任何合规硬件上产生完全相同的位结果。真正的问题是它不满足人类对十进制精确性的直觉,而不是它不稳定。视频若只讲到"你看,出错了!"就停,确实容易造成这种误读——这正是需要用 Goldberg 论文补上的一层:在误差界内使用浮点做科学计算,是完全严谨的工程实践,天气预报、流体力学的千亿行代码都建立在它之上。

其二,"所以你应该到处用 Decimal"是流行但昂贵的建议。十进制类型不是免费的:Python Decimal 比原生 float 慢一个数量级左右,且很多科学计算库(NumPy/PyTorch 底层)根本不支持。正确的工程判断是分场景:账务、计费、库存用整数最小单位(分)或 Decimal;图形渲染、机器学习、信号处理继续用 float——后者的世界本来就是近似的,GPU 甚至主动拥抱更低精度的 FP16/BF16 换取吞吐量。

其三,视频年代较早,未覆盖的后续发展值得补充。2019 年后 Python 3.12 之前 repr 已改为输出"最短往返表示"(所以现在打印 0.1 + 0.2 在 Python 里显示 0.30000000000000004,而 0.1 显示 0.1 而不是那串 0555…);2022 年起 Intel x86 已逐步放弃 80 位 x87 中间精度路线,全面转向 SSE/AVX 与 IEEE 严格语义;AI 推理领域 FP8 格式也已进入实用。视频教的原理不变,但生态在演进,阅读时应有这个时间意识。

需要澄清的一个常见讹传

流传甚广的说法" floats 存钱会随机丢几分钱"并不准确——浮点误差是系统性的、方向确定的(二进制循环小数被舍入),不是随机噪声。这个区分很重要:因为系统性和确定性,它才可以被预测、被补偿(Kahan 求和)、被结构性规避(换十进制或整数);如果是随机的,这些手段全都无效。把"确定性误差"误当成"随机不可靠",会导致错误的工程决策(比如放弃 float64 去追求并不解决问题的"更高精度魔法")。

五、认知提升与实用启示

启示一:给"相等判断"立一条铁律

从今天起,代码里永远不要出现 floatA == floatB。替代方案按优先级:绝对误差比较 abs(a-b) < 1e-9(数值范围已知且小时);相对误差比较 abs(a-b) <= epsilon * max(abs(a), abs(b))(数值跨度大时);Python 里直接用 math.isclose(),它两者兼顾且默认参数合理。把这条写进团队 code review 的 checklist——它是浮点错误里发生率最高、却最容易预防的一类。

启示二:货币计算的"三不原则"

不用 float 存金额;不做浮点除法后四舍五入再累加;不在数据库里用 FLOAT/DOUBLE 存钱。正确姿势二选一:以最小货币单位整数存储(美分/分),或用 Decimal/NUMERIC。一个实用的自检问题:"这个数会有用户拿着计算器对账吗?"——只要有,就属于十进制世界,禁用二进制浮点。

启示三:AI 时代的特殊意义——你必须是那个看得懂的人

2026 年的现实是:大量代码由 AI 生成。但 AI 生成的代码恰恰高频复现人类语料里的经典错误——用 float 存价格、用 == 比较浮点、在循环里朴素累加上万次浮点数。这些代码看起来能跑、测试还可能通过(误差恰好小于断言阈值),上线后在真实数据规模下才爆雷。底层原理(哪些数在二进制里循环、误差往哪个方向漂移、累积速度多快)目前仍是人类工程师的护城河——AI 能写出 Decimal,但判断"这里必须用 Decimal"的责任在你。这条视频十几分钟的投入,本质上是在给你的代码审查能力上保险。

六、视频论证结构图

graph TD
    A[根本矛盾: 无限实数 vs 有限比特] --> B[方案一: 定点数]
    B --> B1{缺陷: 范围与精度不可兼得}
    B1 --> C[方案二: 二进制科学计数法<br/>= 浮点数]
    C --> D[IEEE 754 布局<br/>符号1 + 指数8 + 尾数23]
    D --> E[设计巧思: 隐藏首位1<br/>偏移指数127]
    D --> F[核心代价: 0.1 = 0.0001100...二进制无限循环]
    F --> G[舍入后: 0.1+0.2 = 0.30000000000000004]
    D --> H[间隔ULP不均匀: 精度是相对的]
    H --> I[大数吞小数: i+1==i 可复现]
    G & I --> J[工程防线]
    J --> K[epsilon比较 / math.isclose]
    J --> L[货币: 整数分 或 Decimal]
    J --> M[Kahan求和 / math.fsum]
    D --> N[特殊值逃生口: Inf / NaN / 非规格化数]
    N --> O[确定性成就<br/>任何硬件结果一致]
    O --> P[历史代价: 爱国者1991 / 阿丽亚娜5 1996 / 温哥华指数1982]

延伸阅读

  1. 《What Every Computer Scientist Should Know About Floating-Point Arithmetic》(David Goldberg,1991)——浮点数领域的"圣经"论文,视频所有原理的严格数学出处,Oracle/Sun 原版全文可免费阅读。
  2. Python 官方教程第 14 章 "Floating Point Arithmetic: Issues and Limitations"——用最短的篇幅讲清 Python 视角的浮点表示与 math.fsum/Decimal 实践,标准库文档里的宝藏章节。

参考来源

  1. Computerphile — Floating Point Numbers(YouTube 频道页) — 本文解读的主视频;因本次采集环境网络受限,未能附上可直接核验的 watch 链接,请在频道内搜索标题获取,播放数据以频道页面实时显示为准。
  2. David Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic(Oracle/Sun 存档) — 1991 年经典论文全文。
  3. Python 官方教程:Floating Point Arithmetic — 0.1 的表示、最短 repr 与 math.fsum 的官方说明。
  4. GAO 报告 GAO/IMTEC-92-26:Patriot Missile Time-Tracking Problem — 1991 年宰赫兰拦截失败事故的官方调查报告,浮点/定点截断误差致 28 人死亡的原始出处。

本文由「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学通信知识星球海报-扫码加入