首页 › programming › 小数其实是整数:浮点数内核拆解

小数其实是整数:拆解 IEEE 754 浮点数内核

导语

每个程序员都会撞上同一堵墙:在 IDE 里敲下 0.1 + 0.2,回车,屏幕返回的不是干净的 0.3,而是 0.30000000000000004。你的第一反应大概是"我的代码出 bug 了",第二反应是"这语言真烂"。但真相是:你的代码没 bug,语言也没烂,烂的是"十进制小数"和"二进制硬件"之间那道无法弥合的鸿沟。

这堵墙的名字叫 IEEE 754——一个 1985 年由 William Kahan 主导制定、至今统治着从手机到超算再到 AI 加速卡几乎所有"小数运算"的标准。它用 64 个 bit 装下了从 1e-308 到 1e+308 的整个宇宙,代价是精度永远有限。本期解读的视频《IEEE 754 Explained: How Your CPU Really Stores Decimal Numbers》最大的价值,不在于告诉你"浮点会算错",而在于把那个反直觉的结论彻底拆开给你看:你以为的小数,本质上是一段被当成整数来读的 bit 串,它的"小数性"完全来自一种叫"科学计数法"的解码方式。看懂这一层,你不仅不再害怕 0.1+0.2,还能预测哪里会出错、为什么出错、怎么绕过去。

视频背景卡片

项目 内容
视频标题 IEEE 754 Explained: How Your CPU Really Stores Decimal Numbers
频道类型 计算机科学科普频道(CS Education)
时长 约 12–15 分钟(中等篇幅深度讲解)
影响力 IEEE 754 主题下高互动讲解之一,"0.1+0.2≠0.3"现象的代表性拆解
核心主题 浮点数的位级表示、偏置指数、隐含前导 1,以及舍入误差的根源
适配人群 后端/前端/数据/算法工程师、CS 学生、所有被浮点 bug 坑过的人

核心内容拆解

一、所谓"小数",其实是 64 个 bit 的三段式布局

视频开篇就给出了最关键的一击:一个 double(双精度浮点),在内存里和 long 整数长得一模一样——就是连续的 64 个 bit,没有任何"小数标记"。区别只在于你怎么读它。IEEE 754 把这 64 位切成三段:

我的延伸判断:这正是为什么很多老派程序员喜欢把 float 和 int 互相 reinterpret——同一块内存,换种"读法"就能从整数秒变浮点。浮点数之所以叫"浮点",不是因为它的值在飘,而是因为那个二进制小数点的位置会随着指数浮动。它本质上是"二进制版的科学计数法":值 = (-1)^sign × 1.mantissa × 2^(exponent - bias)。记住这个公式,后面所有现象都是它的推论。

二、偏置码(Bias):把指数也变成无符号整数

视频详细解释了一个看似古怪的设计:指数字段里存的不是真实指数,而是"真实指数 + 1023"(double 的 bias 是 1023,单精度 float 的 bias 是 127)。这叫偏置(biased exponent)。

为什么要加这个偏置?我的补充分析:这是硬件工程师的优雅权衡。如果不偏置,指数就得用补码表示有符号数,比较两个浮点数大小时要先做符号判断,电路复杂。加上偏置后,指数字段永远是 0–2047 的无符号整数,于是比较两个正浮点数的大小,退化成比较 64 位整数的大小——一条 CPU 指令搞定。这就是为什么早期没有 FPU 的机器也能"凑合"做浮点比较:把 bit 串当整数比就行。一个看似多余的 +1023,换来了硬件的极简。

三、隐含的前导 1:白送一位精度

科学计数法里,正规化后的数形如 1.xxxxx × 2^n,小数点前面那个 1 永远存在。视频指出,IEEE 754 干脆不存这个 1——它默认就在那。于是 52 位的尾数字段,实际提供了 53 位精度,等于白捡了一位。

这条规则有个例外:当指数为 0 时(即非规格化数,denormal),这个隐含的 1 变成 0。这个设计让浮点数能在接近 0 时"优雅衰减",而不是突然归零——下一节会看到它的重要性。

四、为什么 0.1 死活存不进去:二进制的无限循环

这是全片的高潮,也是 0.1+0.2≠0.3 的真正病根。问题不在硬件,在进制。十进制的 0.1,转成二进制是 0.00011001100110011...——一个无限循环小数,就像十进制里 1/3 = 0.333… 一样永远写不完。

可尾数只有 52 位(算上隐含 1 是 53 位),硬件只能把它截断并舍入到最近的可表示值。于是内存里存的不是真正的 0.1,而是 0.1 的一个"近似替身"。0.2 同理。两个替身相加,误差叠加,结果就落在了 0.3 的替身和真值之间的缝隙里,于是你看到了那个 …00000004。

我的延伸洞察:这跟"四舍五入"是同一个问题,只不过发生在二进制。只要一个分数的分母含 2 以外的质因子(比如 0.1=1/10 含 5,0.3=3/10 含 5),它在二进制下就是无限循环,就注定存不精确。反过来,0.5、0.25、0.125 这些 2 的幂次分母的分数,在浮点里是"干净"的。理解这一点,你就拥有了预测力:哪个数会算错,从一开始就能看出来。

五、浮点数不是均匀分布的:越靠近 0 越密

视频揭示了第二个反直觉事实:64 个 bit 看起来均匀,但它表示的数值却不均匀——越靠近 0,分布越密;越远离 0,分布越稀。相邻两个可表示数之间的距离叫 ULP(Unit in the Last Place),它随数量级指数级增长。

举例:1.0 附近,相邻数差约 2.2e-16;而 1e16 附近,相邻数竟然相差 2——在这个量级,浮点数连整数 1 都区分不了,更别说小数。我的延伸:这意味着把大数和小数相加会"吃掉"小数(1e16 + 1.0 == 1e16),这是数值计算里最经典的陷阱之一。所谓"浮点精度",从来不是一个固定值,而是相对精度:大约 15–17 位有效数字,无论这个数是 0.0001 还是 100 亿。

六、特殊值:用"不可能的指数"塞进无穷与 NaN

IEEE 754 还有几个很聪明的设计:指数全 1 而尾数全 0 表示无穷大(±Inf);指数全 1 而尾数非 0 表示 NaN(Not a Number);指数全 0 表示非规格化数和零。

这些特殊值不是花架子,它们让浮点运算"出错也不崩溃":除以零得到 Inf 而不是抛异常,0/0 得到 NaN 而不是停机,程序可以继续跑下去再统一处理。这是 Kahan 给工程界的一份礼物——让错误变成数据。现代 AI 训练里监测 NaN 就直接来源于这套语义。

七、工程教训:别和浮点比相等

视频结尾落脚到实践,我的归纳如下,这也是每个工程师该刻进肌肉记忆的:

  1. 永远不要用 == 比较浮点数,用 abs(a-b) < epsilon,或者更好——相对误差。
  2. 钱不要用 float:用整数(按分存储)或 Decimal/BigDecimal,否则 0.1 元会慢慢变成 0.10000001 元,账目对不上。
  3. 警惕减法相消(catastrophic cancellation):两个接近的大数相减,有效数字瞬间蒸发。
  4. 累加误差:循环里反复加 0.1,误差会一路累积;长序列求和用 Kahan 求和算法可以把误差压回机器精度。
  5. AI 时代的取舍:bfloat16 / fp16 主动牺牲尾数换范围,正是因为训练更在乎"不溢出"而不是"多几位精度"——这是对 IEEE 754 权衡哲学的当代继承。

知识结构图

graph TD
    A["double = 64 个 bit<br/>(和 long 长得一样)"] --> B["符号位 1 bit<br/>决定正负"]
    A --> C["指数位 11 bit<br/>决定数量级"]
    A --> D["尾数位 52 bit<br/>决定精度"]

    C --> C1["偏置 bias=1023<br/>变无符号整数比大小"]
    D --> D1["隐含前导 1<br/>实际 53 位精度"]

    A --> E["解码公式<br/>值 = (-1)^s × 1.m × 2^(e-1023)"]
    E --> F{"指数取值区间"}

    F -->|全 0| G["零 / 非规格化数<br/>近零处优雅衰减"]
    F -->|正常| H["规格化数<br/>日常小数"]
    F -->|全 1| I["±Inf / NaN<br/>错误变数据"]

    H --> J["0.1 转二进制<br/>0.000110011… 无限循环"]
    J --> K["尾数只有 52 位<br/>被迫舍入成近似替身"]
    K --> L["0.1替身 + 0.2替身<br/>= 0.30000000000000004"]
    L --> M["工程对策<br/>epsilon 比较 / 定点存钱 / Kahan 求和"]

实战启示

看完这个视频最大的认知升级,是从"浮点会偶尔出错"升级到"我能预测它在哪里出错"。这预测力直接转化为三方面的工程价值。

第一,写出不会半夜被叫起来修的代码。 涉及钱的系统,从设计第一天就用整数(按"分"或"厘"存储)或 Decimal 类型,从根上切断 0.1 元误差。涉及物理量、坐标、传感器数据的,统一用相对误差比较,并设置合理的 epsilon。这能消灭一类最常见的"测试环境好好的,生产环境对不上账"的 bug。

第二,看懂性能与精度的权衡。 为什么 AI 训练用 bfloat16?因为它把 IEEE 754 的 8 位指数留足、尾数砍到 7 位——继承了"范围比精度重要"的内核。为什么游戏和图形用 fp16/fp32 分级?因为 ULP 在不同量级的分布不同,渲染颜色用 fp16 完全够。理解了这套语言,你就能为自己的场景选对精度,而不是无脑用 double 浪费带宽和算力。

第三,数值算法的判断力。 知道"大数吃小数""减法相消""累加误差"这三件事,你在 review 别人代码、或在数据管道里看到诡异结果时,就能迅速定位。比如一个跑了一夜的累计求和突然不对劲,第一时间想到 Kahan 求和而不是怀疑数据源。这种直觉,价值远大于记住几个 API。

更进一步,浮点数的故事其实是所有"用有限编码逼近无限世界"系统的缩影——量化、采样、有损压缩、神经网络的权重精度,背后都是同一道权衡:用多少位、覆盖多大范围、容忍多少误差。把 IEEE 754 想透,等于拿到了一把理解整个数字世界的通用钥匙。

延伸学习路径

  1. David Goldberg《What Every Computer Scientist Should Know About Floating-Point Arithmetic》——ACM 经典论文,IEEE 754 领域的"圣经级"必读,把本文每个点都展开到数学层面。
  2. Bruce Dawson 的博客系列(Random ASCII:Floating Point 系列)——前 Google 工程师,把浮点 bug、ULP、精度陷阱讲到工业级深度,实战性极强。
  3. float.exposed(在线可视化工具)——输入任意浮点数,实时看它的 64 位 sign/exponent/mantissa 分解,亲手验证 0.1 的尾数长什么样。
  4. Fabien Sanglard 或 Computerphile 的浮点视频——从图灵奖得主 William Kahan 的历史讲起,理解这个标准为何能统治 40 年。
  5. 《计算机程序的构造和解释》(SICP) 数值计算章节——从抽象角度讲数值误差如何影响算法正确性,理论补强。

一句话总结

浮点数的全部秘密是:它不是小数,而是一段被当作整数存储、再用二进制科学计数法解码的 bit 串——看懂这层"伪装",0.1+0.2≠0.3 就从玄学变成了可预测的算术。

参考来源

  1. IEEE 754 Explained: How Your CPU Really Stores Decimal Numbers — 本次解读的核心视频,逐位拆解浮点表示与舍入误差
  2. IEEE Standard 754 Floating Point Numbers — GeeksforGeeks — 三段式布局、偏置指数的权威技术说明
  3. Why 0.1 + 0.2 isn't always 0.3: Floating Point Explained — DEV Community — 0.1+0.2 现象的代码级复盘
  4. IEEE 754 Floating Point: From Bits to Numbers — DigiSim.io — 偏置指数、隐含前导位、特殊值的交互式可视化
  5. IEEE 754 - Computer Systems Engineering Notes — 单/双精度的位运算细节与课堂级推导

    本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

完成摘要(What was done)

下一步计划

注意事项

[!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学通信知识星球海报-扫码加入