IEEE-754工业标准
前言
众所周知,计算机内部系统实际只能存储二进制数据,我们在计算机中所使用到的文档、图片、影音等数据,实际都是以二进制的数据形式存放在计算机的内存或者硬盘中,不管内存(内存条)还是硬盘,在这里我们且都称之为“内存”,“内存”可以理解为一个大的容器,里面均匀的分配了很多小格子,这些格子只能存放1或者0。
我们日常所使用的小数也不例外,都会以二进制的数据形式存储的到“内存”中;事实上,直到20世纪八十年代,计算机厂商们还在为各自而战,每家厂商都在设计自己的浮点类型数据存储规则,彼此之间都不兼容;直到1985年,IEEE,电气电子工程师学会(Institute of Electrical and Electronics Engineers)提出里IEEE-754工业标准,浮点类型数据存储问题才有了一个通用的工业标准。
IEEE754标准发布于1985年,包括java、C在内许多比较著名的编程语言都在浮点类型数据存储上遵这这个标准;IEEE754标准提供了四项精度规范,其中最常用的是单精度浮点类型和双精度浮点类型,单精度浮点类型在内存中占32位bit,双精度浮点类型在内存中占64位bit,所以我们通常提到的32位浮点类型或者单精度浮点类型一般是指32位单精度浮点类型,双精度浮点类型也同理;在java中,float类型就是单精度浮点类型,double就是双精度浮点类型。
本文主要以单精度浮点类型为例,向大家介绍IEEE-754标准下的浮点数如何存储,以及精度丢失等内容。文章中参考了很多比较知名的博客内容,但绝无抄袭,主要内容是由作者本人对IEEE-754的一些个人理解,希望能给大家带来帮助。
单精度浮点存储概述
每个单精度浮点类型在内存中都占32个bit,可以理解为一个长条状的盒子中有32个小格子,这32个小格子被划分为三个区域(符号位、指数位、尾数位),且都有顺序,从0-31(从右到左)
符号位(sign):
符号位,表示该浮点类型的正负,0为正,1为负
指数位(exponent):
指数位,表示该浮点类型数据的大小容量,8位,容量高达256种不同形态,通过阶计算而来,计算方式为(规格数) 阶 + 偏移量 = 指数,阶码取值范围为[-127,128],单精度偏移量为127,双精度偏移量为1023,偏移是为了是指保持指数的存储始终为正数,指数位格子越多可存储的浮点类型数据就越大
尾数位(fraction):
尾数位,也叫有效数字(后面会详细讲),表示该浮点类型数据的精度,通过浮点数的二进制规范化而来,会将第一位的 1. 隐藏起来,格子越多存储的浮点类型数据精度就越高
单精度浮点存储过程
想要将一个十进制小数存储到这32位的小格子中也需要一定存储过程,此存储过程非彼存储过程(不是指数据库的存储过程),我们现在以十进制数8.5为例,一步一步将它“存储”到这32个格子中。
符号位存储
这一步比较简单,前面有有提到过,0表示浮点数为正数,1为负数,我们单凭肉眼就能看出,8.5为正数 所以符号位是0
指数位存储
规范化(normalized)
要获取这个浮点类型的指数位,我首先需要将它转换为二进制数,整数“除二进余 取倒序”,小数“乘二取整 取顺序”,8.5的二进制为1000.1,转换为二进制后可以正式进行规范化。将已经转换为二进制小数的1000.1的小数点往左边移动,移动至小数点左边第一位为1为止,这个过程中小数点移动的位数就是这个浮点类型的阶码,1000.1移动3位变成1.0001,1.0001是它的尾数,3就是这个浮点类型的阶码,小数点向左移动其实也是将这个数除以2^向左移动的位数,所以1000.1变成1.0001,其实是除以2的3次方的到的结果,这个过程就是规范化(normalized)。
规范化也会出现小数反向移动的情况,如果浮点类型没有整数,例如十进制数0.5的二进制形式0.1,此时需要向右边反向移动1位,即为1.0,-1就是它的阶码,移动后的尾数第一位始终为1(因为它是向第一位1的位置开始移动),IEEE-754没有必要将它存储,阶码也不会单独再分配一个的符号位,于是就有了偏移量,单精度浮点类型的偏移量为127,双精度浮点类型为1023,之前提到过指数位只能存储[-127,128],假设存储一个非常小的单精度浮点类型,规范化时小数点已经需要向右移动127位,即它的指数为-127,加上单精度浮点类型的偏移量127后,也不过是0,它依然是一个整数;所以这个偏移量存在的必要,就是为了确保它在有限的指数位取值范围中保持整数。回归主题,存储阶码为3的浮点类型8.5,它的阶码加上偏移量后(3+127)为130,转换为二进制就是1000 0010;
指数补零
指数位只能存八位,其实也会存在计算出的二进制数不足八位的情况,例如119,二进制为1110 111,这是不足八位的,但是也要存储到格子中,所以我们需要补零操作,我们需要在二进制前补零,切记整数二进制后面补零会影响数值,而小数二进制前面补零会影响数值,1110 111补零后为 0111 0111。
尾数位存储
隐藏高位
之前有提到过,规范化后的二进制小数就是它的尾数,1.0001就是尾数,所以一个浮点数主要存储在尾数位,规范化会将小数点左边第一位1移动,所以尾数前面第一位始终都是1.,这个1.是可以省略的,所以尾数又变成了0001
低位补零
尾数位占用23位,没有达到23位时,需补足23位,尾数位存储的都是小数,所以它只能在后面补零,
补完之后的尾数是 0001 0000 0000 0000 0000 000
-
Result
最后再拼接一下
符号位:8.5为整数,所以符号位为 0
指数位:1000.1左移3位变成尾数 1.0001 ,3+127=130,130二进制为 1000 0010
尾数位:尾数位1.0001,藏高补低后为 0001 0000 0000 0000 0000 000
结果:十进制数8.5的单精度浮点类型存储格式为 0 1000 0010 0001 0000 0000 0000 0000 000
32位浮点类型取值范围
32位单精度浮点类型的取值范围是[1.17549435…E-38,3.40282366…E1038] ∪ [-3.40282366…E38 , -1.17549435…E-38],虽然我们直接抛出了答案,但是我们需要思考一些为什么是这么多呢?那么首先,先了解一下IEEE-754标准的一些概念
规格数(normal number)
符号位 | 指数位 | 尾数位 |
---|---|---|
0或者1 | [0000 0001 ,1111 1110],不可全为0,也不可全为1 | [000……000,111……111],隐藏1. |
尾数会隐藏整数部分的1.,这个上面有提到过,这个叫高位隐藏 ,1. 开头的尾数在IEEE-754标准中都定义为正常数,即规格数;我们大部分情况下使用的浮点类型几乎都属于规格数,它们的指数即不会全为0,也不会全为1,指数位取值为[1,254],阶码范围为[-126,127]。
因为隐藏1.的特性,使得它无法表示0,尾数位的范围是1.0000 0000 0000 0000 0000 000 ~ 1.1111 1111 1111 1111 1111 111,这个范围可以理解为从1到2中它能够表示的所有小数(小数是无限的,怎么能表示所有小数呢?一个人虽然不够富裕,但他已经把自己的所有都给了你,这个人何尝不是最爱你的人呢?),这些数值始终都是大于0的,无论指数再怎么移动,我的值始终大于0;我其中一个的因数始终大于0,无论与什么数相乘的积都不会等于0,不过大家不要被误导了,尾数的1~2范围是指,规范化后的值,真正存储的不一定就是1~2。
ps: 这里说的三种数(规格、非规格、特殊数) 都涵盖了单双精度两种浮点类型,只是以单精度为例,解释这个概念。