打开任何一个编程语言的 REPL,敲下 0.1 + 0.2,Python 会一本正经地回你 0.30000000000000004,JavaScript、Java、C、Rust 全都一样。这不是 bug,这是设计。Computerphile 的这期《Floating Point Numbers》就用十分钟讲透了一个反直觉的事实:计算机里的小数,本质上不是"数",而是一种"带精度限制的科学记数法"。它快得离谱,却天生算不准钱。
为什么这件事值得每个程序员认真看一遍?因为浮点数是这个行业里最普遍、最隐蔽、又最被低估的陷阱。它不是面试八股,它是真的会炸火箭、漏导弹、让股票指数凭空蒸发掉一半的隐形杀手。这门"必修课"大多数人从来没系统学过,全靠线上事故被动补课。这篇解读会把视频的核心论点拆开重组,再补上视频没展开的真实灾难、设计者 William Kahan 的天才之处,以及你在工程里真正该怎么做。
| 项目 | 内容 |
|---|---|
| 视频标题 | Floating Point Numbers - Computerphile |
| 频道 | Computerphile(Brady Haran 出品,Numberphile 姊妹频道,订阅约 250 万+) |
| 主讲人 | Robert Miles(诺丁汉大学计算机系) |
| 发布日期 | 2014-01-22 |
| 时长 | 约 10 分钟(系列含 Part 1 浮点 vs 定点、Part 2 浮点加法) |
| 播放量 | 百万级(Computerphile 最经典高播放视频之一) |
| 核心主题 | 浮点数如何用二进制科学记数法表示、为什么算不了钱 |
这是整个视频最该记住的一句话。我们小学学的科学记数法是 1.234 × 10⁵ 这种形式——一个尾数乘以一个底数的若干次方。浮点数把这件事搬到二进制:符号位 × 尾数(significand) × 2^指数。一个 64 位 double(IEEE 754 binary64)的 64 个 bit 被切成三段:1 位符号、11 位指数、52 位尾数。因为正规化的二进制小数永远以 1.xxxx 开头,那个开头的 1 不用存,凭空白送你一位精度——这就是著名的"隐含位(hidden bit)",也是 Kahan 设计上的小巧思。
理解了"科学记数法"这个本体,后面所有反直觉现象都有了解释。尾数决定精度(double 大约 15–17 位有效十进制位),指数决定范围(double 能表示从约 2.2e-308 到 1.8e308 这个匪夷所思的跨度)。精度和范围从来是同一个跷跷板的两端,浮点数用"有限位数 + 可滑动的小数点"换来了极大的表示范围,代价是大部分实数它根本存不下来,只能存"最近的那个能表示的值"。这是后续一切舍入误差的根源。
视频里 Robert Miles 反复强调一个观点:浮点数是"带精度限制的近似"。把 0.1 这个十进制数翻译成二进制,你会得到 0.0001100110011... 一个无限循环小数——就像十进制里 1/3 = 0.333... 永远写不完一样。但 double 只有 52 位尾数,于是循环部分被硬生生截断、舍入。存进去的 0.1 其实是 0.1000000000000000055...,0.2 也一样是近似值。两个近似值相加,误差叠加,结果就变成了 0.30000000000000004。
这是我的延伸:关键不是"误差很小可以忽略",而是"浮点数本质上是离散网格上的点"。整个实数轴上,double 只能精确命中极少数的点(那些能被 2 的幂次组合出来的数,比如 0.5、0.25、0.75),其余全是四舍五入到最近网格点的近似。而且这个网格不是均匀的——数字越大,网格间距越大(这就是 ULP,unit in the last place,"最后一位单位")。在 1 附近,相邻两个 double 相差约 2.2e-16;到了 1e16 附近,相邻两个 double 之间的间距已经大于 1,也就是说 1e16 + 1 == 1e16,那个 1 直接被吞掉了。这叫"大数吃小数",是浮点数最坑人的特性之一。
视频给了一个很直观的对比数据:同样一段计算,用浮点数跑比用定点数跑大约快 4.5 倍(这也是 Computerphile 视频里 Robert Miles 点出的核心卖点)。原因是浮点运算有专门的硬件浮点单元(FPU)支持,正规化的格式让硬件加减乘除可以流水线化、并行化,指数对齐、尾数移位都是专用电路一条龙搞定。现代 CPU/GPU 的浮点峰值算力(TFLOPS)就是这么堆出来的,AI 训练、3D 渲染、物理仿真全靠它。
但这笔买卖的不完美之处在于:你买到的是"快",不是"准"。对于图形渲染、科学计算、机器学习这些领域,0.0001 的误差完全无所谓,甚至模型本身就是概率近似——浮点数的速度优势是碾压性的,精度损失可以接受。视频里那句"Why can't floating point do money?"正是这个权衡的反面:钱的场景要求精确,0.1 元 + 0.2 元必须等于 0.3 元,一分一厘都不能错。于是浮点数在金融系统里是禁区,这是行业铁律。
这是视频点到但没深挖、而我特别想展开的部分。IEEE 754 之所以被称为天才设计,很大程度上是因为它规定了一整套特殊值的语义,让计算在异常情况下不崩溃、错误还能被传播和观测:
1/+0 = +Inf,1/-0 = -Inf,这在处理极限方向时极其有用。1.0 / 0.0 = Inf,后续计算可以继续,错误以"传染"的方式显现,而不是当场爆炸。0/0、sqrt(-1)、Inf - Inf 这类无定义运算的结果。最著名的坑是 NaN ≠ NaN 返回 true——因为 NaN 表示"我不知道这是什么值",两个"不知道"自然不相等。所以你永远不能用 x != x 之外的方式检测 NaN,== 对它失效。a - b(当 a、b 都很小)这类运算的相对误差保持可控。这一整套设计的精神是:让异常变得"可计算"而非"致命"。在 754 之前,不同厂商的机器浮点格式各不相同,一段数值代码换个机器结果就变,NaN 这种东西更是不存在——出错直接停机。Kahan 用一套标准把整个行业的"数值可移植性"问题解决了。
理论讲完,来看真实的血泪。这些是我补充的、视频未涉及但最能震慑人的案例:
爱国者导弹(1991 年 2 月 25 日,沙特宰赫兰)。这是史上最著名的浮点事故之一,美国 GAO 的报告编号 IMTEC-92-26 写得清清楚楚。爱国者系统用一个 24 位定点寄存器,以 1/10 秒为单位累计时间。问题来了:1/10 在二进制里是无限循环小数,被截断后产生微小舍入误差。系统连续运行约 100 小时后,误差累积到约 0.3 秒。导弹以拦截窗口靠这个时间定位来袭的飞毛腿导弹,0.3 秒的偏差意味着雷达把目标锁在了错误的位置——结果那枚飞毛腿直接命中美军兵营,28 名士兵阵亡。一个 0.1 秒单位的舍入,漂成了 0.3 秒,要了 28 条人命。
阿丽亚娜 5 号首飞(1996 年 6 月 4 日,Ariane flight V88)。欧洲航天局的旗舰火箭,首飞起飞约 37 秒后自行解体,连带的 4 颗 Cluster 卫星一起报销,损失超过 5 亿美元。事后调查(ESA Inquiry Board)揪出的根因令人扼腕:火箭的水平速度作为一个 64 位浮点数,被一段从阿丽亚娜 4 号原封不动继承过来的"死代码"转换成 16 位有符号整数。阿丽亚娜 5 的速度比 4 号快得多,数值超出了 16 位整数上限 32767,触发溢出异常,导航计算机崩溃,火箭误判姿态失控,启动自毁。一段本该删掉的代码,因为"它在 4 号上能跑"就被保留了下来——浮点和整数之间的边界,从来不是想当然的安全区。
温哥华股票交易所指数(1982 起)。这是个更隐蔽的慢性中毒。交易所的指数计算程序在更新时把结果截断(向下舍入)到 3 位小数,而不是四舍五入。每一次微小向下偏差不断累加,22 个月里指数从 1000 点被生生"算"成了约 520 点,凭空蒸发了近一半,而真实市场根本没跌那么多。重新用正确算法计算后,指数一夜跳回约 1098。舍入方向选错,时间一长就是系统性失真。
这三个案例覆盖了浮点坑的三种典型形态:累积漂移(爱国者)、范围溢出(Ariane 5)、舍入方向偏差(温哥华)。它们共同说明一件事——浮点数的错误几乎从不是"立刻报错",而是沉默地、缓慢地、在边缘条件下腐蚀你的系统,直到某天撞上一个临界点爆雷。
回到设计层面。IEEE 754-1985 标准的灵魂人物是加州大学伯克利分校的 William Kahan,业界尊称"浮点之父",1989 年获 ACM 图灵奖。他的贡献远不止"定个格式"——他设计了一套让浮点运算在数学性质上尽可能可预测、可移植、可诊断的体系:上述的渐进下溢、有符号零、NaN/Inf 传播、以及严格的舍入规则(默认"就近舍入到偶数",避免统计偏差),都是他坚持的结果。
Kahan 还写过一个叫 Paranoia 的程序(1980 年代),专门用来诊断一台机器的浮点实现是否合规、有哪些缺陷——这种"主动给标准配上体检工具"的工程自觉,在今天看来依然超前。他和学生还贡献了被广泛使用的 fdlibm 数学库。可以说,今天全世界几乎所有 CPU、GPU、编程语言的 float/double 行为一致、跨平台可复现,都要追溯到 1985 年那套标准和 Kahan 的执拗。标准后来在 2008、2019 年修订扩充(加入了十进制浮点、半精度 binary16 等),但骨架未变。
flowchart TD
A["浮点数 IEEE 754<br/>= 二进制科学记数法<br/>符号位 × 尾数 × 2^指数"] --> B["存储结构 binary64"]
A --> C["核心权衡"]
A --> D["特殊值设计"]
A --> E["失败模式"]
A --> F["工程解药"]
B --> B1["符号位 1 bit"]
B --> B2["指数 11 bit 偏移1023<br/>决定范围 ~1e-308..1e308"]
B --> B3["尾数 52 bit + 隐含1<br/>决定精度 ~15-17 位十进制"]
C --> C1["速度:硬件 FPU<br/>比定点快约 4.5 倍"]
C --> C2["代价:舍入误差<br/>只能精确命中少数实数"]
D --> D1["+0 / -0 有符号零"]
D --> D2["+Inf / -Inf 溢出不崩"]
D --> D3["NaN ≠ NaN"]
D --> D4["次正规数 渐进下溢"]
E --> E1["0.1+0.2≠0.3<br/>二进制无限循环小数"]
E --> E2["大数吃小数 累加误差<br/>ULP 随数量级变大"]
E --> E3["灾难性抵消<br/>相近数相减丢精度"]
E --> E4["真实事故<br/>爱国者/Ariane5/温哥华交易所"]
F --> F1["算钱用整数或 BigDecimal"]
F --> F2["比较用 epsilon 容差<br/>禁用 =="]
F --> F3["Kahan 求和算法"]
F --> F4["关键计算用 Decimal/高精度库"]
看完上面这些,最该带走的是一套可执行的纪律,而不是一堆恐惧。我把它们按场景给你拆开:
第一,凡是涉及钱的,永远不要用 float/double。 这是最硬的一条铁律。正确做法三选一:用最小货币单位的整数(比如人民币用"分",金额乘以 100 存成 int);用语言自带的 Decimal 类型(Python 的 decimal.Decimal、C# 的 decimal);用 Java 的 BigDecimal。BigDecimal 还要注意用字符串构造(new BigDecimal("0.1")),千万别用 new BigDecimal(0.1),否则那个 0.1 在传入之前就已经被 double 近似污染了。
第二,永远不要用 == 比较浮点数。 因为相等的"数学值"在浮点里可能差一个 ULP。正确做法是判断差的绝对值是否小于一个容差 epsilon(abs(a - b) < 1e-9),而且对于跨数量级的比较要用相对容差而非绝对容差。如果你用语言内置的近似相等函数(如 Python 3.5+ 的 math.isclose),默认实现就是相对+绝对混合容差,比自己手写更稳。
第三,警惕循环里的累加。 把上百万个小浮点数加起来,误差会以可怕的速度累积,尤其当你用"从大到小"的顺序累加时——大数会吃掉后面所有小数的贡献。两个对策:先排序再从小到大累加;或者用 Kahan summation 算法,它用一个"补偿项"把每次丢失的低位精度捡回来,几乎能恢复到接近全精度的结果。Numpy 的 np.sum 在很多实现里底层就用了成对求和(pairwise summation)来缓解这个问题。
第四,远离"灾难性抵消"。 当你把两个非常接近的大数相减,结果的相对误差会被放大几个数量级(因为前导有效位全部抵消掉了)。经典例子是方差计算:用"平方的均值减去均值的平方"这个教科书公式,在数值上极不稳定,应该用 Welford 算法这种在线、数值稳定的版本。凡是公式里出现"两个接近的大数相减",都要本能地警觉。
第五,给关键计算留出"精度预算"。 在做物理仿真、几何运算、迭代求解时,心里要有个数:这一路算下来会损失多少有效位?关键判定(比如"是否碰撞""是否收敛")的阈值要和这个误差预算匹配,别把 1e-15 量级的抖动当成真实信号。对于必须精确的场景(比如区块链余额、投票计数、科学计量基准),上 Decimal 或定点库,慢一点但绝不会算错。
浮点数是用速度换精度的二进制科学记数法——它让人类算力爆炸,却也用沉默的舍入误差在金融、航天、国防里埋下了无数颗延时引信;真正成熟的程序员,是在用它的速度的同时,永远清楚它在哪里、以什么方式"骗"了你。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
youtube_transcript.py,因云 IP 被 YouTube 封锁未能取到字幕(符合预期),改基于视频描述 + Glasp 摘要 + 深度技术知识完成解读mermaid-generator 或 knowledge-card-pro 将"知识结构图"生成独立图片content-to-video 或 book-to-short-videos skill[!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 等场景全覆盖。
