
浮点数:IEEE 754 的表示与精度
浮点数:IEEE 754 的表示与精度
浮点数是如何表示的?
IEEE 754 的二进制浮点数把位串分成三段:
- $s$ 是符号位,1 bit,0 为正、1 为负。
- $E$ 是指数域,$e$ 个 bit,按无符号整数存储。
- $M$ 是尾数域,$m$ 个 bit,只存小数点后面的部分。
$\text{bias}=2^{e-1}-1$。$e=8$ 时是 127,$e=11$ 时是 1023,半精度 $e=5$ 时是 15。
常用的几种格式:
| 格式 | 符号 | 指数 | 尾数 | bias | 最大正规数 | 最小正规数 | 最小次正规数 |
|---|---|---|---|---|---|---|---|
| FP16 | 1 | 5 | 10 | 15 | 65504 | $6.10\times10^{-5}$ | $5.96\times10^{-8}$ |
| BF16 | 1 | 8 | 7 | 127 | $3.39\times10^{38}$ | $1.18\times10^{-38}$ | $9.18\times10^{-41}$ |
| FP32 | 1 | 8 | 23 | 127 | $3.40\times10^{38}$ | $1.18\times10^{-38}$ | $1.40\times10^{-45}$ |
| FP64 | 1 | 11 | 52 | 1023 | $1.80\times10^{308}$ | $2.23\times10^{-308}$ | $4.94\times10^{-324}$ |
| FP8 E4M3 | 1 | 4 | 3 | 7 | 448 | $1.56\times10^{-2}$ | $1.95\times10^{-3}$ |
| FP8 E5M2 | 1 | 5 | 2 | 15 | 57344 | $6.10\times10^{-5}$ | $1.53\times10^{-5}$ |
BF16 和 FP32 的指数都是 8 位,所以动态范围完全一样,只是尾数从 23 位砍到 7 位。FP16 反过来,尾数还有 10 位,指数只剩 5 位,量级一大就溢出。
浮点数的尾数是怎么计算的?
尾数的计算分四步。先把十进制数转成二进制,再规格化成 $1.xxx\times2^{k}$,然后把小数点后的位舍入到 $m$ 位,最后把指数加上偏移写进指数域。
规格化那一步是隐藏位的来源。任何非零二进制数都能写成 $1.d_1d_2\dots\times2^{k}$,小数点前那一位恒为 1,不用存。$m$ 位尾数因此给出 $m+1$ 位有效精度。这也保证了表示的唯一性,同一个数只有一种写法。
拿 0.1 举例。小数部分乘 2 取整,循环做下去:
1 | 0.1 × 2 = 0.2 → 0 |
小数点后前四位是 0001,从第五位起 1001 循环:
$E=-4+127=123$。尾数取 23 位是 10011001100110011001100,第 24 位是 1 且后面还有非零位,向上进位:
存下来的值其实是 $0.10000000149011612$,误差 $1.49\times10^{-9}$,不到半个 ulp(0.1 附近的 ulp 是 $2^{-27}\approx7.45\times10^{-9}$)。0.1 在二进制里是无限循环小数,任何有限位宽都只能存近似值,FP64 也一样。
舍入默认用「就近取偶」:落在两个可表示数正中间时,取尾数末位为偶的那个。这条规则避免了「一律向上」带来的系统性偏置。
$1+2^{-24}$ 恰好是 1.0 和 $1+2^{-23}$ 的中点,末位为偶,舍回 1.0。$1+3\times2^{-24}$ 是 $1+2^{-23}$ 和 $1+2^{-22}$ 的中点,取偶数尾数的 $1+2^{-22}$。
1 | import numpy as np |
24 位有效二进制位能保证多少位十进制?$24\times\log_{10}2\approx7.22$,所以 FP32 大约 7 位十进制有效数字。想无损往返(十进制写出去再读回来还是同一个浮点数),需要 $\lceil 1+p\lg 2\rceil$ 位,FP32 是 9 位。这也是 printf("%.9g") 能还原 float 的原因。
浮点数如何表示 0 附近的数字
如果只有正规数,能表示的最小正数是 $2^{-126}\approx1.18\times10^{-38}$,再往下直接掉到 0。0 附近因此留出一段无法表示的区间。最小的正规数乘以 $0.01$,真值是 $1.18\times10^{-40}$,没有次正规数就只能存成 0,相对误差 100%。梯度乘学习率、损失里的 exp 输出,都容易落到这一段。
IEEE 754 用次正规数填这个洞。指数域全 0 时,隐藏位不再补 1,指数固定为 $1-\text{bias}$:
对 FP32 就是 $0.M\times2^{-126}$。此时尾数没有隐藏位,$M$ 每加 1,数值就加 $2^{-149}$。间距变成常数,不再随数值缩小。
| 数值 | 位模式 | 说明 |
|---|---|---|
| $1.0$ | 0x3F800000 |
正规数 |
| $1.18\times10^{-38}$ | 0x00800000 |
最小正规数 $2^{-126}$ |
| $1.18\times10^{-38}$ 略小 | 0x007FFFFF |
最大次正规数 |
| $1.40\times10^{-45}$ | 0x00000001 |
最小次正规数 $2^{-149}$ |
| $0$ | 0x00000000 |
零 |
关键是 $2^{-126}$ 两侧的间距都是 $2^{-149}$,跨过这个边界没有跳变。这叫渐进下溢:数值越靠近 0,绝对精度保持不变,相对精度逐渐变差,直到最后一位也保不住,才归零。
相对精度的退化可以算出来。次正规数区间里 ulp 恒为 $2^{-149}$,所以相对间距是 $2^{-149}/x$。在 $x=2^{-126}$ 处是 $2^{-23}$,和正规数衔接;到 $x=2^{-140}$ 已经变成 $2^{-9}\approx2\times10^{-3}$;到最小次正规数,相对间距是 1,只剩一个有效位。
零有正负两个。+0 和 -0 数值相等,但 $1/(+0)=+\infty$,$1/(-0)=-\infty$。符号位在除法、atan2、复数的分支切割里会被读到,不能简单当成同一个东西。
硬件层面还有一点要注意。不少 GPU 和 SIMD 实现默认开启 FTZ(flush to zero)和 DAZ(denormals are zero),把次正规数直接当 0 处理,为的是避免次正规运算拖慢流水线。同一个模型换个后端,0 附近的结果可能不一样。FP16 训练要做 loss scaling,一部分原因也是次正规数区间太窄,梯度一小就整片归零。
浮点数如何计算相对精度?
浮点数的绝对间距不是常数,相对间距才是。对 $x\in[2^{k},2^{k+1})$,尾数 $m$ 位,间距是
把它除以 $x$,得到相对间距的范围:
FP32 就是 $(2^{-24},2^{-23}]$。每跨过一个 2 的幂,间距翻倍,相对间距从下界跳回上界。所以同一个 float 在 1.0 附近能分辨 $1.19\times10^{-7}$,到 2.0 附近间距翻倍到 $2.38\times10^{-7}$,相对精度没变。
由此得到两个常用的量,名字容易混,最好分开记:
- 机器精度 $\varepsilon=2^{-m}$,也就是 1.0 和下一个浮点数的距离,FP32 是 $1.19\times10^{-7}$。NumPy 的
finfo.eps用的是这个定义。 - 单位舍入误差 $u=\varepsilon/2=2^{-(m+1)}$,FP32 是 $5.96\times10^{-8}$。就近舍入下,$|\operatorname{fl}(x)-x|\le u|x|$,误差分析里用的是这个。
有的教材把 $u$ 叫 machine epsilon,看到「机器精度」这个词先确认对方用的是哪个定义,两者差一倍。
| 格式 | 尾数位 | $\varepsilon$ | $u$ | 十进制有效位 | 往返所需位数 |
|---|---|---|---|---|---|
| FP16 | 10 | $9.77\times10^{-4}$ | $4.88\times10^{-4}$ | 3.31 | 5 |
| BF16 | 7 | $7.81\times10^{-3}$ | $3.91\times10^{-3}$ | 2.41 | 4 |
| FP32 | 23 | $1.19\times10^{-7}$ | $5.96\times10^{-8}$ | 7.22 | 9 |
| FP64 | 52 | $2.22\times10^{-16}$ | $1.11\times10^{-16}$ | 15.95 | 17 |
为什么可以用 $2^{E-\text{bias}}$ 做近似?
尾数在 $[1,2)$ 之间,所以
把 $2^{E-\text{bias}}$ 当作这个数的代表值,相对误差最大接近 50%(数值越靠近区间上端越大),但量级一定对。判断溢出、估算动态范围、给 FP8 选缩放因子,用这一步就够了。FP8 的 scale 通常就取 $\text{scale}=2^{\lfloor\log_2\max|x|\rfloor}$ 这种 2 的幂,反量化时乘一个 2 的幂也不引入额外舍入误差。
细一层是带上尾数。$1.M$ 落在 $[1,2)$ 内,相对误差被压到 $2^{-(m+1)}$ 以内,也就是前面算的单位舍入误差 $u$。到这一步才是浮点数真正的相对精度,前面那个 50% 只是量级估计。
再细一层是取对数。对两边取 $\log_2$,得到 $\log_2 x\approx(E-\text{bias})+\log_2(1.M)$,而 $\log_2(1+f)\approx f$,$f$ 是尾数域按 $2^{-m}$ 归一化后的小数。把位模式当成整数 $I=E\cdot2^{m}+M$ 来读,就有
这就是那个近似最实用的一面:指数域和尾数域在整数视角下拼成一条斜率固定的直线,浮点数的位模式近似是它自己的对数。快速求 $\log$、$\exp$、平方根倒数的那类做法(Schraudolph 近似、Quake 里的 0x5f3759df)都从这里来,误差只来自 $\log_2(1+f)$ 与 $f$ 的偏差,最大约 0.086 个二进制位,用一两步牛顿迭代就能压回正常精度。
几个容易踩的坑
0.1 + 0.2 != 0.3在 FP64 下成立,结果是 $0.30000000000000004$。有意思的是 FP32 下这两个式子恰好相等,因为舍入刚好抵消,但这只是巧合,不能当规律用。- 大数加小数会被整个吃掉。FP32 里 $10^{8}+1$ 还是 $10^{8}$,因为这一段的间距是 8;$2^{24}+1$ 也还是 $2^{24}$,因为从 $2^{24}$ 起间距变成 2。做累加时先把小数加起来,再和大数相加。
- 溢出不会报错,直接给出 $\pm\infty$;继续算就得到 NaN,再往后整条计算链都被污染。混合精度训练里 loss 变 NaN,先查 scale 是不是太大。
float32能精确表示的最大连续整数是 $2^{24}=16777216$,再往上就有整数表示不了。用浮点存索引、计步数,超过这个值要留意。
总结
浮点数用固定位宽换来了浮动精度:指数决定量级,尾数决定量级内的分辨率,偏移让指数域可以按整数比较。尾数靠规格化省下隐藏位,靠就近取偶控制舍入偏置。0 附近用次正规数把间距接上,代价是相对精度随数值缩小而退化。衡量精度时看相对间距而不是绝对间距,单次误差是 $u$,累积误差是 $\gamma_n$,相消会把误差放大 $\kappa$ 倍。而 $2^{E-\text{bias}}$ 这个近似,本质上是指数域和尾数域在整数视角下拼成了一条近似的对数直线。
参考资料
- IEEE 754-2019, IEEE Standard for Floating-Point Arithmetic.
- David Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, ACM Computing Surveys, 1991.
- Jean-Michel Muller et al., Handbook of Floating-Point Arithmetic, 2nd ed., Birkhäuser, 2018.
- Paulius Micikevicius et al., FP8 Formats for Deep Learning, 2022.
- OCP, OCP 8-bit Floating Point Specification (OFP8), 2023.




