• 再谈浮点数


    写在前面:本文要求对浮点的编码表示已有一定基础,如果对「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 的数值变化会更平滑)。

  • 相关阅读:
    痛苦之旅——安装Eric4
    如何把自己写的python程序给别人用
    (转)史上最好的Python线程指南
    (转)python编码问题
    Beautiful Soup的一些中文资料
    oracle监听配置
    redhat6.5安装oracle 11g
    《深入浅出MFC》– Document-View深入探讨
    CAS解扰小结
    ts包、表、子表、section的关系
  • 原文地址:https://www.cnblogs.com/sinkinben/p/12387524.html
Copyright © 2020-2023  润新知