这是最近在研究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位双精度浮点数.
下面是单精度浮点数和双精度浮点数的一些信息, 可以先简单看一下, 看不懂也没关系, 下文会对这里的信息做详细的解释...
好啦, 铺垫完了, 开始正文吧~
float类型在内存中的存储方式
IEEE754标准提供了如何在计算机内存中, 以二进制的方式存储十进制浮点数的具体标准, 这里以float类型(即32位单精度类型)为例, 这是其在内存中存储方式的示意图:
用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浮点数内存状态的网站, 我们来验证一下:
可见验证是通过的. 不过为了加深理解, 我们再反向推导一遍:
假设现在我们有一个用二进制表示的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
到这里就讲解的差不多了,
随后是一张大体的计算方法示意图
还有双精度类型的内存状态示意图:
下一篇会讲述为什么32位单精度浮点数的取值范围是±1.18*10^-38 到 ±3.4 * 10^38, 这个值究竟是如何计算出来的...
下一篇再见吧~