Appearance
浮点数与 IEEE 754
概念
定点数(fixed-point)的小数点位置是固定的,于是"能表示很大的数"和"能表示很精细的数"二选一。浮点数(floating-point number)把小数点变成浮动的——本质就是二进制版的科学计数法:
其中
一句话记牢分工:阶码决定范围,尾数决定精度。 后面所有计算题的错误,八成错在这两个词上。
原理
一、浮点数的一般格式(408 教材口径)
教材上的通用格式是四段:
┌───┬──────┬───┬────────────┐
│阶符│ 阶码 │数符│ 尾数 │
│Ef │ E │Sf │ M │
└───┴──────┴───┴────────────┘
1 k 位 1 n 位- 阶码(含阶符)是定点整数,习惯用移码表示——因为移码比较大小等价于无符号比较。
- 尾数(含数符)是定点小数,习惯用原码表示——因为符号与数值分开便于乘除。
- 阶码位数
越大,表示范围越大;尾数位数 越大,精度越高。
规格化(normalization):规定尾数最高位必须是有效数字,否则同样的数就有多种写法,白白浪费精度。原码表示下要求形如
即小数点后第一位必须是 1。
二、IEEE 754 的三段格式
IEEE 754 是工业标准,它删掉了"阶符"和"数符"的冗余,把符号位独立出来:
| 格式 | 总位数 | 符号 | 阶码 | 尾数 | 偏移量 | 阶码真值 |
|---|---|---|---|---|---|---|
| 单精度 float | 32 | 1 位(bit 31) | 8 位(bit 30~23) | 23 位(bit 22~0) | 127 | |
| 双精度 double | 64 | 1 位(bit 63) | 11 位(bit 62~52) | 52 位(bit 51~0) | 1023 |
三条关键约定:
- 阶码用移码,但偏移量是
(不是 )。单精度是 127,双精度是 1023。这一点与第 02 篇的"移码偏移 "不同,是经典陷阱。 - 尾数隐含最高位 1。规格化数的尾数真值是
,那个整数位的 1 不存、白送。于是单精度实际有效位是 位( ),不是 23 位。 全 0 与全 1 被征用表示特殊值(下节)。
规格化数的真值公式:
其中
三、特殊值的"征用"规则
阶码全 0 和全 1 是保留编码,因此规格化数的阶码范围只有
| 阶码 | 尾数 | 含义 | 真值 |
|---|---|---|---|
| 全 0 | 全 0 | ||
| 全 0 | 非规格化数 | ||
| 任意 | 规格化数 | ||
| 全 1(255) | 全 0 | 上溢的标志 | |
| 全 1(255) | NaN | 非法运算结果,如 |
非规格化数(denormalized number)是 IEEE 754 的精妙设计:阶码固定为
四、取值范围与精度(单精度,必背)
| 项 | 计算式 | 数值 |
|---|---|---|
| 最大正数 | ||
| 最小正规格化数 | ||
| 最小正非规格化数 | ||
| 十进制有效位数 | 7 位 |
最大正数为什么是
双精度对照:最大
五、浮点加减法:五步法
这是 408 计算大题的标准流程,每一步都要能默写。
第一步:对阶(align exponents)。求阶差
为什么必须小阶向大阶?因为尾数右移会丢低位(误差小),左移会丢高位(误差大,甚至把有效数字丢光)。方向选错,整题全错。
第二步:尾数求和。对阶后两尾数阶码相同,直接按定点加减法相加。
第三步:规格化。求和结果可能破坏"最高有效位为 1"的规定,需要修:
| 情况 | 现象 | 处理 |
|---|---|---|
| 左规(左归) | 尾数出现 00.0xxx 或 11.1xxx(不满足 | 尾数每左移 1 位,阶码减 1,直到最高有效位为 1 |
| 右规(右归) | 尾数溢出,双符号位为 01 或 10 | 尾数右移 1 位,阶码加 1,只需一次 |
第四步:舍入。右移对阶与右规都会移出低位,需要按规则舍入。IEEE 754 提供四种:
| 方式 | 规则 | 备注 |
|---|---|---|
| 就近舍入到偶数 | 就近取,正中间时取偶数尾 | 默认方式;也叫"四舍六入五成双" |
| 朝零舍入 | 直接截断 | 结果绝对值偏小 |
| 朝 | 一律向上 | 结果偏大 |
| 朝 | 一律向下 | 结果偏小 |
第五步:溢出判断。只有阶码溢出才算真溢出:
- 上溢(阶码超过最大规格化值)→ 结果置
- 下溢(阶码小于最小规格化值,且已无法用非规格化数表示)→ 结果置机器零
- 尾数溢出(右规能修回来)不算溢出
六、浮点数不能精确表示的小数
十进制小数
| 十进制 | 分数 | 分母 | 二进制 | 能否精确 |
|---|---|---|---|---|
| 0.5 | 1/2 | 2 | 0.1 | ✓ |
| 0.375 | 3/8 | 8 | 0.011 | ✓ |
| 0.25 | 1/4 | 4 | 0.01 | ✓ |
| 0.1 | 1/10 | 10 | 0.000110011…(无限循环) | ✗ |
| 0.2 | 1/5 | 5 | 0.001100110…(无限循环) | ✗ |
这就是"
示例
例 1:把 转成单精度 IEEE 754
完整计算过程:
第一步,把绝对值转成二进制。整数部分
从高位读到低位得
第二步,规格化(小数点左移到第一个 1 之后):
第三步,定符号位:
第四步,算阶码。真阶是 3,加偏移量:
第五步,写尾数。取小数点后的 100011,右侧补 0 到 23 位:
第六步,拼装:
| 段 | 内容 |
|---|---|
| 符号 | 1 |
| 阶码 | 1000 0010 |
| 尾数 | 100 0110 0000 0000 0000 0000 |
| 合计 | 1100 0001 0100 0110 0000 0000 0000 0000 |
答案:0xC1460000。
反向验证:
例 2:从机器码读回真值
已知某单精度浮点数的机器码为
0x41C80000,求其真值。
完整计算过程:
第一步,展开为二进制并分段:
第二步,算阶码真值:
第三步,还原尾数:接上隐含的 1 得
第四步,代入真值公式:
答案:真值为
例 3:单精度取值范围为什么要记
完整计算过程:
第一步,阶码最大只能取
第二步,尾数最大为全 1,即
第三步,相乘得最大正数:
第四步,最小正规格化数取
第五步,最小正非规格化数取
结论:单精度能表示的正数量级跨度约从
例 4:按五步法算
完整计算过程:
第一步,写出两个操作数的规格化形式:
第二步,对阶。 阶差
4.5 尾数: 1.00100000000000000000000 (阶 2)
0.25 尾数: 0.00010000000000000000000 (阶 2, 原 1.0 右移 4 位)对应 23 位小数域:
4.5 → 00100000000000000000000
0.25 → 00010000000000000000000第三步,尾数求和:
1.00100000000000000000000 ← 4.5
+ 0.00010000000000000000000 ← 0.25 右移 4 位
──────────────────────────
1.00110000000000000000000第四步,规格化。 结果是
第五步,舍入。 对阶时
第六步,溢出判断。 阶码仍为 2,在
答案:
机器码验证:0x40980000 展开为
例 5:大数吃小数(精度丢失的经典)
用单精度计算
。
完整计算过程:
第一步,
第二步,
第三步,对阶:把
第四步,求和:
答案:
同类现象:
例 6:C 代码——用位域观察浮点数
本机不提供代码运行能力,代码块仅保留 runnable 标记;请自行在本地编译验证。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* 用 union 把 float 的 32 位原样取出来,避免依赖类型双关的未定义行为 */
static uint32_t f2u(float f) {
uint32_t u;
memcpy(&u, &f, sizeof u);
return u;
}
static void decode(float f) {
uint32_t u = f2u(f);
uint32_t s = (u >> 31) & 1u;
uint32_t e = (u >> 23) & 0xFFu;
uint32_t m = u & 0x7FFFFFu;
printf("%-10g -> 0x%08X s=%u E=%u(%d) M=", (double)f, u, s, e, (int)e - 127);
for (int i = 22; i >= 0; i--) putchar((m >> i) & 1u ? '1' : '0');
printf(" 真值=%.10g\n", (double)f);
}
int main(void) {
decode(-12.375f); /* 0xC1460000 E=130(3) M=1000110... */
decode(25.0f); /* 0x41C80000 E=131(4) M=1001000... */
decode(4.5f + 0.25f); /* 0x40980000 */
decode(0.1f); /* 0x3DCCCCCD —— 无法精确表示 */
printf("\n0.1f + 0.2f == 0.3f ? %s\n", (0.1f + 0.2f == 0.3f) ? "true" : "false");
/* 单/双精度整数精度边界 */
printf("2^24f = %.0f\n", (double)(float)(1u << 24));
printf("2^24+1f = %.0f (被吞掉)\n", (double)(float)((1u << 24) + 1));
printf("2^24+2f = %.0f\n", (double)(float)((1u << 24) + 2));
printf("2^53d = %.0f\n", (double)(1ULL << 53));
printf("2^53+1d = %.0f (被吞掉)\n", (double)((1ULL << 53) + 1));
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出(对照本节的推导):
0.1f + 0.2f == 0.3f ? false
2^24f = 16777216
2^24+1f = 16777216 (被吞掉)
2^24+2f = 16777218
2^53d = 9007199254740992
2^53+1d = 9007199254740992 (被吞掉)例 7:Python 对照与验算
import struct, math
def f2bits(f):
u = struct.unpack('>I', struct.pack('>f', f))[0]
s, e, m = u >> 31, (u >> 23) & 0xFF, u & 0x7FFFFF
return u, s, e, format(m, '023b')
def group(b):
return ' '.join(b[i:i+4] for i in range(0, len(b), 4))
# 例 1 / 例 2:机器码与真值互相换算
u, s, e, mb = f2bits(-12.375)
print('%-8s -> 0x%08X E=%d(%d) M=%s' % (-12.375, u, e, e - 127, mb))
print('0x41C80000 -> %.6f' % struct.unpack('>f', struct.pack('>I', 0x41C80000))[0])
print('0x40980000 -> %.6f' % struct.unpack('>f', struct.pack('>I', 0x40980000))[0])
# 例 3:取值范围
print('最大正数 = %.6e' % ((2 - 2**-23) * 2**127))
print('最小正规格化 = %.6e' % (2.0**-126))
print('最小正非规格化= %.6e' % (2.0**-149))
print('有效十进制位数(log10 2^24) = %.4f -> 7 位' % (24 * math.log10(2)))
# 例 5:大数吃小数
for x in (2**24 - 1, 2**24, 2**24 + 1, 2**24 + 2):
y = struct.unpack('>f', struct.pack('>f', float(x)))[0]
print('单精度 %-10d -> %-10.0f %s' % (x, y, '精确' if y == x else '被吞/被舍'))
# 精度边界:分母是否为 2 的幂
for d in (0.5, 0.375, 0.25, 0.1, 0.2, 0.3):
exact = struct.unpack('>f', struct.pack('>f', d))[0] == d
print('单精度 %.3f 可精确表示? %s' % (d, exact))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:-12.375 -> 0xC1460000、0x41C80000 -> 25.000000、最大正数 = 3.402823e+38、单精度 16777217 -> 16777216 被吞/被舍,与例 1、例 2、例 3、例 5 的推导一致。
考点
考点
1. 必背表(单精度)
| 项 | 值 |
|---|---|
| 位域 | 1 + 8 + 23 |
| 偏移量 | 127 |
| 阶码取值(规格化) | |
| 最大正数 | |
| 最小正规格化 | |
| 最小正非规格化 | |
| 有效十进制位 | 7 位 |
2. 高频陷阱
- IEEE 754 的阶码偏移是
(127),不是 (128)。这与第 02 篇移码的一般定义不同,别混用。写 而不是 。 - 尾数隐含 1,实际有效位 24 位。题目问"单精度有效位数"答 24(二进制)或 7(十进制),答 23 就丢了那个隐含的 1。
- 阶码全 0 / 全 1 是保留编码,所以阶码最大值是 254 而不是 255。忘了这点,例 3 的最大值会算成
。 - 对阶一律小阶向大阶。方向写反是致命错误,会连带尾数、阶码全错。
- 只有阶码溢出才算溢出。尾数相加出现进位(右规可修)不算溢出,这是最容易误判的一处。
- 上溢 vs 下溢的处理不同:上溢置
(IEEE 754 有专门编码),下溢置机器零。 与 是两个不同的编码(0x00000000与0x80000000),虽然数值上相等。这也是 IEEE 754 与原码体系都"有两个零"的体现。- 非规格化数的阶码固定为
,且不隐含 1。它存在的意义是渐进下溢,别把它和"阶码下溢"搞混。 - 判断小数能否精确表示:约分后分母必须是 2 的幂。
、 、 都不行, 、 、 都行。 - 比较浮点数别用
==。这是工程常识,也是 的直接后果。
3. 解题模板("给十进制小数写 IEEE 754")
① 整数与小数分开转二进制(小数用"乘 2 取整")
② 规格化成 1.xxx × 2^e
③ 符号位 s
④ 阶码 E = e + 127 ← 别用 128
⑤ 尾数取小数点后 23 位,右侧补 0
⑥ 拼:s | E(8) | M(23),换算十六进制逆向("给机器码求真值"):分段 → 算阶
4. 与后续章节的接口
- 阶码的移码编码回到第 02 篇;浮点加减法里的尾数求和用的是第 04 篇的定点加法器。
- 浮点数在机器内的存储格式,是第 11 篇主存储器里"数据表示与存储字长"的实例。
小结
- 浮点数 = 二进制科学计数法:
,阶码管范围,尾数管精度。 - IEEE 754 单精度是
,偏移量 127,尾数隐含 1,于是有效位 24 位。 - 阶码全 0 表示非规格化数与零,全 1 表示无穷与 NaN;规格化阶码范围只有
。 - 浮点加减五步:对阶(小阶向大阶)→ 尾数求和 → 规格化(左规/右规)→ 舍入 → 溢出判断。只有阶码溢出才算真溢出。
- 分母不是 2 的幂的十进制小数无法精确表示,这就是
的原因;单、双精度能连续精确表示整数的边界分别是 、 。
下一篇:运算器与加法器
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。