首页 › programming › 0.1+0.2≠0.3:浮点为何算不准

0.1+0.2≠0.3:浮点为何算不准

每一个写过几年代码的人,几乎都被同一个"幽灵"咬过一次:你在 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。视频的功劳在于——它让"无限循环"这件事变得可视,而不是一句抽象的"精度有限"。

二、IEEE 754 的三段式:符号、指数、尾数

要理解误差从哪一比特渗进来,必须看懂 double 的内部布局。视频把 64 个比特分成了三段,这是 IEEE 754 标准的铁律:

合起来,一个 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. 灾难性抵消(catastrophic cancellation):两个非常接近的数相减,有效数字会成片丢失。比如 1.0000001 - 1.0,原本的高精度瞬间蒸发,相对误差爆炸。这是数值算法里最隐蔽的陷阱。
  2. 溢出(overflow):指数超过上限就变成 ±∞。Ariane 5 火箭的悲剧本质上和它同源——超出了表示范围。
  3. 下溢(underflow):结果太小、逼近零时,会掉进"次正规数(subnormal)"区间,精度骤降,最终塌缩成 0。很多概率连乘的算法,算着算着就变成 0,根因往往在此。

五、灾难现场:误差真的会杀人

把上面的理论放进真实世界,浮点误差的代价是血淋淋的,这也是这篇文章比视频多走的一步——

海湾战争·爱国者导弹(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 点——几乎腰斩。一个舍入策略的选择错误,让整个市场的"温度计"长期失灵。

这三个案例的共同点是:单次误差小到肉眼看不见,累积起来却能摧毁一座兵营、一枚火箭、一个交易所。 浮点的问题从来不是"算错一个数",而是"错误在沉默中复利"。

六、特殊值不是 bug,是设计:±0、±∞、NaN

视频顺带解释了 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,这在所有语言里通用。把看似的缺陷变成检测手段,这是顶级标准才会有的品味。

七、工程师的对策:比较、定点、补偿求和

知道了病因,对策也就清晰了。视频和业界沉淀出几条铁律:

知识结构图

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 秒、温哥华证交所的逐笔截断,本质上都是"单次无害、累积致命"。当你写任何循环、任何长时间运行的累加、任何自增计数器时,问一句:误差会复利吗?如果是,就得主动管理它。浮点最危险的地方,从来不是它算错,而是它安静地、持续地算错。

延伸学习路径

  1. 《What Every Computer Scientist Should Know About Floating-Point Arithmetic》(David Goldberg 著)——浮点领域的"圣经级"文献,看完你会对所有舍入行为有数学级的掌控。
  2. IEEE 754 在线可视化工具(如 DevToolbox 的 IEEE754 Inspector / DigiSim)——把你输入的任意小数实时翻译成 64 比特,亲手验证 0.1 的真实存储值。
  3. 《Floating Point — Numberphile》——另一个经典科普视角,用更轻松的方式讲同一件事,适合搭配本文视频一起看。
  4. Python decimal 模块官方文档——从工程角度落地"用十进制定点算钱",里面有大量可复制的财务计算范式。
  5. 《An Interview with the Old Man of Floating-Point》(William Kahan 访谈)——听 IEEE 754 架构师本人讲标准背后的取舍,理解那些"奇怪"设计从何而来。

一句话总结

浮点数从来不是"算错了",而是用 53 个比特去装一个无限的小数——它教会程序员的,是永远不要相信光滑的数轴,要用容差、定点和补偿,去管理那个注定存在的缝隙。

参考来源

  1. Why 0.1 + 0.2 ≠ 0.3 — Floating Point, Explained Visually — 本文深度解读的核心视频
  2. Disasters due to rounding error — University of Texas at Austin — 爱国者导弹、温哥华证交所等真实灾难的技术归因
  3. How one line of code brought down a €500M rocket launch — Jam Blog — 阿里亚娜 5 号浮点转整数溢出事故详解
  4. IEEE Standard 754 Floating Point Numbers — GeeksforGeeks — 符号/指数/尾数三段式与偏移编码的规范说明
  5. Real-World Floating-Point Bugs — DevToolbox — 经典浮点 bug 案例库与可视化检查工具

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