美文网首页
IEEE754规范: 四, 非规格数, ±infinity, N

IEEE754规范: 四, 非规格数, ±infinity, N

作者: 等夏天再见啦 | 来源:发表于2020-01-06 21:59 被阅读0次

    第一章提到过, ieee754标准中, 浮点数包含三种状态

    1. normal number(规格数) 

    2. subnormal number(非规格数)

    3. non-number(特殊数)

    本章详细讲解这三种状态.

    一. 首先, 如何区分这三种状态

    其实这三种状态是通过指数部分区分的, 而且很容易区分.

    以32位浮点数为例, 其内存状态分为3部分:

    1位符号位     8位指数位     23位尾数位

    其中, 如果8位指数位全为0, 就代表当前数是个非规格数. 或者说, 形如 * 00000000 *********************** 格式的数就是非规格数.

    如果8位指数位全为1, 就代表当前数是个特殊数. 或者说, 形如 * 11111111 *********************** 格式的数就是特殊数.

    如果8位指数不全为0, 也不全为1(也就是除去以上两种状态外, 剩下的所有状态), 这个数就是规格数.

    随便几个例子: * 10101100 ***********************就是一个规格数

    可见: 非规格数特殊数是两种特殊状态, 规格数则是非常常见的状态

    示意图:

    注意下图把特殊数分为了两种状态, 无穷大NaN:

    深入理解计算机系统第三版

    二. 这三种状态的作用

    为什么要把浮点数分为这三种状态呢? 答案当然是有用啊, 而且作用相当直观:

    规格数: 用于表示最常见的数值, 比如1.2, 34567, 7.996, 0.2. 但规格数不能表示0和非常靠近0的数.

    非规格数: 用于表示0, 以及非常靠近0的数, 比如1E-38.

    特殊数: 用于表示"无穷"和"NaN":

    浮点数的存储和计算中会涉及到"无穷"这个概念, 比如:

    32位浮点数的取值范围是[-3.4*10^{38}, -1.18*10^{-38}] ∪ [1.18*10^{-38}, 3.4 * 10^{38}], 如果你要往里面存储4e38(这超过了最大的可取值), 32位浮点数就会在内存中这样记录 "你存储的数超过了我的最大表示范围, 那我就记录你存储了一个无穷大..."

    浮点数的存储和计算中还会涉及到"NaN (not a number)"这个概念, 比如:

    你要给一个负数开根号(如 √-1), 但是ieee754标准中的浮点数却不知道该怎么进行这个运算, 它就会在内存中这样记录 "不知道怎么算, 这不是个数值, 记录为NaN"

    可见, 这三种状态都是非常有用的, 作用也非常直观, 下面我们一个个来讲.

    三. 状态1: 规格数

    对于规格数: 

    符号位, 1位: 可正可负

    指数位, 8位: 不全为0, 且不全为1

    对于32位浮点数来说, 规格数的指数位的取值范围是[1, 254], 偏置bias是127, 所以实际的指数是:

    [1 - 127, 254 - 127], 即 [-126, 127]

    注: 关于偏置, 可参见本系列第一章, 此处不再赘述

    尾数位, 23位: 尾数位前隐藏的整数部分是1.  而非 0.

    所以尾数位的取值范围是[1.00000000000000000000000, 1.11111111111111111111111] (二进制)

    换算为10进制为[1,2)

    注: 关于尾数位前隐藏的数, 可参见本系列第一章, 此处不再赘述

    规格数的局限性: 无法表示 0 和 及其靠近0 的数

    原因很简单, ieee754浮点数的求值公式是:

     = ±尾数 * 2^{指数}

    所以可求出32位浮点数的取值范围就是:

    = ±[1,2) * 2^{[-126,127]}

    问题就出现在这里: 

    注意尾数部分: 取值范围是[1, 2), 始终大于1

    注意指数部分:2^{[-126,127]}, 这个数始终大于0, 即便2^-167非常小, 但还是大于0

    那么: 一个始终大于1的数 * 一个始终大于0的数, 永远无法等于0

    事实上, 1(尾数最小值) * 2^-167(指数最小值) = 2^-167.    2^-167就是当前我们能表示的最小值

    也就是说: 使用规格数时, 我们除了无法表示0, 也无法表示(0, 2^-167)之间的, 靠近0的极小数...

    这就是规格数的局限性, 这个局限性将由非规格数解决.

    补充一点:

    其实在本系列的第二章, 我们计算过32位浮点数的取值范围:

    =(-2 * 2^{127}, -1 * 2^{-126}] ∪ [1 * 2^{-126}, 2 * 2^{127})

    所以这里可以画一个示意图:

    ↑ 绿色区域就是32位浮点数中规格数的取值范围, 可见它取不到0和靠近0的极小数

    ↑ 红色区域包含0和靠近0的极小数, 红色区域其实是非规格数的取值范围, 见下一节.

    四. 状态2: 非规格数

    对于非规格数: 

    符号位, 1位: 可正可负

    指数位, 8位: 全为0

    对于32位浮点数来说, 规格数的指数位全为0, 对应的值也是0. 偏置bias依旧是127, 但:

    实际指数的计算方法是: 实际指数 = 1 - bias = 1 - 127 = -126, 即非规格数的实际指数固定为-126. 注意这是规定.

    其实我们可以发现, 非规格数实际指数的计算方法(实际指数 = 1 - bias), 和规格数实际指数的计算方法(实际指数  = 指数位的值 - bias)不同

    后文会看到这样规定的原因.

    尾数位, 23位: 尾数位前隐藏的整数部分是0. 而非 1.

    所以尾数位的取值范围是[0.00000000000000000000000, 0.11111111111111111111111] (二进制)

    换算为10进制为[0,1)

    非规格数的作用: 表示0和靠近0的数

    那么规格数是怎么完成这个任务的呢. 

    首先看看非规格数是怎么表示0的:

    依旧要用到我们的ieee754浮点数求值公式:

     = ±尾数 * 2^{指数}

    然后, 非规格数尾数的取值范围是[0, 1), 指数固定为-126. 这就很简单了, 让尾数取0不就能表示数值0了:

    ±0 * 2^{-126} = ±0

    可见当尾数取0时, 通过变更符号位, 我们可以表示出+0-0, IEEE754规范中也确实存在着这两种表示0的方式

    注: 某些场景下, +0和-0会被认为完全相同, 某些场景下, +0和-0又被认为不完全相同. 这往往取决于具体的编程语言和应用场景, 此处不做讨论. 只需知道IEEE754中可以表示+0和-0即可. +0和-0在IEEE754中是两种内存状态(符号位不同)

    然后看看非规格数是怎么表示接近0的数的:

    准确来说, 我们要看看, 对于32位浮点数, 非规格数是怎么表示出=(-1 * 2^{-126}, 1 * 2^{-126})之间的数的. 也就是如何表示出下图中的红色区域的:

    其实也很简单:

    浮点数求值公式:

     = ±尾数 * 2^{指数}

    然后, 非规格数尾数的取值范围是[0, 1), 指数固定为-126.

    所以, 非规格数的取值范围就是:

    ±[1,2) * 2^{-126}

    等于:

      (-1*2^{-126}, 0] ∪ [0, 1*2^{-126})

    等于:

     (-1*2^{-126}, 1*2^{-126})

    这样就完成了...

    现在我们尝试着把32位浮点数中的非规格数的取值范围, 和规格数的取值范围拼接在一起

    32位浮点数中, 非规格数的取值范围:

     (-1*2^{-126}, 1*2^{-126})

    32位浮点数中, 规格数的取值范围:

    (-2 * 2^{127}, -1 * 2^{-126}] ∪ [1 * 2^{-126}, 2 * 2^{127})

    仔细看一下, 啊...非规格数的取值范围, 正好可以卡在规格数取值范围的中间, 现在我们得到了一个完整的取值范围:

    (-2 * 2^{127},  2 * 2^{127})

    感觉世界一下子清爽了起来. 

    这就是非规格数的作用: 用于表示0和靠近0的数, 用于和规格数"珠联璧合", 形成一个完整的取值范围.

    不过这还没有完...

    五. 非规格数补充

    1. 逐渐溢出

    前文说过, 非规格数尾数的取值范围是[0, 1), 指数固定为-126

    所以是尾数的变化在导致非规格数的值变大, 举例:

    0 00000000 00000000000000000000001

    就比

    0 00000000 00000000000000000000000

    要大一些

    随着尾数逐渐增大, 相应的非规格数也在不断增大:

    ...

    0 00000000 11111111111111111111111 这是非规格数的最大值

    此时尾数(带上隐藏的整数部分0.)其实是0.11111111111111111111111, 是个比1小一点点的数, 不妨记做(1 - ε)

    那, 此时非规格数的值就是 (1-ε) * 2^{-126}

    好, 我们再往前前进一格, 此时会进入规格数的范围:

    0 00000001 00000000000000000000000

    这是个规格数, 

    其尾数位的值: 其实隐藏了 1. 或者说, 此时真正的尾数应该是1.00000000000000000000000 , 也就是1

    其指数位的值: 是1, 则实际指数应该是1 - bias = 1-127 = -126

    所以这个规格数的值就是: 1 * 2^{-126}, 这是规格数的最小值.

    注意到没有: 非规格数的最大值是: (1-ε) * 2^{-126}

    规格数的最小值是: 1 * 2^{-126}

    两者之间实现了非常平滑的过度, 非规格数的最大值非常紧密的连接上了规格数的最小值

    非规格数 "一点点逐渐变大,  最后其最大值平稳的衔接上规格数的最小值", 这种特性在ieee754中被叫做逐渐溢出(gradual underflow)

    明白了这一点, 就很容易想通: 

    ① 为什么规定非规格数的尾数前隐藏的整数部分是 0.  而规格数尾数前隐藏的整数部分是1. 

    ② 为什么非规格数的真实指数的计算公式是 1 - bias, 而规格数的真实指数的计算公式是 指数部分的值 - bias

    仔细思考一下, 就是这些设计实现了逐渐溢出这种特性. 

    ↑ 关于第①点: 这使得非规格数的尾数取值范围是[0,1), 而规格数的尾数取值范围是[1,2), 两者平滑的衔接在了一起

    ↑ 关于第②点: 这使得对于32位浮点数来说, 非规格数的真实指数固定为-126, 而规格数的指数是[-126, 127], 两者也平滑的衔接在了一起...

    2. 密集分布

    第三章中我们说过: 如果把ieee754浮点数想象成一个表盘的话, 那表盘上的蓝点是越来越稀疏的. 或者说越靠近0越密集.

    不过当时仅讨论了规格数的分布情况, 那非规格数呢.

    答案是, 非规格数的蓝点分布间隔, 和规格数中蓝点最密集的区域(也就是最靠近0的区域)一致, 可以验证一下:

    非规格数: 范围是 [0, 1*2^{-126}), 在这个范围中分布了2^23个蓝点, 则蓝点间的间隔是2^{-126} / 2^{23} = 2^{-149}

    规格数中蓝点最密集的区域, 也就是最靠近0的区域是: [2^{-126}, 2^{-125}], 在这个范围中分布了2^23个蓝点, 则蓝点间的间隔是(2^{-125}-2^{-126}) / 2^{23} = 2 ^ {-126}/2^{23} = 2^{-149}

    所以, 即便把非规格数与规格数放在一起审视, ieee754浮点数表盘上的蓝点依旧是越靠近0越密集, 越靠近∞越稀疏

    深入理解计算机系统第三版, 浮点数密度示意图. 中间部分密密麻麻的都是非规格数

    下面是在c语言中的测试结果:

    六. 状态3: 特殊数

    特殊数分为两种: 无穷和NaN

    1. 先说无穷

    理解了非规格数, 再理解无穷就很简单了, 两者有很多相似之处:

    对于无穷: 

    符号位, 1位: 可正可负

    指数位, 8位: 全为1

    尾数位, 23位: 全部为0

    当内存位于上述状态时, 就表示无穷(infinity)

    具体写出来就是: * 11111111 00000000000000000000000 用于表示无穷(infinity)

    其中符号位可正可负, 分别记做+infinity和-infinity

    以32位浮点数为例, 其规格数的取值范围是: 

    (-2 * 2^{127}, -1 * 2^{-126}] ∪ [1 * 2^{-126}, 2 * 2^{127})

    当要存储的数大于规格数取值范围的最大值时, 就会被记做+infinity, 比如2^128, 刚刚超过规格数的取值范围的最大值, 就会被记做+infinity

    当要存储的数小于规格数取值范围的最小值时, 就会被记做-infinity, 比如-2^128, 刚刚小于规格数的取值范围的最小值, 就会被记做-infinity

    需要注意的是: 所有+infinity的内存状态都是0 11111111 00000000000000000000000, 不会有任何变动

    2^128对应的内存状态是0 11111111 00000000000000000000000

    2^123456789对应的内存状态还是0 11111111 00000000000000000000000

    同理, -infinity的内存状态都是1 11111111 00000000000000000000000

    此外: 就像非规格数的最大值可以和规格数的最小值平稳衔接一样, 规格数的最大值也可以和+infinity平稳衔接:

    规格数的最大值是: 0 11111110 11111111111111111111111

    尾数位其实是1.11111111111111111111111, 非常接近2, 不妨记做2-ε

    指数是127

    所以最大值是: (2-ε)*2^{127}

    +infinity的内存状态则是: 0 11111111 00000000000000000000000

    尾数其实是: 1.00000000000000000000000, 等于1

    指数是128

    所以+infinity的内存状态对应的值是: 2^{128}

    可见规格数的最大值也能和+infinity平稳衔接. -infinity同理.

    现在我们就集齐了整个数轴:

    ↑ 而且各个节点都能平稳的衔接在一起

    2. NaN

    NaN则更简单, 前面说过, 如果计算出来的值不是一个数值, 则记录为NaN

    NaN的内存状态是: 

    符号位, 1位: 可正可负

    指数位, 8位: 全为1

    尾数位, 23位: 不全为0即可

    仅仅是一种特殊状态标记而已.

    需要注意的是, 根据wiki, 没有+NaN或-NaN这种说法, 统称为NaN

    七. 总结

    本章介绍了IEEE754规范中的非规格数, 特殊值(±infinity, NaN), 包括它们的内存状态, 作用, 工作原理等.

    下一章会先偏离一下主线, 补充一些之前没有提到的琐碎知识点. 

    下一章再见吧~

    相关文章

      网友评论

          本文标题:IEEE754规范: 四, 非规格数, ±infinity, N

          本文链接:https://www.haomeiwen.com/subject/tfehactx.html