FP32表达的数字范围及由来
一、FP32数据类型表达的数据范围
在计算机科学和 IEEE 754 标准中,FP32(单精度浮点数,Single-precision floating-point format)使用 32 个比特(bits)来表示。
根据其位结构(1 位符号位 + 8 位指数位 + 23 位尾数位),FP32 的数据表达范围如下:
1.1. 核心数值范围
FP32 能够表达的数值跨度极大,主要分为以下两个指标:
最大正值(Max Positive Value):
\[\approx 3.4028235 \times 10^{38}\]
(这是由指数位全为 1,尾数位除最后一位全为 1 时计算得出的边界)。
最小正值(非零最小绝对值):
- 规格化数(Normalized):\(\approx 1.17549435 \times 10^{-38}\)
- 非规格化数(Subnormal,精度较低):\(\approx 1.40129846 \times 10^{-45}\)
1.2. 精度限制(有效数字)
FP32 的有效数字位数大约是 6 到 9 位十进制数字。
通常在工程实践中,我们认为 FP32 具有 7 位可靠的十进制精度。这意味着如果一个数字超过 7 位(例如 1234567.89),末尾的数字可能会因为舍入误差而变得不准确。
二、这个表达范围是怎么计算来的?
要理解 FP32(单精度浮点数)的表达范围,我们需要拆解 IEEE 754 标准定义的 32 位二进制结构。
FP32 的 32 个比特(bits)被分为三部分:
- 符号位 (Sign, \(S\)):1 位(0 代表正,1 代表负)。
- 指数位 (Exponent, \(E\)):8 位(决定数值的大小级数,即范围)。
- 尾数位 (Fraction/Mantissa, \(M\)):23 位(决定数值的精确度)。
其数值计算公式为:
\[\text{Value} = (-1)^S \times (1 + M) \times 2^{(E - 127)}\]
2.1. 最大正值的计算(\(\approx 3.4 \times 10^{38}\))
要得到最大的数,我们需要让指数 \(E\) 和尾数 \(M\) 都尽可能大:
指数位 \(E\):8 位二进制全为 1 时是 255。但 IEEE 754 规定 255 留给特殊值(如 \(\infty\) 和 NaN)。因此,最大的可用指数是 254。
- 代入偏移量(Bias = 127):\(E - 127 = 254 - 127 = 127\)。
- 这意味着最大数大约是 \(2^{127}\)。
尾数位 \(M\):23 位全为 1。由于公式中是 \((1 + M)\),当 \(M\) 无限接近 1 时,该部分接近 2。
最终计算:
\[\text{Max} \approx 2 \times 2^{127} = 2^{128}\]
用十进制换算:\(2^{128} \approx 3.4028235 \times 10^{38}\)。
2.2. 最小正值的计算(\(\approx 1.17 \times 10^{-38}\))
这里指“规格化”情况下的最小非零正数,即让指数尽可能小:
指数位 \(E\):\(E\) 不能全为 0(全 0 留给了“非规格化数”或数字 0)。因此最小的 \(E\) 是 1。
- 代入偏移量:\(E - 127 = 1 - 127 = -126\)。
尾数位 \(M\):全为 0。此时 \((1 + M) = 1\)。
最终计算:
\[\text{Min} = 1 \times 2^{-126} \approx 1.17549435 \times 10^{-38}\]
2.3. 为什么是 7 位有效数字?
这是由 23 位尾数 决定的。
- 在二进制里,23 位尾数加上隐藏的整数位“1”,总共有 24 位精度。
- 计算十进制精度:\(\log_{10}(2^{24}) \approx 24 \times 0.3010 \approx 7.22\)。
- 这意味着 FP32 可以稳定地表示 7 位十进制有效数字。
三、指数位为什么要减去一个偏移量127?
这是一个非常经典且精妙的计算机底层设计问题。简而言之:减去 127 是为了用“无符号”的二进制数,同时表示出“正指数”和“负指数”。
如果不减去偏移量,8 位指数位(0-255)只能表示正数,那我们就没法表示像 \(10^{-38}\) 这样极小的微观数字了。
3.1. 为什么不直接用“正负号位”?
你可能会问:既然符号位(Sign)可以决定整个数字的正负,为什么不在指数位里也划出 1 位来当正负号?
- 原因:为了方便排序(比较大小)。
- 如果指数位带符号(补码表示),负指数的二进制开头的数字会比正指数大(比如补码中 -1 的二进制领先于 1)。
- IEEE 754 的天才设计:通过加上一个偏移量(Bias),将原本范围是 \([-126, +127]\) 的指数,整体平移到了 \([1, 254]\) 这个全正数区间。
- 结果:计算机在比较两个浮点数大小时,可以直接从高位到低位像比较整数一样进行位比较,而不需要先去处理指数的正负逻辑,这极大提升了硬件执行效率。
3.2. 偏移量为什么是 127?
指数位一共有 8 位,能表示的整数个数是 \(2^8 = 256\) 个(0 到 255)。
为了让正负指数的范围几乎对称,我们取中间值作为“零点”:
- 公式:\(\text{Bias} = 2^{(k-1)} - 1\)(其中 \(k\) 是指数位数)。
- 对于 FP32,\(k=8\),所以 \(\text{Bias} = 2^7 - 1 = 127\)。
映射关系如下:
- 二进制存储为
01111111(127) \(\rightarrow\) 实际指数为 \(127 - 127 = 0\) - 二进制存储为
11111110(254) \(\rightarrow\) 实际指数为 \(254 - 127 = 127\)(最大值) - 二进制存储为
00000001(1) \(\rightarrow\) 实际指数为 \(1 - 127 = -126\)(最小值)
3.3. 特殊值的保留
你可能注意到 0 和 255 被跳过了:
- 全 0 (00000000):留给数字
0或者“非规格化数”(极小的数)。 - 全 1 (11111111):留给
Inf(无穷大)或者NaN(不是一个数字)。
四、M 最大时为什么无限接近于1
要理解为什么 \(M\) 无限接近 \(1\),我们需要把这 23 位“尾数”看作是二进制的小数部分。
在 FP32 的公式 \(\text{Value} = 1.M \times 2^{E-127}\) 中,那个 \(1\) 是隐含的(Hidden Bit),而 \(M\) 实际上是小数点后面的数字。
4.1. 二进制小数的权重
在十进制中,小数点后第一位权重是 \(10^{-1} (0.1)\),第二位是 \(10^{-2} (0.01)\)。
在二进制中,权重是按 \(2\) 的负幂次递减的:
- 第 1 位:\(2^{-1} = 0.5\)
- 第 2 位:\(2^{-2} = 0.25\)
- 第 3 位:\(2^{-3} = 0.125\)
- ...
- 第 23 位:\(2^{-23} \approx 0.000000119\)
4.2. 当 23 位全为 1 时的求和
当尾数位 \(M\) 的 23 个比特位全部填满 \(1\) 时,它的数学表达式是一个等比数列求和:
\[M = \frac{1}{2} + \frac{1}{4} + \frac{1}{8} + \dots + \frac{1}{2^{23}}\]
根据等比数列求和公式 \(S_n = a_1 \frac{1-r^n}{1-r}\):
\[M = \frac{1}{2} \times \frac{1 - (1/2)^{23}}{1 - 1/2} = 1 - \frac{1}{2^{23}}\]
计算结果:
\[M = 1 - 0.0000001192... = \mathbf{0.9999998807...}\]
所以,加上隐含的整数位 \(1\),\((1 + M)\) 的最大值就是 \(1.9999998807...\),极其接近 \(2\) 但永远不会达到 \(2\)。
4.3. 为什么要这样设计?(隐含位 1 的妙处)
你可能会问:既然接近 2,为什么不直接让它到 2?
这其实是为了唯一性和节省空间:
- 科学计数法的规范化:就像十进制科学计数法要求开头必须是 \(1 \sim 9\) 之间(例如 \(1.23 \times 10^5\)),二进制要求开头必须是 \(1\)(例如 \(1.011 \times 2^3\))。
- 白捡 1 位精度:既然规格化浮点数的开头第一位永远是 1,那我们干脆不在内存里存这个 \(1\),只存小数点后面的部分。
- 效果:原本 23 位的空间,实际发挥了 24 位的作用。
五、7 位有效数字到底是怎么来的?
这是一个非常深刻的数学跨界问题:如何将“二进制的精度”翻译成“十进制的精度”?
简单来说,这行公式是在计算:一个由 24 位二进制(0 或 1)组成的数字,最多能表达多少种不同的状态?而这些状态如果换成十进制,相当于多少位有效数字?
我们可以通过以下三个步骤拆解这个逻辑:
5.1. 状态总量的对等性
在二进制(Base-2)里,24 位比特(bits)能表示的不同数值状态总数是:
\[2^{24} = 16,777,216\]
在十进制(Base-10)里,如果我们有 \(N\) 位有效数字,能表示的状态总数是:
\[10^N\]
我们要找的“十进制精度” \(N\),就是让这两个表达能力相等的那个值:
\[10^N = 2^{24}\]
5.2. 为什么使用对数(Log)?
为了求出 \(N\),我们需要对等式两边取以 10 为底的对数:
\[\log_{10}(10^N) = \log_{10}(2^{24})\]
根据对数的性质(幂可以提到前面),等式变为:
\[N = 24 \times \log_{10}(2)\]
因为 \(\log_{10}(2) \approx 0.30103\)(这代表 1 位二进制大约相当于 0.301 位十进制),所以:
\[N \approx 24 \times 0.30103 \approx 7.2247\]
这意味着 24 位二进制的精确度,等效于 7.22 位的十进制精确度。
5.3. “7.22 位”在现实中意味着什么?
这里的 \(7.22\) 位是一个理论值,在实际应用中:
- 7 位是“绝对稳定”的:由于 \(7.22 > 7\),任何 7 位的十进制数(比如
9,999,999)转换成二进制再转回来,都不会丢失信息。 - 第 8 位是“不可靠”的:因为达不到 8 位(\(7.22 < 8\)),如果你有一个 8
位的数字(比如
12,345,678),FP32 的 24 位二进制可能无法区分它与12,345,679之间的微小差别。
浮点数在数轴上并不是绝对连续的。 要解答你的疑问,我们需要区分两个概念:“表达的范围”*和*“表达的精度(步长)”。
5.4. 继续深入学习23个尾数位
但这23位不是小数部分吗?它们能表示的最大数就是全为1时,这时翻译成十进制就是0.999多;表示的最小数字就是全为0时,翻译成十进制时就是0。
这23位能连续地表达出0.000000000001(其中有22个0)到\[M = 1 - 0.0000001192... = \mathbf{0.9999998807...}\] 吗?
1. 这 23 位能“连续”表达吗?
答案是:不能。 它们是离散的。
因为 \(M\) 只有 23 位,它只能表示 \(2^{23} = 8,388,608\) 个不同的值。
- 最小的步长(精细度):二进制下是 \(2^{-23}\)。
- 对应的十进制步长:\(1 \div 8,388,608 \approx 0.0000001192...\)
这意味着,在 \(1.0\) 到 \(2.0\) 之间,FP32 只能表达类似下面的数值:
- \(1.0000000000...\) (全 0)
- \(1.0000001192...\) (最后一位是 1)
- \(1.0000002384...\) (最后一位是 2)
- ...
- 你提到的 \(0.000000000001\)(12位小数)是表达不出来的。 因为它比 FP32 能表示的最小步长还要小得多。
2. 关于“7 位有效数字”的真正含义
你之前疑惑的 \(\log_{10}(2^{24})\) 算出来的 \(7.22\),其实是在告诉你:在 1 到 10 之间,这 23 位尾数能把这个区间切成多细?
由于尾数能表达 800 多万种状态,它能保证:
任何一个 7 位以内的十进制小数(比如 \(1.234567\)),在 FP32 的这 800 多万个“格子”里,都能找到一个极其接近的二进制对应点。
但如果你想要表示 12 位的小数(比如 \(1.000000000001\)),FP32 的“格子”就太粗了,它会强制把你这个数“四舍五入”到最近的一个格子上(通常就是 \(1.0\))。
3. 浮点数的“贫富差距”:动态步长
这是浮点数最神奇的地方。由于公式里有 \(2^{E-127}\),随着数字变大,刻度会越来越粗。
- 在数字很小时(指数 \(E\) 很小):步长非常精细。比如在 \(10^{-38}\) 附近,它可以区分极其微小的差别。
- 在数字很大时(指数 \(E\)
很大):步长变得极其粗旷。
- 比如当数字达到 \(2^{24}\)(即 \(16,777,216\))时,步长变成了 1。
- 这意味着:在 FP32 里,\(16,777,216 + 1\) 依然等于 \(16,777,216\)。它已经无法表示“加 1”这个操作了,因为 \(1\) 已经超出了它的 23 位有效精度范围!