再谈浮点数

Posted sinkinben

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再谈浮点数相关的知识,希望对你有一定的参考价值。

写在前面:本文要求对浮点的编码表示已有一定基础,如果对「IEEE 754」这一个词组不熟悉,请勿继续阅读,以免浪费时间。

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

先来复习一下基本知识:对于一个浮点数,计算机采用「科学计数法」去表示:
[ 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 imes 2^{E} imes(-1)^{S}) 。其中,(E)exp 相关,二者的值并不相等(什么关系可以看下面)。

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

对于 (1.0101 imes 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 imes 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 imes 2^{exp-bias} imes (-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 imes 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 imes 0, infty - infty) 属于此类情况。

引入「非规格化数」之后,就解决了 0 的编码问题。对于 0x00000000 ,可得 exp = 0x00, frac = 0...0 ,因此 (E=1-127=-126) ,所以 (val = 0.frac imes 2^E = 0.0...0 imes 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 quad underflow) 的性质(后续我打算做一个图示来说明为什么引入「非规格化数」之后,float 的数值变化会更平滑)。

以上是关于再谈浮点数的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL:为啥我不能将单个浮点数从顶点着色器传递到片段着色器?

内部格式为 GL_RGBA8 的纹理在片段着色器中显示为浮点数

JavaScript 有用的代码片段和 trick

工作小数点数

再谈接口测试

再谈编码---小数据池(概念)