程序员入职第一周就会遇到那个著名的灵异事件:0.1 + 0.2 不等于 0.3,而是一个长达十七位的 0.30000000000000004。绝大多数人的反应是背下结论——“浮点有精度问题”,然后一辈子没弄清精度问题到底出在哪一位上。Computerphile 频道的经典视频《Floating Point Numbers》就是补这一课的:主讲人 Mike Pound 拿着纸笔,把“电脑如何存小数”这件事从二进制的第一性原理推了一遍。这条视频上线十年来长期是该频道播放量最高的一批视频之一,“0.1 加 0.2”更是程序员社区永远复读的梗,甚至催生了专门的网站 0.30000000000000004.com。本文按视频的叙事顺序拆解其论证,补上视频没讲到的理论出处与真实工程事故,并给出可以直接落地的避坑建议——看完这篇,你对浮点数的理解会比看完视频还多一层。
| 项目 | 内容 |
|---|---|
| 视频标题 | Floating Point Numbers(浮点数) |
| 频道 | Computerphile |
| 主讲人 | Dr. Mike Pound(诺丁汉大学计算机科学系) |
| 上线时间 | 2014 年前后上线,长期常青 |
| 热度 | 数百万播放量级;“0.1+0.2”话题在 Reddit、Twitter/X 等社区常年被反复引用 |
| 内容形式 | 纸笔手推式白板讲解,约 20 分钟 |
注:本篇制作时未能实时抓取频道页面,上述时间与热度为量级描述,精确数字请以频道页面为准。
Mike Pound 的讲法非常克制:不用 PPT,不堆术语,就是从“二进制怎么表示数”开始,一步一步推到“为什么 0.1 会出事”。整条视频的论证链条可以拆成五步。
视频的起点是让听众安心:整数部分没有问题。8 个比特可以无损地表示 0 到 255,因为整数的二进制表示是有限的。真正的问题从“小数点”出现的那一刻开始。二进制里小数点后的第一位是 1/2,第二位是 1/4,第三位是 1/8——每一个位置只能是十进制分数 1/2 的幂次。这意味着:凡是能写成 2 的幂次之和的小数,二进制存得干干净净(比如 0.5、0.25、0.375);凡是不能的,比如十进制的 0.1,二进制里就是无限循环小数。0.1 换算成二进制是 0.000110011001100…,那个 0011 永远循环下去。而计算机的存储位数是有限的,无限循环必须在哪里停下来——这就是一切事故的种子。
视频顺着推:既然只能存有限位,最朴素的方案是“定点数”——固定小数点后保留 N 位,多余的统统截掉。但定点数有个致命缺陷:它把表示范围锁死了。你想表示原子直径那么小的数,也想表示国家预算那么大的数,定点数就得保留同样多的位数,极其浪费。Mike Pound 用了一个非常直观的类比:科学家写大数从来不写一长串零,而是写科学记数法——6.022 × 10²³。真正携带信息的是“6.022”这几位有效数字和“23”这个指数。把这套思路搬进二进制,让小数点可以“浮动”,就是 floating point 这个名字的由来:小数点的位置不再固定,而是由一个专门的“指数”字段来指挥。
视频的核心段落是在纸上画出浮点数的内存布局。以 32 位单精度(float)为例,32 个比特被切成三段:
这里藏着视频里最精巧的一个细节:隐含的前导 1。规格化的二进制科学记数法里,有效数字永远以 1 开头(就像十进制科学记数法永远以非零数字开头),既然永远是 1,那就没必要存它——23 位尾数实际能表达 24 位的精度,凭空多赚一位。讲完单精度,视频顺带对比了 64 位双精度(double):1 位符号 + 11 位指数 + 52 位尾数,十进制有效数字从约 7 位跃升到约 15~16 位。这也是 Mike Pound 给普通开发者的第一建议:没有极端性能压力,就用 double,它能帮你躲掉绝大多数精度尴尬。
理解了布局,很多“灵异现象”瞬间可以自己推导出来,不用背。视频演示的核心机制是:两个浮点数相加时,硬件必须先对齐指数,把小的那个数的尾数右移。右移就会丢位。当两个数大小悬殊时,小数会被“吃得一根毛都不剩”——比如给一个很大的数反复加 0.0001,加了一万次,结果可能纹丝不动,因为 0.0001 在对齐指数时被整个舍入掉了。这直接推出两个结论:第一,浮点加法不满足结合律,(a+b)+c 和 a+(b+c) 可以算出不同结果,加法的顺序本身会影响答案;第二,任何超出有效数字范围的十进制小数,存进电脑那一刻就已经不是“原来的那个数”了——0.1 和 0.2 各自带着一点点偏差,相加后偏差的方向恰好让你看到 0.30000000000000004。
很多人看完视频会把浮点误差理解为“电脑算错了”。Mike Pound 在结尾把态度摆正了:浮点运算不是算错,而是在有限资源下做出的、被精确定义过的近似。IEEE 754 是一份极其严肃的标准,它对每一种运算的舍入方式都做了明确规定(默认舍入到最近偶数),误差是有界的、可分析的。真正的工程问题从来不是“有误差”,而是“不了解误差就从事业务”。视频停在认知层面,而这条认知恰好是无数真实事故的分水岭——这正是下文要补的。
视频本身没有给出处,但它的每一块内容背后都有扎实的文献和血淋淋的工程史。补上这层,你才知道这门课的分量。
视频里那张内存版图,来自 1985 年首次发布的 IEEE 754 标准。它的主要推动者是加州大学伯克利分校的 William Kahan 教授——在此之前,每家硬件厂商的浮点行为各不相同:CDC、Cray、IBM 各有各的格式和舍入规则,同一份 Fortran 程序在不同机器上能跑出不同的结果,数值软件几乎没有可移植性可言。Kahan 主导设计了这套统一的格式与舍入语义,并因此获得 1989 年图灵奖。IEEE 754 后来历经 2008、2019 两轮修订,至今仍是所有主流 CPU、GPU、JavaScript、Python 数值类型的共同地基。
如果只读一篇浮点数文献,就是这个:David Goldberg 1991 年发表在 ACM Computing Surveys 上的《What Every Computer Scientist Should Know About Floating-Point Arithmetic》。视频里所有现象——舍入误差、大数吃小数、加法结合律失效——论文里都有严格的形式化证明和更深的展开(比如“灾难性抵消”:两个相近的数相减,有效数字会断崖式流失)。这篇论文至今仍被 Oracle 官方文档全文收录,是数值计算方向的事实入门教材。
浮点误差最沉重的一次现身,是 1991 年 2 月 25 日的沙特阿拉伯达兰。美军“爱国者”防空系统因时钟以 0.1 秒为单位累加计时——而 0.1 在二进制里同样是无限循环小数,系统用截断方式存储——运行约 100 小时后累积出约 0.34 秒的时钟偏差。对拦截弹道导弹来说,0.34 秒意味着预测位置偏出数百米,系统未能发现并拦截来袭的飞毛腿导弹,美军第 14 军需分队营房被击中,28 名士兵死亡、近百人受伤。美国审计总署(GAO)事后发布报告(GAO/IMTEC-92-26),结论直指浮点与时钟计算的软件缺陷。这条视频讲的每一个比特,在历史上都是真金白银甚至人命换来的。
另一个经典案例更贴近商业:1982 年温哥华证券交易所推出指数时,每次更新都对成分股均值做小数截断(保留三位)。单次截断微不足道,但交易日内指数上万次更新,误差单向累积,到 1983 年 11 月重新核算时,人们发现指数比真实值低了近一半——一家公司上市三年,被“舍入方式”蒸发了超过一半的账面市值。它与爱国者导弹共同说明视频要点四的本质:单次误差无害,误差的系统性累积才是杀手。
这里要替浮点数说句公道话,也是多数转述文章搞混的地方。1996 年阿丽亚娜 5 号火箭首飞爆炸,常被说成“浮点溢出”,实际原因是 64 位浮点数转 16 位整数时的整数溢出;1999 年火星气候探测器坠毁,则是公制与英制单位混用。数字事故的形式远比“浮点不准”多样:整数溢出、单位混乱、截断误差各有各的死法。把每类误差的机制分清楚,才算真正看懂了这条视频。
视频没来得及讲的时代注脚:今天的 AI 芯片大规模使用 16 位甚至 8 位浮点(fp16、bfloat16),用牺牲精度换取吞吐量——因为深度学习对单次运算精度的容忍度远高于对算力的需求。IEEE 754 后续修订也为此加入了对应格式。同一个标准,四十年前追求“统一而可靠”,四十年后服务于“可控的不精确”,这条演化线本身就很值得玩味。
证据充分的部分:视频对二进制分数、IEEE 754 布局、舍入机制的讲解与标准和教科书完全一致,属于可以放心照单全收的硬知识;0.1 不可精确表示、double 约 16 位有效数字、大数吃小数,这些都能在任何一本数值分析教材和 Goldberg 论文中得到印证。
被社区夸大的部分:围绕这条视频衍生出的“电脑根本不会算数”式 meme 是典型的以讹传讹。恰恰相反,IEEE 754 是精心设计到“每一次运算的误差都有严格上界”的标准,比人手算可靠得多。真正的问题从来是使用者不了解表示边界,而不是标准不可靠。
视频为入门做了简化的部分:第一,视频没有展开非规格化数(subnormal)、±0、无穷与 NaN 的特殊语义——比如 NaN 不等于自身、0.1+0.2 的显示值取决于语言的最短往返表示法(Python 3 的 repr 会自动显示 0.1 而不是一长串尾巴,直到你做运算才暴露);第二,视频没有给出误差控制手段,比如补偿求和(Kahan summation)可以在累加时把丢失的低位“捡回来”;第三,视频暗示“换 double 就好”,但对金融、计费场景,正确答案不是 double,而是下一节的 decimal——十进制类型能精确表示 0.1,因为它直接按十进制位存储,这也是为什么 Java 有 BigDecimal、Python 有 decimal 模块、数据库有 DECIMAL 字段。当然 decimal 也不是万能药:1/3 在任何进制下都是无限循环,十进制解决的是“我们以十进制书写的量”的精确性,而不是消除一切循环小数。
第一条:钱的事,永远不要交给二进制浮点。 金额字段用整数(以“分”为单位)或 decimal 类型,这是计费、支付、财务系统的铁律。二进制浮点存不了 0.1,就意味着对账时总会冒出一分钱的幽灵差异。同理,JavaScript 里处理第三方接口返回的雪花 ID 这类超过 2^53 的整数时,要当字符串处理,否则 Number 类型会在 15 位之后悄悄改写你的数字。
第二条:浮点比较不用 ==,累加要管顺序。 判断两个浮点数相等,用“差的绝对值小于容差”的方式;容差最好同时考虑绝对阈值和相对阈值。对海量数据累加,如果结果要跨平台、跨机器复现(比如风控、结算、科学计算),要么采用固定顺序的分块归约,要么直接上 Kahan 补偿求和——记住视频要点四:浮点加法没有结合律,顺序就是语义的一部分。
第三条:把“误差思维”带进架构评审。 看到任何涉及计时的系统(爱国者导弹式的累加时钟)、涉及高频更新的统计值(温哥华指数式的滚动截断),问一句:这个误差是双向抵消,还是单向累积?累积路径有没有上限和定期重校准?这类问题便宜,出事之后的代价昂贵。
flowchart TD
A[整数能精确存储] --> B[分数需要二进制小数点]
B --> C[0.1 在二进制里无限循环]
C --> D[位数有限必须舍入]
D --> E[定点数范围受限]
E --> F[科学记数法搬进二进制]
F --> G[IEEE 754 统一格式]
G --> H[符号 指数 尾数三段]
H --> I[单精度约7位有效数字]
I --> J[大数吃小数]
J --> K[加法顺序影响结果]
K --> L[误差单向累积成事故]
L --> M[对策 选对类型与算法]
本文由「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 等场景全覆盖。
