浮点数(IEEE 754)
浮点格式只能保存一组离散值。若输入不在其中,就按舍入规则选取可表示值。字中有符号、指数、尾数字段;对正规数,按 ±1.f×2ᵉ 读回。IEEE 754 规定格式与舍入方式,本课采用默认的就近舍入、平局取偶。
可表示值像间距变化的刻度。正规数的指数越大,相邻刻度越远,指数增加 1,间距翻倍。非正规数区域则使用固定间距。本课 binary16 分配 1+5+10 位;binary32 为 1+8+23,binary64 为 1+11+52。更多尾数位提供更细的相对精度,更多指数位扩展范围,但有限位宽仍只保存有限个刻度。
浮点数如何工作
对非零有限值,先按符号、介于 1 与 2 之间的有效数、2 的幂来分解。例如 0.1=1.6×2⁻⁴,4096=1.0×2¹²。是否能作为正规数保存,还取决于格式范围。
符号位保存正负。这里可以单独存符号,因为浮点加法本来就需要对阶等处理,不能直接套用普通整数加法器。
指数字段保存 e+bias。binary16 的 bias=15,所以 −4 存为 11。偏置让指数字段保持无符号并随实际指数递增。对非负、非 NaN 的编码,按无符号整数比较原始位模式可保持数值顺序;不能把这个性质扩展到任意带符号模式。
尾数字段只存前导 1. 后面的位。正规数必有这个 1,无须占一位,所以十个存储位得到十一位有效精度。零不符合 1.f×2ᵉ,全零指数因此另作零与非正规数编码。
binary16 的十位 fraction 给 [1,2) 内 1024 个刻度。乘 2ᵉ 后,正规数当前指数区间的一个 ulp(最低有效位单位)为 2^(e−10)。0.1 附近是 2⁻¹⁴≈0.000061,4096 起的区间是 4。绝对误差尺度随数值大小变化。
就近舍入选更近的值;恰在中点时,选有效数最低位为偶数的一侧。有限范围内的相邻刻度舍入误差不超过半个间距。取偶避免每次中点都向同一方向偏移,但不保证任意数据的误差必然相消。
逐步演示
默认把 0.1 编码为 binary16:
- 建立十六个位置,分为三个字段。没有独立小数点位,小数点位置由指数解释。
- 正数的符号位 15 写
0。 - 分解为
1.6×2⁻⁴,指数 e=−4。 - 1.6 的二进制是
1.1001100110011…,1001无限重复。1/10 的约分分母含因子 5,因此不是有限二进制小数,增加有限位数也不能精确表示。 - 指数加偏置:−4+15=11,写
01011。 - 保存前十个 fraction 位
1001100110,后续011001…位于保留边界之外。 - 舍弃尾部约为一个间距的 0.4,小于一半,不向上舍入。
- 读回
(1+614/1024)×2⁻⁴=0.0999755859375。 - 在
[0.0625,0.125)内间距为2⁻¹⁴;到下个指数区间翻倍为约 0.00012。同宽数轴区间只能容纳一半刻度。 - 相对输入的有符号误差约为
−2.4×10⁻⁵,即 −0.4 个间距。该刻度附近半步内的一段输入,约 0.09995–0.10001,会舍入到相同十六位;精确边界还要按平局规则处理。
有效数工作区位于编码字上方,竖线左边保留、右边舍弃。下行是实际编码,刻度条显示 0.1 处于两个可表示值之间。三个字段确定后编码已经结束,读回是解释字段,不是额外编码。理想编码推导中,只有最终舍入丢失信息,其余是表示转换。
为什么 0.1+0.2 不一定等于 0.3
add 变体默认在 binary16 中计算。两个操作数先各自舍入:0.1 为 0.0999755859375,0.2 为 0.199951171875。指数相差一,较小有效数右移一列对齐,本例这次对阶未丢失信息。
有效数相加得到 10.0110011001×2⁻³,超出正规有效数的 [1,2),右移一位并增大指数,成为 1.0011001100|1×2⁻²。边界外那一位恰为半步,平局取偶保留 …1100,不进到 …1101。结果是 0.2998046875。
直接把 0.3 存成 binary16 却得到 0.300048828125,比和高一个刻度,因此比较为假。这是输入舍入与结果舍入共同决定的结果。
Same sum, binary32 将输入和每次运算都按 binary32 舍入,23 位 fraction 使比较变为真。常用 binary64 中比较又为假,Python 浮点与 JavaScript Number 的例子会得到 0.30000000000000004。不能只看十进制表达式推断相等,也不能把结果当随机:指定格式和运算规则后,结果确定。
其他预设展示同样的规则:
- Absorbed: 4096 + 1:binary16 从 4096 向上的间距为 4,1 仅四分之一步,舍入后仍为 4096,
a+b==a。 - Overflow: 65504 + 100:有限数之和超过 binary16 最大有限值并舍入到无穷。IEEE 754 设置 overflow、inexact 状态;非陷阱环境可继续运行,语言或库也可能报告、触发异常。
- Subnormal: 0.00004:指数域为 0,不再使用隐含前导 1,进入非正规数范围。间距固定,数值越小,相对精度越低。
边界情况
- 零:指数与 fraction 全零,符号位仍保留,所以有
-0.0。它与0.0比较相等,位模式不同。IEEE 非陷阱除法的1/0.0、1/-0.0是相反符号无穷;Python 浮点除零等语言操作可抛异常。 - 非正规数:binary16 最小正规数为
2⁻¹⁴,再向下固定有效指数,前导位变 0,最小正非零值为2⁻²⁴。以损失相对有效位换取渐进下溢。 - 无穷与 NaN:全一指数、零 fraction 表示无穷;非零 fraction 表示 NaN。有限数加无穷仍为该无穷。
inf-inf、inf*0、inf/inf得 NaN,NaN 不等于包括自身在内的任何值;inf+inf仍是 inf。 - 舍入可改变指数:2047.9 转成 binary16 时,进位越过 fraction 字段,指数增加,得到 2048。
- 部分小数精确:
0.375=0.011₂,没有误差。约分分母为 2 的幂时具有有限二进制展开,还需有效位和指数装得下;例如 0.5、0.25、2.75 在这些格式中都精确。
常见错误
- 无条件用
==比较计算结果。代数等价不保证舍入路径相同。需要近似比较时,可按问题选取abs(a - b) <= max(rel * max(abs(a), abs(b)), abs_tol),兼顾相对与近零绝对误差。相等比较并非禁用;binary64 中绝对值不超过2⁵³的整数、未改变的同一存储值等可做精确比较。NaN 等特殊值另有规则。 - 认为更多位能精确存下 0.1。binary64 仍是有限二进制,只能减少或改变误差。
- 假定加法结合律成立。
(a+b)+c与a+(b+c)在不同位置舍入。累加大小悬殊的项时,小项可能被吞掉;调整求和顺序或补偿求和可减少误差,效果取决于数据。 - 用二进制浮点直接表达要求十进制精确的金额。分的分母是 100,通常需要整数最小单位或十进制类型,并按业务规则舍入。
- 把打印的小数当作精确存储值。常见最短往返格式会打印 0.1,但该字符串只是能还原同一浮点模式的表示。
- 忽略溢出检测。非陷阱加法可能得到无穷,应按需求预查范围或事后用 isfinite 检查结果;C 可通过
<fenv.h>访问状态,NumPy 有np.errstate,各语言和 API 对其他操作的异常行为不完全相同。
模型范围
画面使用 binary16 和 binary32,binary64 的 52 个 fraction 位不适合逐列展示。加法按较大指数对阶、相加、规格化,最后使用被舍弃尾部做一次舍入。真实硬件用 guard、round、sticky 位处理细节,融合乘加可在乘加两个运算后只舍入一次。
本课未展开减法与灾难性消减、NaN 传播细则、其他舍入模式、异常状态寄存器和 x87 的 80 位扩展寄存器。格式与平台规则是精确行为的依据,动画用于理解编码和舍入路径。