CSAPP(lecture 4_01)

浮点数的表示原理

上节我们已经学习了整数的二进制表示,但计算机不仅需要处理整数,还需要表示小数。

例如,如何使用二进制表示 5.755.755.75、0.10.10.1 这样的数值?

实际上,二进制小数与十进制小数遵循完全相同的位权规则 ,只是将基数从 101010 换成了 222。

对于十进制数,小数点左侧各位的权重依次为 100,101,102,...10^0,10^1,10^2,\ldots100,101,102,...,右侧则依次为 10−1,10−2,10−3,...10^{-1},10^{-2},10^{-3},\ldots10−1,10−2,10−3,...。二进制同理,小数点左侧的权重为 20,21,22,...2^0,2^1,2^2,\ldots20,21,22,...,右侧为 2−1,2−2,2−3,...2^{-1},2^{-2},2^{-3},\ldots2−1,2−2,2−3,...。

因此,二进制小数可以统一表示为:

B=∑k=−jibk2k,bk∈{0,1} B=\sum_{k=-j}^{i}b_k2^k,\qquad b_k\in\{0,1\} B=k=−j∑ibk2k,bk∈{0,1}

例如:

1011.1012=23+21+20+2−1+2−3 1011.101_2 =2^3+2^1+2^0+2^{-1}+2^{-3} 1011.1012=23+21+20+2−1+2−3

=8+2+1+12+18=11.625 =8+2+1+\frac12+\frac18 =11.625 =8+2+1+21+81=11.625

由此可见,二进制小数并没有采用新的计算规则,只是允许二进制位具有负指数权重。

这种表示方式还可以直接解释小数点移动与乘除运算之间的关系。例如:

101.112=5.75 101.11_2=5.75 101.112=5.75

将二进制小数点向左移动一位:

10.1112=2.875 10.111_2=2.875 10.1112=2.875

恰好满足:

5.75÷2=2.875 5.75\div2=2.875 5.75÷2=2.875

反过来,小数点向右移动一位,则相当于乘以 222。

因此,在二进制数的数学表示中,移动小数点的位置,相当于改变数值中各位的权重,也就是乘以或除以 222 的整数次幂。

另一个值得注意的规律是,二进制小数点右侧如果连续出现 111,其数值会不断接近 111:

0.12=12 0.1_2=\frac12 0.12=21

0.112=12+14=34 0.11_2=\frac12+\frac14=\frac34 0.112=21+41=43

0.1112=12+14+18=78 0.111_2=\frac12+\frac14+\frac18=\frac78 0.1112=21+41+81=87

如果小数点右侧有 nnn 个连续的 111,则:

∑i=1n2−i=1−2−n \sum_{i=1}^{n}2^{-i}=1-2^{-n} i=1∑n2−i=1−2−n

随着 nnn 增大,该数值不断接近 111,但只要 nnn 有限,就始终小于 111。因此,可以将其表示为 1−ε1-\varepsilon1−ε,其中:

ε=2−n \varepsilon=2^{-n} ε=2−n

这里的 ε\varepsilonε 只是表示当前位数所决定的差值,并不是一个固定不变的常数。

这个规律后面还会用于分析浮点数的有效数字及精度。

既然二进制能够表示小数,为什么还需要浮点数?

因为有限位数的二进制小数存在两个根本限制:并不是所有小数都能被精确表示,而且固定的小数点位置无法同时兼顾表示范围与精度。

首先考虑精确表示问题。

如果一个二进制小数只有有限的 kkk 位小数,那么它必然可以写成:

x2k \frac{x}{2^k} 2kx

其中 xxx 是整数。

因此,能够使用有限二进制小数精确表示的数,必须能够写成分母为 222 的整数次幂的分数。对于一个最简分数,这等价于其分母只能包含因子 222。

例如:

18=0.0012 \frac18=0.001_2 81=0.0012

34=0.112 \frac34=0.11_2 43=0.112

这些数都能够使用有限的二进制位精确表示。

但对于 13\frac1331、15\frac1551 或 110\frac1{10}101,情况就不同了:

13=0.01010101...2 \frac13=0.01010101\ldots_2 31=0.01010101...2

15=0.001100110011...2 \frac15=0.001100110011\ldots_2 51=0.001100110011...2

110=0.0001100110011...2 \frac1{10}=0.0001100110011\ldots_2 101=0.0001100110011...2

它们在二进制中具有无限循环的小数部分。由于计算机能够存储的位数有限,因此无法完整保存这些数的精确二进制表示,只能保留有限位并进行近似。

这也解释了为什么十进制中的 0.10.10.1,在计算机中未必能够被精确存储。

需要注意,这并不是浮点数特有的问题,而是有限位二进制表示本身的限制。即使换成浮点数,只要仍然采用有限位二进制编码,就无法让所有十进制小数都被精确表示。

第二个问题是表示范围与精度之间的矛盾。

假设我们只有 888 位存储空间,采用**定点数(Fixed Point)**表示,也就是提前规定二进制小数点的位置。

如果将 444 位分配给整数部分,另外 444 位分配给小数部分:

text 复制代码
IIII.FFFF

那么能够表示的最大无符号数为:

1111.11112=15+1516=15.9375 1111.1111_2=15+\frac{15}{16}=15.9375 1111.11112=15+1615=15.9375

而相邻两个可表示数之间的最小间隔为:

2−4=116=0.0625 2^{-4}=\frac1{16}=0.0625 2−4=161=0.0625

如果希望提高小数精度,可以将更多位分配给小数部分。例如:

text 复制代码
II.FFFFFF

这时最小间隔缩小为:

2−6=164=0.015625 2^{-6}=\frac1{64}=0.015625 2−6=641=0.015625

但由于整数部分只剩下 222 位,最大可表示数也下降为:

11.1111112=3.984375 11.111111_2=3.984375 11.1111112=3.984375

也就是说,在总位宽固定的情况下,小数部分分配的位数越多,能够区分的小数越精细,但能够表示的大数范围就越小;反之,表示范围扩大,小数精度就会下降。

对于科学计算,这种限制尤其明显。一个程序可能既需要表示非常大的数,也需要表示极其接近 000 的数。如果二进制小数点的位置始终固定,就很难同时满足这两种需求。

解决思路是:不再固定二进制小数点的位置,而是用一部分二进制位保存有效数字,另一部分保存指数,通过指数控制小数点的实际位置。

这就是浮点数(Floating Point)的核心思想。

例如:

101.112=1.01112×22 101.11_2=1.0111_2\times2^2 101.112=1.01112×22

同样,较小的数可以表示为:

0.0010112=1.0112×2−3 0.001011_2=1.011_2\times2^{-3} 0.0010112=1.0112×2−3

可以发现,原本具有不同小数点位置的二进制数,都能够转换为类似科学计数法的形式:

M×2E M\times2^E M×2E

其中,MMM 保存有效数字,EEE 决定数值的数量级。

这样一来,指数较大时,可以表示很大的数;指数较小时,可以表示接近 000 的数,而有效数字部分仍然保留一定数量的二进制位。

需要注意,浮点数并没有消除位数有限的问题,而是将有限的位数重新分配给有效数字和指数,以精确表示所有数值为代价,换取更大的动态范围(Dynamic Range)以及与数值大小相关的精度分布。

不过,浮点表示如果没有统一规则,不同计算机可能采用不同的编码方式、舍入规则以及溢出处理方式,导致同一个程序在不同机器上得到不同结果。

为了解决这一问题,IEEE 于 198519851985 年制定了 IEEE 754 浮点运算标准,对浮点数的编码及相关运算行为进行规范,使不同硬件能够遵循相对统一的数值计算规则。

IEEE 754 使用类似二进制科学计数法的形式表示浮点数:

V=(−1)s×M×2E \boxed{V=(-1)^s\times M\times2^E} V=(−1)s×M×2E

其中:

  • sss:符号位(Sign),决定数值的正负。
  • MMM:有效数(Significand),保存数值中的有效数字。
  • EEE:指数(Exponent),控制数值的数量级。

对于通常的规格化浮点数,有效数满足:

1≤M<2 1\le M<2 1≤M<2

之所以希望 MMM 位于这个范围,是因为任意非零的有限二进制数,都可以通过移动小数点,调整为 1.xxxx2×2E1.xxxx_2\times2^E1.xxxx2×2E 的形式。这样能够统一表示方式,避免同一个数值出现多种等价的科学计数法形式;

而且还有一个好处,因为M始终是大于等于1的,所以,整数部分的1,可以不用存储,只存储小数部分。

例如:

101.112=1.01112×22 101.11_2=1.0111_2\times2^2 101.112=1.01112×22

所以:

s=0,M=1.01112,E=2 s=0,\qquad M=1.0111_2,\qquad E=2 s=0,M=1.01112,E=2

这里需要区分数学上的浮点数形式 与实际存储时的二进制编码。

在 IEEE 754 的常见二进制格式中,浮点数分为三个字段:

text 复制代码
┌───────┬──────────────┬────────────────────┐
│   s   │     exp      │        frac        │
└───────┴──────────────┴────────────────────┘
 符号位      指数字段          小数字段

其中:

  • sss 直接表示符号。
  • exp\mathrm{exp}exp 用于编码实际指数 EEE,但存储的值不直接等于 EEE。
  • frac\mathrm{frac}frac 用于编码有效数 MMM,但存储的位模式也不直接等于完整的 MMM。

为什么指数和有效数不直接保存,而是采用额外的编码规则?这是因为还需要兼顾负指数、有效位利用率以及零和特殊值的表示,后面再逐步展开。

IEEE 754 中最常见的两种二进制浮点格式是:

格式 总位数 符号位 sss 指数字段 exp\mathrm{exp}exp 小数字段 frac\mathrm{frac}frac
单精度(binary32) 323232 111 888 232323
双精度(binary64) 646464 111 111111 525252

在常见的 C/C++ 平台中,float\mathrm{float}float 对应 323232 位单精度浮点数,double\mathrm{double}double 对应 646464 位双精度浮点数。

双精度相比单精度,不仅具有更多有效数字位,而且指数字段更宽,因此通常同时提供更高的计算精度和更大的数值表示范围。

值得说的一点,这里的字段位数还不能直接说明浮点数具有多少位有效精度,因为在规格化表示中,有效数最高位的 111 可以不实际存储,从而额外获得一位有效精度。

至此,浮点数的整体设计思路已经明确:通过有效数保存精度,通过指数调整范围,再通过统一的编码规则将两者组合起来。

IEEE 754 规格化浮点数

前面我们已经知道,IEEE 754 采用类似二进制科学计数法的形式表示浮点数:

V=(−1)s×M×2E V=(-1)^s\times M\times2^E V=(−1)s×M×2E

其中,符号位 sss 决定正负,有效数 MMM 保存有效数字,指数 EEE 决定数值的数量级。实际存储时,浮点数被划分为三个字段:

text 复制代码
┌─────┬─────────────┬───────────────────────┐
│  s  │     exp     │         frac          │
└─────┴─────────────┴───────────────────────┘

但这里还有两个问题没有解决:指数 EEE 既可能为正也可能为负,应该怎样存入指数字段?有效数 MMM 又应该如何在有限的位数中尽可能保留更多精度?

为了解决这两个问题,IEEE 754 分别采用了偏置指数编码(Biased Exponent)和隐藏最高有效位(Implicit Leading One)。

首先考虑指数。以单精度浮点数为例,指数字段占用 888 位,能够形成 28=2562^8=25628=256 种不同的位模式。如果将其直接解释为无符号整数,显然无法表示负指数。

一种直观的解决方式是使用补码,但 IEEE 754 并没有这样设计,而是采用偏置编码(Biased Encoding):将实际指数加上一个固定偏置值,使正指数和负指数都能够转换为非负整数保存。

对于规格化浮点数,实际指数与存储指数之间满足:

E=Exp−Bias \boxed{E=Exp-Bias} E=Exp−Bias

其中:

  • EEE 表示实际参与计算的指数。
  • ExpExpExp 表示将 exp\mathrm{exp}exp 字段按无符号整数解释得到的值。
  • BiasBiasBias 表示固定的指数偏置值。

如果指数字段占用 kkk 位,那么 IEEE 754 采用的偏置值为:

Bias=2k−1−1 \boxed{Bias=2^{k-1}-1} Bias=2k−1−1

因此,对于单精度浮点数:

k=8,Bias=27−1=127 k=8,\qquad Bias=2^7-1=127 k=8,Bias=27−1=127

对于双精度浮点数:

k=11,Bias=210−1=1023 k=11,\qquad Bias=2^{10}-1=1023 k=11,Bias=210−1=1023

例如,在单精度浮点数中,如果希望表示实际指数 E=−3E=-3E=−3,则需要存储:

Exp=−3+127=124 Exp=-3+127=124 Exp=−3+127=124

对应的二进制为:

124=011111002 124=01111100_2 124=011111002

同理,实际指数 E=0E=0E=0 和 E=3E=3E=3 分别对应:

Exp=127=011111112 Exp=127=01111111_2 Exp=127=011111112

Exp=130=100000102 Exp=130=10000010_2 Exp=130=100000102

可以发现,通过加上固定偏置值,负指数和正指数都被转换为无符号整数,而且原来的指数大小关系得到了保留。

例如:

−3<0<3 -3<0<3 −3<0<3

经过偏置编码:

124<127<130 124<127<130 124<127<130

这正是偏置编码相比补码的一项重要优势。

在补码表示中,负数的最高位为 111,如果直接将整个指数位模式当成无符号整数比较,就会出现负指数反而大于正指数的问题。例如,888 位补码中的 −3-3−3 为 11111101211111101_2111111012,按照无符号整数解释为 253253253,明显大于正数 333 的位模式 00000011200000011_2000000112。

而偏置编码使指数的二进制位模式与实际数值保持相同的排序关系。这样,在符号和其他条件适当处理后,可以更容易地根据编码位模式比较浮点数的大小。

不过,IEEE 754 并没有将指数字段的全部 256256256 种位模式都用于规格化浮点数。其中,全 000 和全 111 的指数字段被保留,用于表示其他特殊类型的浮点数。

因此,对于单精度规格化浮点数:

1≤Exp≤254 1\le Exp\le254 1≤Exp≤254

由于 Bias=127Bias=127Bias=127,实际指数范围为:

−126≤E≤127 \boxed{-126\le E\le127} −126≤E≤127

双精度浮点数同理:

1≤Exp≤2046 1\le Exp\le2046 1≤Exp≤2046

−1022≤E≤1023 \boxed{-1022\le E\le1023} −1022≤E≤1023

由此可见,指数位数决定了浮点数能够覆盖的数量级范围。增加指数字段位数,就能够表示更大或更小的数值。

解决了指数的编码问题,接下来需要考虑有效数 MMM。

对于任意非零的二进制数,都可以通过移动二进制小数点,将其写成:

1.xxxx2×2E 1.xxxx_2\times2^E 1.xxxx2×2E

例如:

101.1012=1.011012×22 101.101_2=1.01101_2\times2^2 101.1012=1.011012×22

0.0011012=1.1012×2−3 0.001101_2=1.101_2\times2^{-3} 0.0011012=1.1012×2−3

这类表示称为规格化表示(Normalized Representation)。通过调整指数,可以让有效数始终满足:

1≤M<2 \boxed{1\le M<2} 1≤M<2

这里隐藏着一个很重要的性质:规格化二进制数的有效数,最高位一定是 111。

既然最高位永远都是 111,还有必要专门使用一个二进制位保存它吗?

实际上没有必要。IEEE 754 直接省略这个固定的最高位,只保存二进制小数点右侧的部分,并在解释规格化浮点数时自动补上 前导 111。

例如,有效数:

M=1.1011012 M=1.101101_2 M=1.1011012

实际保存的小数字段只需要:

frac=101101... frac=101101\ldots frac=101101...

解码时,再将隐藏的 111 恢复到最前面:

M=1.frac M=1.frac M=1.frac

这就是隐藏最高有效位 Implicit Leading One的设计。

对于具有 ttt 位小数字段的规格化浮点数,假设各位分别为 f1,f2,...,ftf_1,f_2,\ldots,f_tf1,f2,...,ft,那么实际有效数为:

M=1+∑i=1tfi2−i \boxed{ M=1+\sum_{i=1}^{t}f_i2^{-i} } M=1+i=1∑tfi2−i

其中,fif_ifi 只能取 000 或 111。

如果 frac\mathrm{frac}frac 全部为 000:

M=1.000...02=1 M=1.000\ldots0_2=1 M=1.000...02=1

如果 frac\mathrm{frac}frac 全部为 111:

M=1.111...12 M=1.111\ldots1_2 M=1.111...12

由于:

∑i=1t2−i=1−2−t \sum_{i=1}^{t}2^{-i}=1-2^{-t} i=1∑t2−i=1−2−t

因此:

Mmax⁡=2−2−t M_{\max}=2-2^{-t} Mmax=2−2−t

这说明规格化有效数的最大值可以无限接近 222,但始终小于 222

更重要的是,隐藏最高位能够在不增加实际存储空间的情况下,多获得一位有效数字精度。

例如,单精度浮点数的小数字段只有 232323 位,但由于额外隐含了最高位的 111,因此实际拥有 242424 位二进制有效精度。

双精度浮点数的小数字段为 525252 位,加上隐藏的最高位后,实际具有 535353 位二进制有效精度。

格式 小数字段 隐藏最高位 实际二进制有效精度
单精度 232323 位 111 位 242424 位
双精度 525252 位 111 位 535353 位

至此,规格化浮点数的编码规则已经完整建立:

V=(−1)s×(1+∑i=1tfi2−i)×2Exp−Bias \boxed{ V=(-1)^s\times \left(1+\sum_{i=1}^{t}f_i2^{-i}\right) \times2^{Exp-Bias} } V=(−1)s×(1+i=1∑tfi2−i)×2Exp−Bias

其中,exp\mathrm{exp}exp 字段采用偏置编码,frac\mathrm{frac}frac 字段保存有效数的小数部分,最高有效位 111 不需要实际存储。

下面通过一个完整的例子,观察十进制数如何转换成 IEEE 754 单精度浮点数。

假设:

c 复制代码
float f = 15213.0f;

我们希望确定它在内存中对应的 323232 位浮点数编码。

首先,将十进制整数 152131521315213 转换为二进制:

1521310=111011011011012 15213_{10}=11101101101101_2 1521310=111011011011012

由于该数为正数,因此符号位为:

s=0 s=0 s=0

接下来进行规格化。将二进制小数点从最右侧向左移动 131313 位,使有效数满足 1≤M<21\le M<21≤M<2:

111011011011012=1.11011011011012×213 11101101101101_2= 1.1101101101101_2\times2^{13} 111011011011012=1.11011011011012×213

因此:

M=1.11011011011012 M=1.1101101101101_2 M=1.11011011011012

E=13 E=13 E=13

现在已经得到数学上的有效数和指数,接下来需要按照 IEEE 754 的编码规则分别存储。

先处理小数字段。由于有效数最高位的 111 是隐含的,所以只需要提取二进制小数点右侧的部分:

1101101101101 1101101101101 1101101101101

单精度浮点数要求小数字段占用 232323 位 ,因此在末尾补充 101010 个 000:

frac=11011011011010000000000 \boxed{frac=11011011011010000000000} frac=11011011011010000000000

这里需要注意,补零并不会改变有效数的实际数值,因为二进制小数末尾增加 000 不影响其数值。

然后处理指数字段。实际指数为:

E=13 E=13 E=13

由于单精度偏置值为 127127127,因此存储指数应该为:

Exp=E+Bias=13+127=140 Exp=E+Bias=13+127=140 Exp=E+Bias=13+127=140

将 140140140 转换为 888 位二进制:

exp=10001100 \boxed{exp=10001100} exp=10001100

于是,三个字段分别为:

s=0 s=0 s=0

exp=10001100 exp=10001100 exp=10001100

frac=11011011011010000000000 frac=11011011011010000000000 frac=11011011011010000000000

将它们按照 IEEE 754 的规定拼接:

text 复制代码
符号位        指数字段                小数字段

  0      10001100       11011011011010000000000
  │         │                      │
  └─────────┴──────────────────────┘
                    ↓
  01000110011011011011010000000000

最终的 323232 位浮点数编码为:

010001100110110110110100000000002 \boxed{01000110011011011011010000000000_2} 010001100110110110110100000000002

转换为十六进制:

0x466DB400 \boxed{\texttt{0x466DB400}} 0x466DB400

为了确认编码正确,可以反过来进行解码。

首先,符号位 s=0s=0s=0,说明数值为正。

指数字段为:

Exp=100011002=140 Exp=10001100_2=140 Exp=100011002=140

因此:

E=140−127=13 E=140-127=13 E=140−127=13

小数字段恢复隐藏最高位之后:

M=1.11011011011012 M=1.1101101101101_2 M=1.11011011011012

于是:

V=M×213 V=M\times2^{13} V=M×213

V=1.11011011011012×213 V=1.1101101101101_2\times2^{13} V=1.11011011011012×213

将二进制小数点向右移动 131313 位,得到:

V=111011011011012=15213 V=11101101101101_2=15213 V=111011011011012=15213

这说明编码与解码完全一致,且本次转换没有产生精度损失。

这里需要特别区分:虽然整数 152131521315213 与浮点数 15213.015213.015213.0 在数学上表示相同的数值,但它们在内存中的二进制编码完全不同。

例如,在常见的 323232 位整数表示中:

15213int=0x00003B6D 15213_{\mathrm{int}}=\texttt{0x00003B6D} 15213int=0x00003B6D

而 IEEE 754 单精度浮点数:

15213.0float=0x466DB400 15213.0_{\mathrm{float}}=\texttt{0x466DB400} 15213.0float=0x466DB400

因此,整数转换为浮点数并不是简单地保留原来的二进制位模式,而是按照浮点数的符号位、指数字段和小数字段重新编码。

我们还可以使用 C 语言验证浮点数的实际编码:

c 复制代码
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>

int main(void)
{
    float f = 15213.0f;
    uint32_t bits;

    memcpy(&bits, &f, sizeof(bits));

    printf("Floating-point bits: 0x%08" PRIX32 "\n", bits);

    return 0;
}

在采用 IEEE 754 单精度格式的常见平台上,输出为:

text 复制代码
Floating-point bits: 0x466DB400

这里使用 memcpy\mathrm{memcpy}memcpy 复制对象的原始字节,再通过整数类型观察其位模式,可以避免直接使用不兼容指针类型访问对象所带来的严格别名问题。

不过,前面的例子还有一个值得思考的地方:为什么整数 152131521315213 能够被单精度浮点数精确表示,而某些更大的整数却不能?

原因就在于有效数字的位数有限。

单精度浮点数虽然具有 888 位指数字段,能够表示非常大的数值,但它的有效数字只有 242424 位。因此,浮点数的表示范围很大,并不意味着它能够精确区分该范围内的所有整数。

考虑一个规格化浮点数:

V=M×2E V=M\times2^E V=M×2E

其有效数具有 232323 位显式小数位,因此,在相同指数 EEE 下,小数字段最低位的权重为:

2−23 2^{-23} 2−23

对应到最终数值,其权重变为:

2−23×2E=2E−23 2^{-23}\times2^E=2^{E-23} 2−23×2E=2E−23

因此,在规格化区间:

[2E,2E+1) [2^E,2^{E+1}) [2E,2E+1)

相邻两个可表示单精度浮点数之间的间隔为:

Δ=2E−23 \boxed{\Delta=2^{E-23}} Δ=2E−23

这个间隔也可以理解为当前数量级上的相邻浮点数间距(ULP)。

例如,整数 152131521315213 对应的指数为 E=13E=13E=13,因此:

Δ=213−23=2−10=11024 \Delta=2^{13-23}=2^{-10}=\frac1{1024} Δ=213−23=2−10=10241

也就是说,在 152131521315213 附近,相邻两个可表示的单精度浮点数之间相差 11024\frac1{1024}10241,远小于 111,因此整数 152131521315213 可以被精确表示。

但当数值增大到 2242^{24}224 附近时:

Δ=224−23=2 \Delta=2^{24-23}=2 Δ=224−23=2

这意味着在区间:

[224,225) [2^{24},2^{25}) [224,225)

相邻的单精度浮点数之间相差 222,因此已经无法表示所有连续整数。

例如:

224=16777216 2^{24}=16777216 224=16777216

可以精确表示,而:

224+1=16777217 2^{24}+1=16777217 224+1=16777217

位于两个相邻可表示浮点数之间,无法被单精度浮点数精确表示。

在默认的最近偶数舍入模式(Round to Nearest, Ties to Even)下,167772171677721716777217 会被舍入为:

16777216 16777216 16777216

因此,单精度浮点数可以精确表示所有绝对值不超过 2242^{24}224 的整数,但超过这个范围后,就不能保证每一个整数都可以被精确表示。

双精度浮点数由于具有 535353 位有效精度,相应地可以精确表示所有绝对值不超过 2532^{53}253 的整数。

这揭示了浮点数与整数最重要的区别之一:整数在自身表示范围内具有均匀的精度,相邻整数始终相差 111;
而浮点数通过指数扩大表示范围,但相邻可表示数之间的距离也会随数量级增大。

所以,单精度浮点数虽然能够表示远大于 2242^{24}224 的数,却无法精确表示其中的每一个整数。浮点数并不是在整个数轴上均匀分布,而是越接近较大的数值,相邻表示之间的绝对间隔通常越大。

不过,规格化表示本身还有一个限制:由于有效数始终满足 M≥1M\ge1M≥1,因此它无法直接表示 000 ;同时,规格化指数存在最小值,也无法表示任意接近 000 的非零数。

例如,在单精度中,最小的正规格化数为:

Vmin⁡=1.0×2−126 V_{\min}=1.0\times2^{-126} Vmin=1.0×2−126

那么,如何表示 000?对于介于 000 与 2−1262^{-126}2−126 之间的极小数,又应该如何处理,才能避免数值突然从最小规格化数跳变到 000?

这就需要引入 IEEE 754 的另一类表示方式:非规格化浮点数(Denormalized / Subnormal Numbers)

IEEE 754 非规格化数与特殊值

前面已经知道,IEEE 754 使用以下形式表示浮点数:

V=(−1)s×M×2E V=(-1)^s\times M\times2^E V=(−1)s×M×2E

对于规格化浮点数,有效数始终满足 1≤M<21\le M<21≤M<2,因此可以省略最高位固定的 111,只保存小数部分 frac\mathrm{frac}frac,从而在有限的存储空间中额外获得一位有效精度。

但这种设计也引入了一个问题:既然有效数始终包含隐含的 111,那么即使小数字段全部为 000,浮点数也不可能表示真正的 000

例如,IEEE 754 单精度浮点数的规格化指数范围为:

−126≤E≤127 -126\le E\le127 −126≤E≤127

因此,最小的正规格化浮点数只能取:

Vmin⁡=1.0×2−126=2−126 V_{\min}=1.0\times2^{-126}=2^{-126} Vmin=1.0×2−126=2−126

那么,比 2−1262^{-126}2−126 更小的正数应该如何表示?

如果直接将这些数全部舍入为 000,就会在 000 与 2−1262^{-126}2−126 之间出现一大片无法表示的数值区间,使接近零的计算结果突然丢失。

为了解决这一问题,IEEE 754 专门保留了指数字段 exp\mathrm{exp}exp 全为 000 的编码,用来表示非规格化数(Denormalized Numbers,也称 Subnormal Numbers)。

非规格化数仍然采用相同的数值公式:

V=(−1)s×M×2E V=(-1)^s\times M\times2^E V=(−1)s×M×2E

但它与规格化数存在两个关键区别:

  • 有效数不再隐含最高位的 111,而是隐含最高位的 000。
  • 实际指数固定为 E=1−BiasE=1-BiasE=1−Bias,不再按照 E=Exp−BiasE=Exp-BiasE=Exp−Bias 计算。

因此,当 exp\mathrm{exp}exp 全为 000 时,有效数的形式由原来的 1.xxxx21.xxxx_21.xxxx2 变为 0.xxxx20.xxxx_20.xxxx2:

M=∑i=1tfi2−i \boxed{ M=\sum_{i=1}^{t}f_i2^{-i} } M=i=1∑tfi2−i

其中,ttt 表示小数字段的位数。

这意味着非规格化数的有效数满足:

0≤M<1 0\le M<1 0≤M<1

于是,在保持指数较小的情况下,通过不断减小 MMM,就能表示比最小规格化数更接近 000 的数值。

但这里还有一个值得深入思考的问题:为什么当 exp=0\mathrm{exp}=0exp=0 时,实际指数不是 E=−BiasE=-BiasE=−Bias,而是 E=1−BiasE=1-BiasE=1−Bias?

原因是为了让规格化数与非规格化数之间保持连续、平滑的过渡

对于单精度浮点数,Bias=127Bias=127Bias=127。最小规格化数对应的指数字段为:

Exp=1 Exp=1 Exp=1

因此,其实际指数为:

E=1−127=−126 E=1-127=-126 E=1−127=−126

当 exp\mathrm{exp}exp 全为 000 时,IEEE 754 仍然规定:

E=1−127=−126 E=1-127=-126 E=1−127=−126

也就是说,最大的非规格化数与最小的规格化数采用相同的实际指数,区别只在于有效数是否包含隐含的最高位 111。

为了观察这种设计的效果,先考虑单精度浮点数。

其小数字段占用 232323 位,因此最大的非规格化有效数为:

Mmax⁡=0.111...111⏟23位2=1−2−23 M_{\max}=0.\underbrace{111\ldots111}_{23\text{位}}{}_2 =1-2^{-23} Mmax=0.23位 111...1112=1−2−23

由于非规格化指数固定为 −126-126−126,最大的正非规格化数为:

Vmax⁡,sub=(1−2−23)2−126 V_{\max,\mathrm{sub}} =(1-2^{-23})2^{-126} Vmax,sub=(1−2−23)2−126

展开得到:

Vmax⁡,sub=2−126−2−149 \boxed{ V_{\max,\mathrm{sub}}=2^{-126}-2^{-149} } Vmax,sub=2−126−2−149

而最小的正规格化数为:

Vmin⁡,normal=1.0×2−126=2−126 V_{\min,\mathrm{normal}}=1.0\times2^{-126}=2^{-126} Vmin,normal=1.0×2−126=2−126

因此,两者之间的间隔恰好为:

Δ=2−126−(2−126−2−149) \Delta=2^{-126}-(2^{-126}-2^{-149}) Δ=2−126−(2−126−2−149)

Δ=2−149 \boxed{\Delta=2^{-149}} Δ=2−149

再来看最小的正非规格化数。当 frac\mathrm{frac}frac 只有最低位为 111 时:

M=2−23 M=2^{-23} M=2−23

因此:

Vmin⁡,sub=2−23×2−126 V_{\min,\mathrm{sub}}=2^{-23}\times2^{-126} Vmin,sub=2−23×2−126

Vmin⁡,sub=2−149 \boxed{V_{\min,\mathrm{sub}}=2^{-149}} Vmin,sub=2−149

这意味着,从 000 开始,正非规格化数依次为:

0,2−149,2×2−149,3×2−149,... 0,\quad2^{-149},\quad2\times2^{-149},\quad3\times2^{-149},\ldots 0,2−149,2×2−149,3×2−149,...

直到最大的非规格化数:

(223−1)2−149 (2^{23}-1)2^{-149} (223−1)2−149

再往后一个可表示数,恰好就是最小规格化数:

223×2−149=2−126 2^{23}\times2^{-149}=2^{-126} 223×2−149=2−126

因此,在非规格化数区域,相邻可表示数之间的间隔始终为:

Δsub=2−149 \boxed{\Delta_{\mathrm{sub}}=2^{-149}} Δsub=2−149

而且这个间隔与最小规格化数附近的间隔完全一致。

这就是指数采用 E=1−BiasE=1-BiasE=1−Bias 的根本原因:让非规格化数与规格化数在交界处保持相同的数值间隔,避免出现不必要的巨大空隙。

为了更直观地理解这一过程,可以构造一个简化的 888 位浮点数格式:

text 复制代码
┌─────┬────────────┬───────────┐
│  s  │    exp     │   frac    │
└─────┴────────────┴───────────┘
 1 位      4 位         3 位

其指数字段为 444 位,因此:

Bias=24−1−1=7 Bias=2^{4-1}-1=7 Bias=24−1−1=7

对于非规格化数:

E=1−7=−6 E=1-7=-6 E=1−7=−6

而有效数由 333 位小数字段直接决定:

M=0.f1f2f3 M=0.f_1f_2f_3 M=0.f1f2f3

因此,非规格化数可以依次表示为:

exp\mathrm{exp}exp frac\mathrm{frac}frac 有效数 MMM 实际数值
000000000000 000000000 000 000
000000000000 001001001 1/81/81/8 1/5121/5121/512
000000000000 010010010 2/82/82/8 2/5122/5122/512
000000000000 011011011 3/83/83/8 3/5123/5123/512
⋯\cdots⋯ ⋯\cdots⋯ ⋯\cdots⋯ ⋯\cdots⋯
000000000000 111111111 7/87/87/8 7/5127/5127/512

可以发现,每当小数字段增加 111,实际数值就增加:

2−3×2−6=2−9=1512 2^{-3}\times2^{-6}=2^{-9}=\frac1{512} 2−3×2−6=2−9=5121

当非规格化数达到最大值 7/5127/5127/512 后,继续增加一个编码单位,指数字段由 000000000000 变成 000100010001,小数字段则回到 000000000:

text 复制代码
最大非规格化数:

0 | 0000 | 111

最小规格化数:

0 | 0001 | 000

此时,由于指数字段不再全为 000,有效数恢复为包含隐含最高位 111 的规格化形式:

M=1.0002=1 M=1.000_2=1 M=1.0002=1

实际指数为:

E=1−7=−6 E=1-7=-6 E=1−7=−6

因此:

Vmin⁡,normal=1×2−6=8512 V_{\min,\mathrm{normal}}=1\times2^{-6}=\frac8{512} Vmin,normal=1×2−6=5128

可以看到,最大的非规格化数与最小规格化数之间,只相差:

8512−7512=1512 \frac8{512}-\frac7{512}=\frac1{512} 5128−5127=5121

恰好等于非规格化区域中相邻两个数之间的间隔。

非规格化数通过取消隐藏最高位的 111,让有效数可以逐渐接近 000;通过保持与最小规格化数相同的指数,又保证了两个表示区间能够平滑衔接。

这种机制通常称为渐进下溢(Gradual Underflow)。

它使浮点数在逐渐接近 000 时,不会立即失去所有非零表示,而是先进入非规格化区域,使用更少的有效数字继续表示较小的数值。

不过,非规格化数虽然扩大了接近 000 的表示范围,却并没有保持与规格化数相同的相对精度。因为非规格化数的指数固定,而有效数不断缩小,其有效数字位数也会逐渐减少。

因此,非规格化数的特点是:绝对间隔保持固定,但越接近 000,相对精度通常越差。

那么,既然非规格化数允许有效数的隐含最高位为 000,当小数字段也全部为 000 时,就可以真正表示零:

M=0,V=(−1)s×0×2E=0 M=0,\qquad V=(-1)^s\times0\times2^E=0 M=0,V=(−1)s×0×2E=0

但这里又出现一个特殊现象:IEEE 754 保留了符号位,因此零实际上具有两种不同的二进制编码。

当符号位为 000 时:

s=0,exp=0,frac=0 s=0,\quad exp=0,\quad frac=0 s=0,exp=0,frac=0

得到正零:

+0 +0 +0

当符号位为 111 时:

s=1,exp=0,frac=0 s=1,\quad exp=0,\quad frac=0 s=1,exp=0,frac=0

得到负零:

−0 -0 −0

也就是说,IEEE 754 中的正零与负零具有不同的位模式,但在普通数值比较中被认为相等。

例如,在常见的 IEEE 754 浮点环境下:

c 复制代码
double x = 0.0;
double y = -0.0;

printf("%d\n", x == y);  // 1

既然二者相等,为什么还需要区分正零和负零?

因为零的符号可以保留某些计算结果的方向信息。例如,一个很小的负数在舍入后可能变成负零,而不是正零。

此外,零的符号还会影响某些浮点运算。在遵循 IEEE 754 默认异常处理方式的环境下:

1+0=+∞ \frac1{+0}=+\infty +01=+∞

1−0=−∞ \frac1{-0}=-\infty −01=−∞

因此,正零与负零虽然在数值比较中相等,但并不意味着它们在所有浮点运算中都具有完全相同的行为。

既然 IEEE 754 已经为零附近的数值提供了特殊表示,那么对于另一端的问题------当运算结果超过最大有限浮点数时,应该如何处理?

对于固定宽度的无符号整数,溢出会按照模运算回绕;

但浮点数不能简单采用这种方式,否则一个非常大的正数可能突然变成极小数,这通常不符合数值计算的需要。

因此,IEEE 754 将指数字段全为 111 的编码保留出来,用于表示无穷大和其他特殊值。

当:

exp=11...112 exp=11\ldots11_2 exp=11...112

并且:

frac=00...002 frac=00\ldots00_2 frac=00...002

时,表示无穷大(Infinity)。

符号位决定它是正无穷大还是负无穷大:

s=0⇒+∞ s=0\Rightarrow+\infty s=0⇒+∞

s=1⇒−∞ s=1\Rightarrow-\infty s=1⇒−∞

无穷大可以表示某些超出有限浮点数范围的运算结果,也可以出现在非零有限数除以零等运算中。

例如,在默认的最近值舍入模式及非陷阱异常处理下,一个过大的正数运算结果可能成为:

+∞ +\infty +∞

但需要注意,浮点溢出不一定在所有舍入模式下都得到无穷大。采用某些定向舍入模式时,结果也可能被限制到最大有限浮点数。

无穷大与普通有限数之间仍然具有明确的比较关系:

−∞<x<+∞ -\infty<x<+\infty −∞<x<+∞

其中 xxx 为任意有限浮点数。

部分涉及无穷大的运算也具有确定结果:

+∞+1=+∞ +\infty+1=+\infty +∞+1=+∞

+∞×2=+∞ +\infty\times2=+\infty +∞×2=+∞

但并非所有涉及无穷大的运算都能得到明确的数值。例如:

+∞−(+∞) +\infty-(+\infty) +∞−(+∞)

其结果无法确定,因为两个趋向正无穷的量相减,可能趋向不同的结果。

类似地:

0×∞ 0\times\infty 0×∞

也没有一个能够统一适用于所有情况的有限数值结果。

这就需要 IEEE 754 的另一类特殊值:NaN(Not a Number,非数值)。

当指数字段全部为 111,而小数字段不全为 000 时:

exp=11...112 exp=11\ldots11_2 exp=11...112

frac≠0 frac\ne0 frac=0

对应的就是 NaN。

NaN 用于表示某些无法得到正常浮点数结果的运算,例如:

−1 \sqrt{-1} −1

∞−∞ \infty-\infty ∞−∞

0×∞ 0\times\infty 0×∞

这里讨论的是实数范围内的 IEEE 754 浮点运算,而不是复数运算。

NaN 还有一个非常特殊的性质:它不参与普通的数值大小关系,包括与自身比较也不相等。

例如:

c 复制代码
double x = NAN;

printf("%d\n", x == x);  // 0
printf("%d\n", x != x);  // 1

对于 NaN,普通的 <<<、>>>、≤\le≤ 和 ≥\ge≥ 比较通常都返回假;而"不等于"比较返回真。

因此,不能像判断普通数值一样,通过比较大小判断一个浮点数是否为 NaN。实际程序中通常使用数学库提供的 isnan\mathrm{isnan}isnan 等函数进行检测。

此外,NaN 还可以区分不同的编码形式,例如静默 NaN(Quiet NaN)和信号 NaN(Signaling NaN),后者与浮点异常处理有关。对于一般的数值程序,最重要的是理解:NaN 表示当前浮点结果不具有可用于普通数值比较的有效数值,不能将它当成零或者无穷大处理。

至此,IEEE 754 的三类指数编码就全部建立起来了:

指数字段 exp\mathrm{exp}exp 小数字段 frac\mathrm{frac}frac 表示类型 数值规则
全 000 全 000 正零或负零 (−1)s×0(-1)^s\times0(−1)s×0
全 000 非零 非规格化数 M=0.frac, E=1−BiasM=0.frac,\ E=1-BiasM=0.frac, E=1−Bias
非全 000、非全 111 任意 规格化数 M=1.frac, E=Exp−BiasM=1.frac,\ E=Exp-BiasM=1.frac, E=Exp−Bias
全 111 全 000 正无穷或负无穷 +∞+\infty+∞ 或 −∞-\infty−∞
全 111 非零 NaN 非普通数值

这些编码共同解决了浮点数的不同需求:规格化数在较大的动态范围内提供有效精度;非规格化数让数值能够逐渐接近零;带符号零保留零附近的符号信息;无穷大和 NaN 则处理超出有限表示范围或无法得到普通数值结果的运算。

如果将所有浮点数放到数轴上观察,可以发现,有限的非规格化数分布在最接近零的区域,随后平滑过渡到规格化数,而正负无穷大位于数值范围的两端。

NaN 则不同,它不属于数轴上的某个确定位置,也不能简单认为它比所有普通数字都大或都小。

IEEE 754 浮点数的分布规律与编码特性

前面已经知道,IEEE 754 使用符号位、指数字段和小数字段共同表示浮点数:

V=(−1)s×M×2E V=(-1)^s\times M\times2^E V=(−1)s×M×2E

其中,有效数 MMM 决定数值的有效数字,指数 EEE 决定数值的数量级。对于规格化浮点数,M=1.fracM=1.fracM=1.frac;对于非规格化浮点数,M=0.fracM=0.fracM=0.frac,且指数固定为 1−Bias1-Bias1−Bias。

这种编码方式虽然能够扩大数值范围,但也引出了一个问题:同样是相邻两个浮点数,它们之间的数值差距是否始终相同?

为了分析这个问题,先考虑一个简化的 888 位浮点数格式:

text 复制代码
┌─────┬───────────┬─────────┐
│  s  │    exp    │  frac   │
└─────┴───────────┴─────────┘
 1 位      4 位      3 位

由于指数字段占 444 位,偏置值为:

Bias=24−1−1=7 Bias=2^{4-1}-1=7 Bias=24−1−1=7

小数字段占 333 位,因此对于规格化数,有效数具有以下形式:

M=1.f1f2f3 M=1.f_1f_2f_3 M=1.f1f2f3

如果指数字段固定,那么指数 EEE 就固定。此时,每当小数字段增加 111,有效数就增加:

ΔM=2−3=18 \Delta M=2^{-3}=\frac18 ΔM=2−3=81

对应的实际数值增加:

ΔV=2E−3 \boxed{\Delta V=2^{E-3}} ΔV=2E−3

这意味着,在相同指数下,相邻两个规格化浮点数之间的间隔是固定的。但当指数发生变化时,这个间隔也会跟着变化。

例如,当实际指数为 E=0E=0E=0 时,浮点数依次为:

1.0002×20=1 1.000_2\times2^0=1 1.0002×20=1

1.0012×20=1.125 1.001_2\times2^0=1.125 1.0012×20=1.125

1.0102×20=1.25 1.010_2\times2^0=1.25 1.0102×20=1.25

相邻数值之间的间隔为:

ΔV=20−3=18 \Delta V=2^{0-3}=\frac18 ΔV=20−3=81

而当指数增加到 E=1E=1E=1 时:

1.0002×21=2 1.000_2\times2^1=2 1.0002×21=2

1.0012×21=2.25 1.001_2\times2^1=2.25 1.0012×21=2.25

1.0102×21=2.5 1.010_2\times2^1=2.5 1.0102×21=2.5

此时相邻数值之间的间隔变成:

ΔV=21−3=14 \Delta V=2^{1-3}=\frac14 ΔV=21−3=41

可以发现,当指数增加 111 时,数值的数量级扩大 222 倍,相邻浮点数之间的绝对间隔也扩大 222 倍。

这就是浮点数在数轴上并非均匀分布的根本原因。

对于具有 ttt 位小数字段的规格化浮点数,其数值为:

V=(1+∑i=1tfi2−i)2E V=\left(1+\sum_{i=1}^{t}f_i2^{-i}\right)2^E V=(1+i=1∑tfi2−i)2E

在指数固定的情况下,小数字段最低位的权重为 2−t2^{-t}2−t,因此相邻规格化浮点数之间的间隔为:

ΔV=2E−t \boxed{\Delta V=2^{E-t}} ΔV=2E−t

对于单精度浮点数,t=23t=23t=23:

ΔV=2E−23 \Delta V=2^{E-23} ΔV=2E−23

对于双精度浮点数,t=52t=52t=52:

ΔV=2E−52 \Delta V=2^{E-52} ΔV=2E−52

因此,浮点数的绝对精度并不是固定不变的,而是由当前指数决定。

为了更直观地观察数值分布,可以进一步简化为 666 位浮点格式:

text 复制代码
┌─────┬───────────┬─────────┐
│  s  │    exp    │  frac   │
└─────┴───────────┴─────────┘
 1 位      3 位      2 位

其指数字段为 333 位,因此:

Bias=23−1−1=3 Bias=2^{3-1}-1=3 Bias=23−1−1=3

小数字段为 222 位,所以规格化有效数只能取:

M∈{1,54,32,74} M\in\left\{1,\frac54,\frac32,\frac74\right\} M∈{1,45,23,47}

我们只考虑正数,观察不同指数字段下能够表示的数值:

exp\mathrm{exp}exp 实际指数 EEE 可表示的正规格化数 相邻间隔
001001001 −2-2−2 14,516,38,716\frac14,\frac5{16},\frac38,\frac7{16}41,165,83,167 116\frac1{16}161
010010010 −1-1−1 12,58,34,78\frac12,\frac58,\frac34,\frac7821,85,43,87 18\frac1881
011011011 000 1,54,32,741,\frac54,\frac32,\frac741,45,23,47 14\frac1441
100100100 111 2,52,3,722,\frac52,3,\frac722,25,3,27 12\frac1221
101101101 222 4,5,6,74,5,6,74,5,6,7 111
110110110 333 8,10,12,148,10,12,148,10,12,14 222

这个表格清楚说明:当指数从 −2-2−2 增大到 333 时,相邻规格化浮点数之间的间隔,从 116\frac1{16}161 逐渐增大到 222。

因此,同样使用有限位数,浮点数能够在接近零的区域提供较密集的表示,而在较大数值区域采用较稀疏的表示。

但需要注意,浮点数不是简单地"越接近零,精度越高",而是越接近零,规格化浮点数之间的绝对间隔越小

因为精度还可以分为绝对精度和相对精度。

绝对精度关注两个可表示数之间的实际差值,而相对精度则关注误差与数值本身的比例。

对于规格化浮点数:

2E≤∣V∣<2E+1 2^E\le |V|<2^{E+1} 2E≤∣V∣<2E+1

相邻浮点数的间隔为:

ΔV=2E−t \Delta V=2^{E-t} ΔV=2E−t

因此:

2−(t+1)<ΔV∣V∣≤2−t 2^{-(t+1)}<\frac{\Delta V}{|V|}\le2^{-t} 2−(t+1)<∣V∣ΔV≤2−t

也就是说,规格化浮点数的绝对间隔会随指数改变,但相对于数值大小的间隔比例始终保持在同一个量级。

这也是浮点数适合科学计算的重要原因:它并不是在整个数轴上保持固定的绝对精度,而是在较大的动态范围内维持近似稳定的相对精度。

不过,这一规律并不能 直接推广到非规格化数。

非规格化数的指数固定为:

E=1−Bias E=1-Bias E=1−Bias

而有效数不再具有隐含的最高位 111,因此其数值为:

V=(∑i=1tfi2−i)21−Bias V=\left(\sum_{i=1}^{t}f_i2^{-i}\right)2^{1-Bias} V=(i=1∑tfi2−i)21−Bias

每当小数字段增加 111,实际数值增加:

ΔVsub=21−Bias−t \boxed{\Delta V_{\mathrm{sub}}=2^{1-Bias-t}} ΔVsub=21−Bias−t

由于指数固定,非规格化数之间的绝对间隔始终相同,不会继续随着数值接近零而缩小。

回到前面的 666 位浮点格式:

Bias=3,t=2 Bias=3,\qquad t=2 Bias=3,t=2

非规格化数的实际指数为:

E=1−3=−2 E=1-3=-2 E=1−3=−2

因此,非规格化数依次为:

0,116,216,316 0,\quad\frac1{16},\quad\frac2{16},\quad\frac3{16} 0,161,162,163

随后便进入最小的正规格化数:

14=416 \frac14=\frac4{16} 41=164

可以看到,从 000 到最小规格化数之间,相邻数值的间隔始终为 116\frac1{16}161。

再往后,规格化数开始随着指数的增加逐渐扩大间隔。

这意味着 IEEE 754 的数值分布具有一个重要特点:非规格化区域采用固定的绝对间隔,规格化区域则采用随指数成倍变化的绝对间隔。

因此,非规格化数虽然能够填补零附近的数值空隙,但越接近零,其相对间隔就越大,能够保留的相对精度也越低。

这种浮点数分布还引出了另一个值得注意的性质:IEEE 754 不仅能够表示数值,而且其二进制编码在一定条件下还保留了数值的大小顺序。

首先考虑正浮点数。

对于规格化正数:

V=M×2E V=M\times2^E V=M×2E

当指数相同时,有效数 MMM 越大,实际数值就越大。而有效数的大小又由小数字段 frac\mathrm{frac}frac 决定,因此:

frac1<frac2  ⟹  V1<V2 frac_1<frac_2\implies V_1<V_2 frac1<frac2⟹V1<V2

如果指数不同,指数较大的规格化正数,通常对应更大的数值区间;在两个正常的相邻指数区间之间,较大指数对应的最小值仍大于较小指数对应的最大值。

例如,前面的 666 位浮点格式中:

Exp=3⇒E=0 Exp=3\Rightarrow E=0 Exp=3⇒E=0

其最大规格化数为:

1.112×20=74 1.11_2\times2^0=\frac74 1.112×20=47

而下一个指数:

Exp=4⇒E=1 Exp=4\Rightarrow E=1 Exp=4⇒E=1

其最小规格化数为:

1.002×21=2 1.00_2\times2^1=2 1.002×21=2

显然:

74<2 \frac74<2 47<2

因此,对于正的有限浮点数,指数字段越大,数值越大;指数字段相同时,小数字段越大,数值也越大。

这种编码顺序还能够自然衔接非规格化数与规格化数。

例如,在相同的 666 位格式中:

text 复制代码
最大非规格化数:

0 | 000 | 11      = 3/16

最小规格化数:

0 | 001 | 00      = 4/16

从二进制位模式来看:

0000112<0001002 000011_2<000100_2 0000112<0001002

从实际数值来看:

316<416 \frac3{16}<\frac4{16} 163<164

两种顺序完全一致。

因此,对于非负且不是 NaN 的 IEEE 754 浮点数,如果将其完整位模式按无符号整数解释,那么位模式的大小顺序与浮点数的数值大小顺序一致。

这一性质甚至可以延伸到正无穷大,因为正无穷大的指数字段全为 111,小数字段全为 000,其位模式恰好位于所有有限正浮点数之后。

但负数不能直接采用同样的规则。

原因在于,IEEE 754 使用最高位作为符号位,而负数的指数越大、有效数越大,其绝对值越大,实际数值反而越小。

例如,单精度浮点数中:

−1.0⟶0xBF800000 -1.0\longrightarrow\texttt{0xBF800000} −1.0⟶0xBF800000

−2.0⟶0xC0000000 -2.0\longrightarrow\texttt{0xC0000000} −2.0⟶0xC0000000

按照无符号整数比较:

0xC0000000>0xBF800000 \texttt{0xC0000000}>\texttt{0xBF800000} 0xC0000000>0xBF800000

但按照浮点数值比较:

−2.0<−1.0 -2.0<-1.0 −2.0<−1.0

因此,负数的位模式顺序与实际数值顺序恰好相反。

如果希望仅通过位模式实现浮点数的有序比较,就需要根据符号位采用不同的处理方式:

  • 对非负数,可以通过适当调整符号位,使其排在负数之后,并保留原来的编码顺序。
  • 对负数,需要反转其位模式顺序,使绝对值更大的负数排在更前面。

不过,这种方法还必须特殊处理两个问题。

首先,IEEE 754 中的正零和负零具有不同的位模式:

+0.0≠bits−0.0 +0.0\ne_{\text{bits}}-0.0 +0.0=bits−0.0

但普通浮点数比较认为:

+0.0=−0.0 +0.0=-0.0 +0.0=−0.0

其次,NaN 不具有正常的数值顺序关系,即使它的位模式能够按整数大小排序,也不能因此认为它在数学上大于或小于某个浮点数。

例如:

NaN<1.0 NaN<1.0 NaN<1.0

NaN>1.0 NaN>1.0 NaN>1.0

NaN=1.0 NaN=1.0 NaN=1.0

这些普通比较均为假。

因此,IEEE 754 的编码具有很好的数值排序性质,但并不能不加处理地将所有浮点数位模式转换为无符号整数后直接比较。

这种编码顺序还有一个实际意义:IEEE 754 的设计不仅关注如何在有限位数中保存数值,还尽量使不同数值之间的排列关系体现在二进制位模式中,从而简化部分浮点处理与比较操作。

不过,浮点数即使拥有明确的编码规则,也仍然无法精确表示所有实数。因此,两个能够精确表示的浮点数相加或相乘,其数学结果也未必能够被当前精度完整保存。

这就引出了浮点运算与整数运算之间的根本区别:整数溢出主要涉及固定位宽下的截断,而浮点运算除了可能超出指数范围,还需要在有限有效位数内对结果进行舍入。

因此,接下来必须进一步讨论 IEEE 754 的舍入规则,以及为什么浮点加法和乘法不一定遵守普通数学中的结合律、分配律。

IEEE 754 最近偶数舍入

前面已经知道,IEEE 754 使用有限的二进制位保存浮点数,因此它只能表示实数中的一部分。即使两个浮点数本身能够被精确表示,它们相加或相乘的结果也不一定能够被当前格式完整保存。

例如,假设某种浮点格式只能保留二进制小数点后的 222 位,那么它能够表示:

10.002=2 10.00_2=2 10.002=2

10.012=2.25 10.01_2=2.25 10.012=2.25

10.102=2.5 10.10_2=2.5 10.102=2.5

10.112=2.75 10.11_2=2.75 10.112=2.75

但如果计算结果为 2.18752.18752.1875,其二进制表示为:

2.1875=10.00112 2.1875=10.0011_2 2.1875=10.00112

这个数需要 444 位二进制小数才能精确表示,显然无法直接放入只有 222 位小数的格式中。

那么应该怎么办?一种简单的方法是直接丢弃多余的低位:

10.00112⟶10.002=2 10.0011_2\longrightarrow10.00_2=2 10.00112⟶10.002=2

但这种处理不一定得到最接近真实结果的数值。例如,2.18752.18752.1875 距离 222 为 0.18750.18750.1875,距离 2.252.252.25 只有 0.06250.06250.0625,显然选择 2.252.252.25 更准确。

因此,浮点数不能简单地通过截断低位处理所有精度损失,而是需要根据舍入规则,在有限的可表示数值中选择合适的近似结果。

IEEE 754 将浮点运算抽象为:先求数学上的精确结果,再按照目标格式的有效精度进行舍入。

对于浮点加法和乘法,可以分别表示为:

x+fy=Round⁡(x+y) \boxed{x+_fy=\operatorname{Round}(x+y)} x+fy=Round(x+y)

x×fy=Round⁡(x×y) \boxed{x\times_fy=\operatorname{Round}(x\times y)} x×fy=Round(x×y)

这里的 Round⁡\operatorname{Round}Round 表示将数学上的精确结果舍入到目标浮点格式。实际硬件不一定真的先保存一个无限精度的完整结果,而是通过有限的额外运算位和舍入逻辑,实现标准要求的正确舍入结果。

当然,如果指数超出目标浮点格式的表示范围,还可能发生溢出;如果结果过于接近零,则可能进入非规格化区域或发生下溢。因此,浮点运算的误差不仅来自有效数字位数不足,还与指数范围和舍入方式有关。

那么,究竟应该怎样舍入?

IEEE 754 定义了四种常见的二进制浮点舍入方向:

舍入模式 规则
向零舍入(Toward Zero) 选择朝向 000 的可表示值
向负无穷舍入(Round Down) 选择不大于精确结果的最大可表示值
向正无穷舍入(Round Up) 选择不小于精确结果的最小可表示值
最近偶数舍入(Round to Nearest, Ties to Even) 选择距离最近的可表示值;距离相同时选择最低有效位为 000 的结果

为了理解它们的区别,暂时假设只能保存整数,并观察下面几个数值:

精确数值 向零 向负无穷 向正无穷 最近偶数
1.401.401.40 111 111 222 111
1.601.601.60 111 111 222 222
1.501.501.50 111 111 222 222
2.502.502.50 222 222 333 222
−1.50-1.50−1.50 −1-1−1 −2-2−2 −1-1−1 −2-2−2

前三种模式的区别比较直观。例如,−1.5-1.5−1.5 向零舍入得到 −1-1−1,向负无穷舍入得到 −2-2−2,向正无穷舍入仍然得到 −1-1−1。

真正值得关注的是最近偶数舍入 ,它也是 IEEE 754 默认采用的舍入模式。

最近偶数舍入首先遵循一个原则:优先选择与精确结果距离最近的可表示数值。只有当精确结果恰好位于两个相邻可表示数的中点时,才需要使用"偶数"规则决定最终结果。

例如:

1.4⟶1 1.4\longrightarrow1 1.4⟶1

1.6⟶2 1.6\longrightarrow2 1.6⟶2

这两种情况不存在争议,因为其中一个整数明显更接近原始数值。

但对于 1.51.51.5,它与 111、222 的距离完全相同:

∣1.5−1∣=∣1.5−2∣=0.5 |1.5-1|=|1.5-2|=0.5 ∣1.5−1∣=∣1.5−2∣=0.5

此时选择偶数 222:

1.5⟶2 1.5\longrightarrow2 1.5⟶2

同理:

2.5⟶2 2.5\longrightarrow2 2.5⟶2

3.5⟶4 3.5\longrightarrow4 3.5⟶4

−1.5⟶−2 -1.5\longrightarrow-2 −1.5⟶−2

这里需要注意,最近偶数舍入并不是任何时候都选择偶数,而是只在恰好处于中点时,通过偶数规则消除歧义。

为什么不直接采用我们熟悉的"四舍五入",例如遇到 0.50.50.5 就统一向上舍入?

原因在于,这种规则可能产生系统性的舍入偏差。

例如,考虑一组恰好位于两个整数中间的正数:

1.5,2.5,3.5,4.5 1.5,\quad2.5,\quad3.5,\quad4.5 1.5,2.5,3.5,4.5

如果全部采用中点向上舍入:

2,3,4,5 2,\quad3,\quad4,\quad5 2,3,4,5

每次误差都是:

+0.5 +0.5 +0.5

因此,累积误差必然朝同一个方向增加。

而采用最近偶数舍入:

2,2,4,4 2,\quad2,\quad4,\quad4 2,2,4,4

对应误差为:

+0.5,−0.5,+0.5,−0.5 +0.5,\quad-0.5,\quad+0.5,\quad-0.5 +0.5,−0.5,+0.5,−0.5

在这个例子中,正负误差恰好抵消。

因此,最近偶数舍入能够避免 "所有中点都朝同一个方向舍入"造成的系统性偏差。在适当的数据分布下,这种方式通常具有更好的统计性质。当然,它并不能保证任意一组数据的舍入误差都恰好相互抵消。

最近偶数舍入不仅可以用于保留整数,也可以用于保留任意小数位。

例如,假设希望将数值舍入到小数点后 222 位,即最接近的百分位。

考虑:

7.8949999⟶7.89 7.8949999\longrightarrow7.89 7.8949999⟶7.89

因为它距离 7.897.897.89 更近。

而:

7.8950001⟶7.90 7.8950001\longrightarrow7.90 7.8950001⟶7.90

因为它距离 7.907.907.90 更近。

当数值恰好为 7.89500007.89500007.8950000 时:

∣7.895−7.89∣=∣7.895−7.90∣=0.005 |7.895-7.89|=|7.895-7.90|=0.005 ∣7.895−7.89∣=∣7.895−7.90∣=0.005

两个候选结果距离相同,需要比较保留的最后一位:

  • 7.897.897.89 的最后一位为 999,属于奇数。
  • 7.907.907.90 的最后一位为 000,属于偶数。

因此:

7.8950000⟶7.90 \boxed{7.8950000\longrightarrow7.90} 7.8950000⟶7.90

反过来:

7.8850000 7.8850000 7.8850000

恰好位于 7.887.887.88 与 7.897.897.89 之间。由于 7.887.887.88 的最后一位为偶数 888,因此:

7.8850000⟶7.88 \boxed{7.8850000\longrightarrow7.88} 7.8850000⟶7.88

可以发现,这里的"偶数"并不是要求整个数值为偶数,而是要求舍入后保留的最低有效数字为偶数。

这种规则可以推广到二进制,只不过二进制只有 000 和 111,因此判断起来反而更加简单。

对于二进制数,如果需要将某个位置之后的低位全部舍弃,那么"最近偶数舍入"中的偶数,就意味着最终保留部分的最低有效位为 000。

例如,假设只保留二进制小数点后的 222 位:

text 复制代码
保留部分 | 舍弃部分

10.00 | 011
10.00 | 110
10.11 | 100
10.10 | 100

判断舍入方向时,只需要分析舍弃部分所代表的数值,与一个舍入单位的一半之间的关系。

假设最后保留位的权重为:

2−k 2^{-k} 2−k

那么相邻两个可表示结果之间的距离就是:

Δ=2−k \Delta=2^{-k} Δ=2−k

中点距离为:

Δ2=2−(k+1) \frac{\Delta}{2}=2^{-(k+1)} 2Δ=2−(k+1)

这意味着,舍弃部分如果正好从一个 111 开始,后面全部为 000:

1000...2 1000\ldots_2 1000...2

就代表恰好位于两个可表示数之间的中点。

因此,二进制最近偶数舍入可以归纳为:

  • 舍弃部分小于 1000...21000\ldots_21000...2:保留原来的低精度结果。
  • 舍弃部分大于 1000...21000\ldots_21000...2:将最后保留位加 111。
  • 舍弃部分恰好等于 1000...21000\ldots_21000...2:检查最后保留位,如果为 000 则不进位,为 111 则进位。

下面结合几个具体例子。

假设目标精度为二进制小数点后 222 位,相邻可表示数之间的间隔为:

Δ=2−2=14 \Delta=2^{-2}=\frac14 Δ=2−2=41

首先考虑:

2+332=2.09375 2+\frac3{32}=2.09375 2+323=2.09375

其二进制表示为:

10.000112 10.00011_2 10.000112

保留两位小数:

text 复制代码
10.00 | 011

由于舍弃部分以 000 开始,说明它小于中点,因此直接舍弃:

10.000112⟶10.002=2 10.00011_2\longrightarrow10.00_2=2 10.000112⟶10.002=2

再考虑:

2+316=2.1875 2+\frac3{16}=2.1875 2+163=2.1875

二进制表示为:

10.001102 10.00110_2 10.001102

舍入位置为:

text 复制代码
10.00 | 110

舍弃部分大于中点 100...2100\ldots_2100...2,因此需要向上舍入,即将保留部分的最后一位加 111:

10.001102⟶10.012=2.25 10.00110_2\longrightarrow10.01_2=2.25 10.001102⟶10.012=2.25

接下来考虑一个恰好位于中点的数:

2+78=2.875 2+\frac78=2.875 2+87=2.875

其二进制表示为:

10.111002 10.11100_2 10.111002

保留两位小数:

text 复制代码
10.11 | 100

舍弃部分恰好为 100100100,说明数值正好处于两个可表示数之间。

两个候选结果分别为:

10.112=2.75 10.11_2=2.75 10.112=2.75

11.002=3 11.00_2=3 11.002=3

由于 10.11210.11_210.112 的最低有效位为 111,而 11.00211.00_211.002 的最低有效位为 000,因此应选择后者:

10.111002⟶11.002=3 \boxed{10.11100_2\longrightarrow11.00_2=3} 10.111002⟶11.002=3

注意这里发生了进位传播:原本保留的两位小数均为 111,增加一个最低有效位单位后,进位最终传递到了整数部分。

再考虑:

2+58=2.625 2+\frac58=2.625 2+85=2.625

二进制表示为:

10.101002 10.10100_2 10.101002

同样保留两位小数:

text 复制代码
10.10 | 100

它也恰好位于两个可表示数之间:

10.102=2.5 10.10_2=2.5 10.102=2.5

10.112=2.75 10.11_2=2.75 10.112=2.75

但这次,10.10210.10_210.102 的最低有效位已经为 000,因此不需要进位:

10.101002⟶10.102=2.5 \boxed{10.10100_2\longrightarrow10.10_2=2.5} 10.101002⟶10.102=2.5

这两个例子说明,是否进位不能只看舍弃部分是否恰好为一半,还必须结合最后保留位的奇偶性判断。

为了将这种舍入规则真正用于硬件计算,还需要解决一个问题:当一个浮点运算产生许多额外的低位时,是否必须保存所有被舍弃的位,才能判断结果应该如何舍入?

实际上不需要。

在实现最近偶数舍入时,可以利用少量额外信息判断舍弃部分与中点的关系。常见的方法是保留保护位(Guard Bit)、舍入位(Round Bit)和粘滞位(Sticky Bit)。

假设:

  • LLL:当前保留结果的最低有效位。
  • GGG:第一个被舍弃的二进制位。
  • RRR:第二个被舍弃的二进制位。
  • SSS:其余所有被舍弃位进行按位或运算的结果。

例如:

text 复制代码
保留的有效位           被舍弃的部分

1.101101 | 1  0  001010
       L   G  R    其余位

这里:

L=1,G=1,R=0 L=1,\quad G=1,\quad R=0 L=1,G=1,R=0

由于剩余位中至少存在一个 111:

S=1 S=1 S=1

那么,最近偶数舍入什么时候需要进位?

首先,如果 G=0G=0G=0,说明被舍弃部分小于一个舍入单位的一半,因此不需要进位。

如果 G=1G=1G=1,并且 R=1R=1R=1 或 S=1S=1S=1,说明舍弃部分大于一半,需要进位。

如果:

G=1,R=0,S=0 G=1,\qquad R=0,\qquad S=0 G=1,R=0,S=0

说明舍弃部分恰好为一半,此时就需要检查最低保留位 LLL:

  • L=0L=0L=0:保留结果已经是偶数,不进位。
  • L=1L=1L=1:保留结果是奇数,需要进位。

因此,最近偶数舍入的进位条件可以表示为:

Increment=G∧(R∨S∨L) \boxed{ Increment=G\land(R\lor S\lor L) } Increment=G∧(R∨S∨L)

这条规则非常重要,因为它把看似复杂的浮点舍入,转化为几个二进制位之间的布尔运算。

需要注意,上式描述的是对已经正确对齐的有效数进行最近偶数舍入的核心判断。实际浮点运算还需要处理符号、指数调整、规格化、溢出、非规格化数以及舍入进位后的再次规格化等情况。

例如,如果舍入前的有效数为:

1.111...1112 1.111\ldots111_2 1.111...1112

舍入后需要进位,就可能得到:

10.000...0002 10.000\ldots000_2 10.000...0002

但规格化有效数要求:

1≤M<2 1\le M<2 1≤M<2

因此,需要将小数点再向左调整一位,并将指数增加 111:

10.000...0002×2E=1.000...0002×2E+1 10.000\ldots000_2\times2^E= 1.000\ldots000_2\times2^{E+1} 10.000...0002×2E=1.000...0002×2E+1

这也说明,浮点舍入不仅可能改变最低有效位,还可能通过进位影响整个有效数,甚至改变指数。

理解这一点后,就可以进一步解释为什么浮点数的精度误差受到限制。

在最近舍入模式下,如果某个有限精确结果位于两个相邻可表示浮点数之间,且没有特殊的溢出问题,那么舍入后的绝对误差最多为两者间隔的一半:

∣Round⁡(x)−x∣≤12ULP⁡ \boxed{ |\operatorname{Round}(x)-x| \le\frac12\operatorname{ULP} } ∣Round(x)−x∣≤21ULP

其中,ULP⁡\operatorname{ULP}ULP 表示当前区间内相邻可表示浮点数之间的间隔。

对于具有 ttt 位小数字段的规格化浮点数:

ULP⁡=2E−t \operatorname{ULP}=2^{E-t} ULP=2E−t

因此:

∣Round⁡(x)−x∣≤2E−t−1 |\operatorname{Round}(x)-x| \le2^{E-t-1} ∣Round(x)−x∣≤2E−t−1

由于规格化数的绝对值至少为 2E2^E2E,其相对舍入误差通常满足:

∣Round⁡(x)−x∣∣x∣≤2−(t+1) \boxed{ \frac{|\operatorname{Round}(x)-x|}{|x|} \le2^{-(t+1)} } ∣x∣∣Round(x)−x∣≤2−(t+1)

对于单精度浮点数,t=23t=23t=23:

2−24≈5.96×10−8 2^{-24}\approx5.96\times10^{-8} 2−24≈5.96×10−8

对于双精度浮点数,t=52t=52t=52:

2−53≈1.11×10−16 2^{-53}\approx1.11\times10^{-16} 2−53≈1.11×10−16

这些数值反映了默认最近舍入模式下,规格化浮点数能够达到的典型相对精度水平。它们并不代表所有浮点计算的最终误差都不会超过这个界限,因为连续计算中的误差还可能传播和累积。

另外,当计算结果进入非规格化区域时,固定的绝对间隔会使相对误差明显增大,因此不能直接使用相同的规格化相对误差界限。

由此可见,浮点数的舍入规则不仅决定了某一次计算应该保留哪个数值,还直接影响运算误差的大小、误差是否存在系统性偏差,以及连续数值计算的可靠性。

而一旦理解了浮点舍入,就能够进一步解释一个看似违反数学规律的现象:为什么在计算机中,浮点数加法和乘法并不总是满足结合律?

原因正是浮点运算不是无限精度的数学运算,而是在每一步运算结束后,都可能对结果进行舍入。

相关推荐
FPGA小迷弟40 分钟前
用 Claude Code 重构遗留系统:从评估到落地的完整实践指南
c++·python·ai·plotly·django·ai编程·claude
程序员-King.1 小时前
【ROS1】Noetic 开发工作流程与常用命令
c++·ubuntu·ros
云深麋鹿1 小时前
C++ | 基于红黑树实现mymap与myset
开发语言·c++
傲世仙尊3 小时前
string增删查改-insert-erase-replace与find加substr组合技
c++
·present·4 小时前
CSAPP(lecture 3)
c++
本地化文档5 小时前
llvm-libc-docs-l10n
c语言·github·gitcode·sphinx·llvm·crowdin
别动我齐刘海5 小时前
机器人 C++ / 系统联调补强2——异常处理、Protobuf
linux·c++·人工智能·学习·算法·机器学习·机器人
basketball6165 小时前
C++ 中字符串与常见类型的转换总结
开发语言·c++
等我调个Bug5 小时前
C++智能指针通俗讲解
开发语言·c++