计算机中浮点数的表示

重温 ICS,又学习了一遍计算机中的浮点数的 IEEE 754 编码表示,似乎又有了一些新的理解。

先来复习一下基本知识:对于一个浮点数,计算机采用「科学计数法」去表示:

\[value = (-1)^S \cdot M \cdot 2^{E} \]

\(S\) 表示这个数的符号。其中 \(M\) 总是为 1.xxxxx 这种形式( x 代表 0/1 ),\(E\) 是指数。

好了,下面正式「复习」IEEE 754 浮点数标准。

浮点数编码

在 IEEE 754 标准当中,float 采用 32 bit 去保存,double 采用 64 bit 保存,格式如下:

float
---------------------------
| S |  exp  |    frac     |
| 1 |   8   |     23      |
---------------------------

double
----------------------------
| S |  exp   |    frac     |
| 1 |   11   |     52      |
----------------------------

exp 为指数,frac 为尾数,S 为符号位。其数值表示:\(value = 1.frac \times 2^{E} \times(-1)^{S}\) 。其中,\(E\)exp 相关,二者的值并不相等(什么关系可以看下面)。

这里为什么是 1.frac 呢?因为对于任意的二进制小数表示,总是可以化为 \(1.frac \times 2^{E}\) 的形式。例如,\(10.01 = 1.001 \times 2^1\)\(0.000101 = 1.01 \times 2^{-4}\) 。所以只需要记录 小数点后的尾数 即可(节省了 1 bit ,这也体现了计算机系统的设计哲学:尽最大努力进行优化)。

对于 \(1.0101 \times 2^{-4}\) ,易知 frac = 0101 ,需要注意的是,在内存中,frac 的域从高位到低位,分别是 0101 0000 0000 0000 0000 000 。不足 23 位,后面的所有 bit 设置为 0 。

值得重新「复习」的是 exp 这一部分。

对于 exp 来说,任何时候我们都把解释为无符号数,并且保留 2 种特殊情况:

  • exp == 0x00 :表示非规格化浮点数 (Denormalized Number) 。
  • exp == 0xff :表示 2 种特殊值(后面会进一步陈述)。

8 bit 的无符号数,去除最大的全 0 ,和最小的全 1 ,其取值范围为 \([1,254]\) 。显然,这无法表示 \(1.01 \times 2^{-4}\) 这种负指数的情况。因此,在 \([1,254]\) 区间内,需要对这些数值平均分配,做一次映射,一半表示负指数,一半表示正指数,因此有:

\[E = exp - 127 \]

为什么是 127 呢?因为 \([1,254]\) 有 254 个数,127 是 254 的一半,刚好一半表示负指数,一半表示正指数。因此 \(E\) 的取值范围为:

\[E \in [-126, 127] \]

127 就是所谓的偏置常数 \(bias\) ,其数值由下面公式计算所得:

\[bias = 2^{k-1} - 1 \]

其中 \(k\)exp 的比特位数。因此 float\(bias\) 是 127 ,double\(bias\) 是 1023 。

综上所述,对于一个规格化的比特串,应采用下面的公式转换为二进制表示的小数:

\[value = 1.frac \times 2^{exp-bias} \times (-1)^{S} \]

Aside
为什么不把 exp 看作是有符号数?这样采取补码的方式去记录,不就能直接表示正负指数?

这是为了方便在硬件层面比较 floatdouble 。对于正浮点数 ab ,就可以从高位到低位进行比较,规则与 unsigned int 一模一样,如果存在 bit(a,i) > bit(b,i), i=31...0 ,那么就说明 \(a>b\)

如果将 exp 采用补码的方式,那么在比较 2 个浮点数时,除了要比较 float 的符号位,还需要对 exp 的符号位进行比较。

考虑到比特串 0x00000000 ,可以得到 s = 0, exp = 0x00, frac = 0...0如果不考虑非规格化数的特殊规则 ,即不保留 exp = 0x00 这种特殊情况,\(E = exp - 127 = -127\) ,对应的数值应当为 \(1.0 \times 2^{-127}\) ,但很不幸,这应当是 +0 的编码。因此,我们才需要引入非规格化数,并且重新解释 expfrac 。换句话说,如果我们不引入非规格化数,我们就无法在 float 中表示数值 0 ,因为我们的 frac 总是隐含为 1.frac

Aside
为什么要强调是 +0 呢?

因为在「IEEE 754」标准当中,+0-0 虽然都是 0 ,但在科学计算的某些特殊场合,它们是表示不同的含义的,因此给 0 保留了这 2 种编码(可参考 CSAPP 的 2.4 小节)。

好了,我们回到 exp 的特殊情况:

  • exp = 0x00 :表示非规格化浮点数

    此时,\(E = 1 - bias\) ,而不是 \(0-bias\)frac 应该解释为 0.xxxxx 这种形式,而不是 1.xxxxx (后面解释原因)。实质上相当于小数点左移一位 ,指数 exp 加一作为「补偿」。

  • exp = 0xff :表示 2 种特殊值

    • frac = 0 时,表示 \(+\infty\) 或者 \(-\infty\) ,与之做运算会 overflow 。例如,1.0/0.0-1.0/0 属于这种情况。
    • frac != 0 时,表示 NaN (Not A Number) 。例如,\(sqrt(-1), \infty \times 0, \infty - \infty\) 属于此类情况。

引入「非规格化数」之后,就解决了 0 的编码问题。对于 0x00000000 ,可得 exp = 0x00, frac = 0...0 ,因此 \(E=1-127=-126\) ,所以 \(val = 0.frac \times 2^E = 0.0...0 \times 2^{-126} = 0\)

实际上,「非规格化数」的引入还有一个好处:实现最大非规格化数到最小规格化数的平滑转变

在 32 位 float 中,显然非规格化数的所有编码为:

s exp       frac
0 0000 0000 0000 0000 0000 0000 0000 000  => 0
0 0000 0000 0000 0000 0000 0000 0000 001  => 0.00000000000000000000001
0 0000 0000 0000 0000 0000 0000 0000 010  => 0.00000000000000000000010
...
0 0000 0000 1111 1111 1111 1111 1111 111  => 0.11111111111111111111111

最低位不断加 1 ,对应的数值变化是 \(\frac{1}{2^{23}}\) ,这就使得非规格化数在 0 的附近是均匀分布的,具有 gradual underflow 的性质。

posted @ 2020-03-01 23:38  sinkinben  阅读(803)  评论(0编辑  收藏  举报