每一个写过几年代码的人,几乎都被同一个"幽灵"咬过一次:你在 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。视频的功劳在于——它让"无限循环"这件事变得可视,而不是一句抽象的"精度有限"。
要理解误差从哪一比特渗进来,必须看懂 double 的内部布局。视频把 64 个比特分成了三段,这是 IEEE 754 标准的铁律:
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.0000001 - 1.0,原本的高精度瞬间蒸发,相对误差爆炸。这是数值算法里最隐蔽的陷阱。±∞。Ariane 5 火箭的悲剧本质上和它同源——超出了表示范围。把上面的理论放进真实世界,浮点误差的代价是血淋淋的,这也是这篇文章比视频多走的一步——
海湾战争·爱国者导弹(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 点——几乎腰斩。一个舍入策略的选择错误,让整个市场的"温度计"长期失灵。
这三个案例的共同点是:单次误差小到肉眼看不见,累积起来却能摧毁一座兵营、一枚火箭、一个交易所。 浮点的问题从来不是"算错一个数",而是"错误在沉默中复利"。
视频顺带解释了 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。注意容差要按量级缩放,不是写死一个魔法数。decimal、Java 的 BigDecimal、C# 的 decimal、数据库的 DECIMAL——或者干脆以"分"为单位存整数。银行系统永远在整数上算钱。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 秒、温哥华证交所的逐笔截断,本质上都是"单次无害、累积致命"。当你写任何循环、任何长时间运行的累加、任何自增计数器时,问一句:误差会复利吗?如果是,就得主动管理它。浮点最危险的地方,从来不是它算错,而是它安静地、持续地算错。
0.1 的真实存储值。decimal 模块官方文档——从工程角度落地"用十进制定点算钱",里面有大量可复制的财务计算范式。浮点数从来不是"算错了",而是用 53 个比特去装一个无限的小数——它教会程序员的,是永远不要相信光滑的数轴,要用容差、定点和补偿,去管理那个注定存在的缝隙。
本文由「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 等场景全覆盖。
