IEEE754标准: 一 , float类型浮点数在内存中的存储方式

这是最近在研究IEEE754标准的过程中, 总结的一些笔记, 大致包容如下内容:

1. float类型的浮点数在内存中的存储方式

2. float类型的取值范围是如何计算的

3. float类型的精度是如何计算的

4. 简单的介绍一下±0, ±infinity和NaN

文章中包含了一些自己理解的部分, 如果有什么错误或不妥的地方, 欢迎大家讨论并指出.

本篇文章介绍的是第一部分:  float类型浮点数在内存中的存储方式


IEEE754标准

IEEE754是关于如何在计算机中表示浮点数的一个工业标准, 发布于1985年, 包括JS, Java, C在内的许多编程语言在实现浮点数时, 都遵循IEEE754标准. 

IEEE754的最新标准是IEEE754-2008, 但本篇文章主要参考的是IEEE754-1985, 好在两者相差并不大, 而参照1985的标准可以让我们对一些基础概念有更好的理解

IEEE754提供了四种精度规范, 其中最常用的是单精度浮点型双精度浮点型, 但IEEE754并没有规定32位浮点数类型需要叫做float, 或64位浮点数需要叫做double. 它只是提供了关于如何存储不同精度浮点数的一些规范和标准. 不过为了方便行文, 下文如果提到float, 指的就是IEEE754标准中的32位单精度浮点数. 提到double指的则是IEEE754标准中的64位双精度浮点数. 

下面是单精度浮点数和双精度浮点数的一些信息, 可以先简单看一下, 看不懂也没关系, 下文会对这里的信息做详细的解释...

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第1张图片
单双精度信息对比

好啦, 铺垫完了, 开始正文吧~


float类型在内存中的存储方式

IEEE754标准提供了如何在计算机内存中, 以二进制的方式存储十进制浮点数的具体标准, 这里以float类型(即32位单精度类型)为例, 这是其在内存中存储方式的示意图:

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第2张图片
float类型存储方式示意图

用float类型存储一个十进制的浮点数, 共需要32个二进制内存位(对应到图中就是32个方块). 二进制内存位编号从高到低 (从31到0), 包含如下几个部分:

sign: 符号位, 即图中蓝色的方块

biased exponent: 偏移后的指数位, 即图中绿色的方块

fraction: 尾数位, 即图中红色的方块

下面会依次介绍这三个概念


符号位: sign

以float类型 (即32位单精度类型, 以下不再赘述) 为例: 

符号位: 占据最高位(第31位)这一位, 用于表示这个浮点数是正数还是负数, 为0表示正数, 为1表示负数.

举例: 对于十进制数20.5, 存储在内存中时, 符号位为0, 因为这是个正数


偏移后的指数位: biased exponent

以float类型为例: 

指数位: 占据第30位到第23位这8位. 用于表示以2位底的指数. 至于这个指数的作用, 下文会详细讲解, 这里只需要知道: 8位二进制可以表示256种状态, IEEE754规定, 指数位用于表示[-127, 128]范围内的指数.

不过为了表示起来更方便, 浮点型的指数位都有一个固定的偏移量(bias), 来使指数位变为非负整数. 这样指数部分就不用为如何表示负数而担心了. 规定: 在32位单精度类型中, 这个偏移量是127. 在64位双精度类型中, 偏移量是1023.

所以这里偏移量是127,

即如果你运算后得到的指数是 -127, 那么偏移后, 在指数位中需要表示为: -127 + 127(偏移量) = 0

如果你运算后得到的指数是 -10, 那么偏移后, 在指数位中需要表示为: -10 + 127(偏移量) = 117

看, 有了偏移量, 指数位就始终是一个非负整数了. 

看到这里, 可能会觉得还不是很清楚指数的作用到的是什么. 没关系, 让我们先继续往下看吧...


尾数位:fraction

依旧以32位float为例.

尾数位: 占据剩余的22位到0位这23位. 用于存储尾数.

在以二进制格式存储十进制浮点数时, 首先需要把十进制浮点数表示为二进制格式, 还拿十进制数20.5举例:

十进制浮点数20.5 = 二进制10100.1

然后需要把这个二进制数转换为以2为底的指数形式:

二进制10100.1 = 1.01001 * 2^4

注意转换时, 对于乘号左边, 加粗的那个二进制数1.01001, 需要把小数点放在左起第一位和第二位之间. 且第一位需要是个非0数. 这样表示好之后, 其中的1.01001就是尾数. 

这种用二进制表示十进制浮点数, 表示为尾数*指数形式, 把尾数的小数点放在第一位和第二位之间, 然后保证第一位数非0的处理过程, 叫做规范化(normalized)

我们再来看看规范化之后的这个数: 1.01001 * 2^4

其中1.01001是尾数,  而4就是偏移前的指数(unbiased exponent), 上文讲过, 32位float类型, 偏移量(bias)为127, 所以这里加上偏移量之后, 得到的偏移后指数(biased exponent)就是4 + 127 = 131, 131转换为二进制就是1000 0011

现在还需要对尾数做一些特殊处理

1. 隐藏高位1.

你会发现, 尾数部分的最高位始终为1. 比如这里的 1.01001, 这是因为前面说过, 规范化之后, 尾数中的小数点会位于左起第一位和第二位之间. 且第一位是个非0数. 而二进制中, 每一位可取值只有0或1, 如果第一位非0, 则第一位只能为1. 所以在存储尾数时, 可以省略前面的 1和小数点. 只记录尾数中小数点之后的部分, 这样就节约了一位内存. 所以这里只需记录剩余的尾数部分: 01001

所以, 以后再提到尾数, 如无特殊说明, 指的其实是隐藏了整数部分1. 之后, 剩下的小数部分

2. 低位补0

如果尾数不够填满尾数位(即图中的红色部分). 比如这里的, 尾数不够23位, 则在低位补零, 补齐23位. 

之所以在低位补0, 是因为尾数中存储的本质上是二进制的小数部分, 所以如果想要在不影响原数值的情况下, 填满23位, 就需要在低位补零

例如,  把二进制数1.01在不改变原值的情况下填满八位内存, 写出来就是: 1.010 0000, 即需要在低位补0

题外话: 如果尾数位置存储的是整数部分(而非小数部分), 想要在不影响原数值的情况下补齐n位, 就需要在高位补0了,

例如. 把二进制数10在不改变原值的情况下填满八位内存, 写出来就是: 0000 0010, 即需要在高位补0

因为尾数部分存储的本质上是省略了1.之后的小数部分, 所以这里需要低位补0, 

原尾数是01001: 补零之后是 0100 1000 0000 0000 000


表示十进制浮点数20.5

在上面的讨论中, 已经得出

十进制20.5的符号位: 0

偏移后指数位: 1000 0011

补零后尾数位: 0100 1000 0000 0000 000

按顺序放在32位float容器中, 就是 0    1000 0011    0100 1000 0000 0000 000

这就在32位内存中, 以二进制表示了一个十进制数20.5的方式

这里有一个可以验证的IEEE754浮点数内存状态的网站, 我们来验证一下:

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第3张图片

可见验证是通过的. 不过为了加深理解, 我们再反向推导一遍: 

假设现在我们有一个用二进制表示的32位浮点数: 0   1000 0011   0100 1000 0000 0000 000, 求它表示的十进制浮点数是多少?

观察可知: 

符号位为0: 所以这是个正数.

尾数是: 0100 1000 0000 0000 000

去掉后面的补零, 再加上隐藏的整数部分1.  得到完整的尾数(含隐藏的整数部分)为: 1.01001

偏移后的指数位为: 1000 0011, 转换为十进制为131, 减去偏移量127, 得4

所以, 最后得到的浮点数 = 尾数(含隐藏的整数部分) * 以2为底的指数次幂

=  二进制的: 1.01001 * 2^4 

=  把小数点向右移动4位

=  二进制的10100.1 

=  十进制位20.5

附带的, 这里还有一个进制转换网站, 可以看到二进制的10100.1, 确实等于十进制的20.5

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第4张图片

到这里就讲解的差不多了, 

随后是一张大体的计算方法示意图

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第5张图片

还有双精度类型的内存状态示意图:

IEEE754标准: 一 , float类型浮点数在内存中的存储方式_第6张图片

下一篇会讲述为什么32位单精度浮点数的取值范围是±1.18*10^-38 到  ±3.4 * 10^38, 这个值究竟是如何计算出来的...

下一篇再见吧~

你可能感兴趣的:(IEEE754标准: 一 , float类型浮点数在内存中的存储方式)