跳到主要内容

CPU 是怎么存 0.1 的:IEEE 754 浮点格式完全拆解

深入理解 IEEE 754 浮点数表示,从符号位、指数偏置到尾数的实际编码,用真实例子演示 0.1 为什么不等于 0.1+0.2-0.2。

发布于
#ieee754 #浮点数 #二进制 #计算机原理

CPU 是怎么存 0.1 的:IEEE 754 浮点格式完全拆解

写了三年 Python,我一直知道 0.1 + 0.2 != 0.3 这个"梗",但从没真正看过那串位是什么样的。直到排查一个金融计算差了 0.000000000000000111,我才逼自己坐下来,把 IEEE 754 的每一位认真数了一遍。

现在我用 IEEE 754 浮点数转换器 随时拆开任何一个小数,三十秒就能看清楚符号、指数、尾数三段各自编了什么。这篇文章把我当时弄明白的过程写出来,希望能省掉你几小时反复查规范。

IEEE 754 标准存了哪三段

IEEE 754 把一个浮点数劈成三个字段,单精度(32 位)的布局是:

  • 符号位(1 位):0 表示正数,1 表示负数
  • 指数段(8 位):存的不是真实指数,而是真实指数加上偏置值 127
  • 尾数段(23 位):存小数点后面的二进制位,整数部分的 1 被省略不存

双精度(64 位)把指数段扩到 11 位,偏置值变成 1023,尾数段扩到 52 位。

这套设计让同一套电路既能处理 10⁻³⁸ 量级的极小数,也能处理 10³⁸ 量级的极大数,覆盖 2⁵³ 个精度级别(per IEEE 754-2008 标准文档第 3.4 节)。

实际拆解:0.15625 的单精度表示

我选 0.15625 作为第一个例子,因为它刚好是 2 的负幂之和,转换结果是整数,没有精度损失,适合入门。

输入0.15625,精度选单精度(32 位)

工具输出

符号位:0(正数)
指数段:01111100(十进制 124,真实指数 = 124 − 127 = −3)
尾数段:01000000000000000000000
完整十六进制:0x3E200000
完整二进制:0 01111100 01000000000000000000000

验证一下:0.15625 = 1.25 × 2⁻³,换成二进制就是 1.01 × 2⁻³。整数部分的 1 省略,尾数只存 .01,补满 23 位得到 01000000000000000000000。指数 −3 加偏置 127 等于 124,二进制写成 01111100。拼起来就是上面那串,对应十六进制 0x3E200000

把同样的十六进制粘回工具的输入框,它立刻还原成 0.15625,整个过程是可逆的。

0.1 为什么永远"不准"

0.1 在二进制里是一个无限循环小数。就像十进制里的 1/3 写成 0.333…,1/10 用二进制写是:

0.0001100110011001100110011001100110011…(循环节是 0011)

单精度只有 23 位尾数,循环在这里被截断。我用工具输入 0.1,看到的单精度结果是:

十六进制:0x3DCCCCCD
尾数段:10011001100110011001101(最后三位四舍五入进了位)
实际存储的值:0.100000001490116119384765625

误差大约是 1.49 × 10⁻⁸。双精度把尾数延伸到 52 位,误差缩小到约 5.55 × 10⁻¹⁷,但仍然不是精确的 0.1。

这解释了为什么 0.1 + 0.2 == 0.3 在几乎所有语言里都是 false:两个截断后的近似值相加,和精确的 0.3 的截断值差了最低有效位的一个单位(ULP)。

特殊值:零、无穷和 NaN

IEEE 754 用指数段全 0 或全 1 来表示特殊情况:

| 值 | 符号 | 指数段 | 尾数段 | |---|---|---|---| | +0 | 0 | 全 0 | 全 0 | | −0 | 1 | 全 0 | 全 0 | | +∞ | 0 | 全 1 | 全 0 | | −∞ | 1 | 全 1 | 全 0 | | NaN | 任意 | 全 1 | 非 0 | | 非规格化数 | 任意 | 全 0 | 非 0 |

我在工具里输入 Infinity,它返回 0x7F800000,指数段八位全 1,尾数全 0,和表格一致。输入 NaN 得到 0x7FC00000,尾数最高位是 1(安静型 NaN 的约定)。

非规格化数(denormal)最有意思。当指数段全 0 而尾数不为 0,整数部分的隐含 1 变成隐含 0,用来表示非常接近零的极小值。这是 IEEE 754 相比早期格式的一大进步,避免了"突然下溢"到零的问题。

排查实际代码的浮点对不上

工具最实用的场景是并排比较两个数的位表示。

假设你有一段 C 代码,从网络字节流里读了一个 4 字节的十六进制字 0xBF000000,不确定对应什么数值。把它粘进工具:

输入(十六进制模式):BF000000
输出:
  符号位:1(负数)
  指数:01111110(十进制 126,真实指数 = −1)
  尾数:10000000000000000000000
  十进制值:−0.5

一看就清楚,不用手算。

如果你在 Python 里遇到 0.1 + 0.2 - 0.3 不等于零,把三个值分别输入工具切到双精度,对比尾数段。你会发现 0.1 + 0.2 的位和 0.3 差了最低一位,这就是相等比较失败的根源,也是为什么应该用 abs(a - b) < epsilon 而不是直接 ==

如果需要进一步分析不同进制之间的关系,进制转换器 可以在二进制、八进制、十进制、十六进制之间任意互转,配合 IEEE 754 工具一起用,内存转储里的裸字节一目了然。

值得记住的几个数字

学完 IEEE 754 之后,我有几个数字变得非常具体:

  • 单精度最大正数0x7F7FFFFF,约 3.4 × 10³⁸
  • 单精度最小正规格化数0x00800000,约 1.18 × 10⁻³⁸
  • 机器精度(machine epsilon):单精度约 1.19 × 10⁻⁷,双精度约 2.22 × 10⁻¹⁶
  • 0 的两个表示+00x00000000)和 −00x80000000)数值比较相等,但 1/+01/−0 分别是 +∞−∞

写底层序列化或解析网络协议时这些数字经常直接用到。用 IEEE 754 浮点数转换器 现查现验,比背规范快得多。


Made by Toolora · Updated 2026-06-16