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: 这里说的三种数(规格、非规格、特殊数) 都涵盖了单双精度两种浮点类型,只是以单精度为例,解释这个概念。
非规格数(subnormal number) 重点
符号位 | 指数位 | 尾数位 |
---|---|---|
0或者1 | 0000 0000 | [000……000,111……111],隐藏0. |
单精度浮点类型规范化时,有时因为浮点数的二进制没有整数部分,会使它的小数点会向右进行反向移动;指数位最多只能存储[-127,128],如果这个数值特别小,反向移动了127位,那么这浮点类型此时就已经归类到非规格数行列里了,同时反向(向左)移动127,即它的指数在偏移后为0(-127+127),0的二进制还是0,所以它的指数位上是 0000 0000,且当指数位全为0时,尾数位隐藏的也为0.,这就是非规格数吗?
据上面的说法,我们不经思考一个问题,一个数值特别小的单精度浮点类型,规范化时需要向右反向移动刚好127才能找到1,它的阶码偏移后指数为0,归类到非规格数中后,它原本尾数的1.应该舍弃掉吗?变为0.?答案是否定的,舍弃掉1.变为0.后数值会发生变化,甚至是变成了一个毫不相干的数,所以上述所说的全是错误的(上面红色字体所说非规格数描述是错误的),看到这里估计有一些朋友就会有一点点怨言了,如果此时我说我是故意的,那么大家可能会开始有一点点讨厌我了,我在第一次接触这个概念时也是晕头转向,一度怀疑是不是自己上学是没认真听,才无法理解这个概念,在翻阅过一些博文之后,才开始有了一点点自己的理解,我并不是不甘自己的成果直接公布给大家,我是希望大家都能养成刨根问底的学习习惯,不要拿着别人的知识就当自己的,一定要多消化,疑惑时也要敢于提问,敢于质疑,个人理解,-127这个阶码的存在在IEEE-754标准中是不合理的,它无法归类到规格数或非规格数中,在IEEE-754标准现有的规则中,非规格数是“指数全0,尾数藏0.”,但是-127这个阶码打破了这个规律,它使得在规格数中也出现了尾数位全为0的浮点数,尾数高位隐藏后我们无法区分此浮点数究竟是规格数还是非规格数,它的存在使得规格数和非规格数之间没有一个阻隔性的规则,所以要想理解非规格数和规格数的概念,则要把它忽略掉。
除去-127后,最小的指数变为-126,这一点我们在java的Float类的常量中也可以看到。
其实在IEEE-754标准中,有着属于非规格数自己的计算阶码规则,阶码=1-偏移量=-126;同时-126这个阶码也可以存在于规格数,这类规格数的指数位为1,即 0000 0001,这就可以通过指数区分此数为规格数还是非规格数。 藏0.与藏1.从来都不可以区分一个浮点数是属于规格非规格,我们只需要记住当指数位全0时,它的尾数位隐藏的是0.。
逐渐溢出(gradual underflow)
通过上面的一些非规格数的解释后,我们可以了解到非规格数的尾数位的取值范围为0.000… 0.后面共23个0 ~0.111… 0.后面共23个1,这个范围的最小值和最大值转换为十进制后已经非常接近0和1的值了,取值范围暂且记作 [0,1],而[规格数的尾数取值范围是 1.000… 1.后面共23个0 ~1.111… 1.后面共23个1,这个范围装换为二进制后同样也是接近于 [1,2] ,所以 非规格数 的最大值与 规格数 的最小值做一个衔接。
单精度浮点类型单靠我们肉眼去看它的范围属实有点累,所以我们拟一个16位的浮点类型(半精度浮点类型,此半精度非彼半精度),来解释一下逐渐溢出这个概念。先说明一下,该浮点类型并不存在。
那么该类型的[规格数](###规格数(normal number))忽略正负最小的值应该是 * 0001 0000 0000 000 ,计算成十进制就是±0.015625,而非规格数忽略正负最大值是 * 0000 1111 1111 111 计算成十进制为±0.01561737060546875,此值无疑是最近0.015625的值了,两个值仅仅相差0.00000762939453125,在这个范围里面已经找不到比这个差距还小的其它值,逐步溢出跟精度有关,精度越高它们的值就相差越小,与之衔接得越完美。回到正题,32位单精度浮点类型的最小规格数是±1.1754943E-38(保留7位小数),最小非规格数是±1.1754942E-38(保留 7位小数),可以看出它们之间的差距已经非常小了,如果是64位双精度浮点类型,那么差距会更小,而非规格数从最小的值一点一点递增最大值,最后“完美”衔接规格数的最小值,此过程叫做逐步溢出
特殊数(non number)概念
符号位 | 指数位 | 尾数位 |
---|---|---|
0或者1 | 1111 1111 | 000……000 & …不全为0… |
众所周知,在数学中,任何数除以0都等于0,这似乎是没有什么意义的。生活中其实也存在这样的情况,有些事对我们来说没有什么意义,或者说它会带来坏处,我们经常要去避免它或处理它,但是这些事物的存在真的属于不正常吗?个人觉得属于正常,凡事都有利弊,我们在得意时有多么的辉煌,那么在失意时就会有多么的挫败,这是成正比的,每个人的承受能力有限,多数人在得意时往往会变得顾盼自雄,这并不会带来什么直接的损害,但是在失意时却变得自暴自弃,然后失败就被无限放大,形成了失败大于成功不能正比的"假象"。个人崇拜的一些比较成熟的人,他们往往“胜不骄,败不馁”,懂得权衡利弊,“两害相权,取其轻”;”两利相权,取其重“,所以在下给诸位大佬一些小建议:在遇到一些问题时,不要第一时间抱怨(抱怨就想一把挥舞的匕首,靠近你的人都会受到伤害;鼓励就想一个温暖的怀抱,身边的人都能感受你的温暖,同时也会传递跟其他人,一起发光发热!),抱怨不会带来任何收益,它会放大你的情绪,还会使队伍中的其他成员收到牵连从而失去方向,应该先解决问题,权衡利弊,事后再总结经验,才能做到高瞻远瞩。学会接受一些东西,才能汲取到更多的经验,逐渐充实自己,让自己变得更加强大。
java程序在处理除0这种运算一般会抛出异常(ArithmeticException: / by zero),但是在浮点类型中IEEE-754却定义了三个固定的值来表述这些情况,它们就是正无穷(Infinity)、负无穷(-Infinity)和非数(Not a Number),。
±Infinity
IEEE-754标准中存在着“无穷”的概念,个人认为,它并不是觉得除以0是一个错误,而是觉得无法计算出它的真实大小,所以被纳入了无穷的范围。我们知道单精度浮点类型的规格数的最大真实指数是127,也就是说最多能向左移动127位,超过这个范围便是Infinity(正无穷),双精度浮点类型的规格数的最大真实指数可不是127,而是1023,所以这个无穷并不是固定的,而是根据它的指数位来决定的的,会出现在单精度中属于Infinity,在双精度中却不是无穷的情况。
public class Main {
public static void main(String[] args) {
double dIft=3.40282366920938463463374607431768211456E39;
System.out.println("2的128次方在双精度浮点类型中存储打印:"+dIft);
float fIft=(float) dIft;
System.out.println("2的128次方在单精度浮点类型中存储打印:"+fIft);
}
}
输出结果
根据指数范围来定义无穷,我们可能会顺理成章的以为单精度浮点类型真实指数低于-127后,浮点数就会被纳入到负无穷的范围,非也。例如:2的-128次方(2.9387358……E-39),这个值其实已经超出了单精度浮点类型的指数最小值(-126)了,但是它依然可以存储(目前我还不知道为什么指数超过了-126却还能存储且不为无穷),不会被纳入无穷中。
public static void main(String[] args) {
/*2^(-128)*/
float fIft=2.9387358770557187699218413430556141945E-39f;
System.out.printf("%.100f\n",fIft);
//打印结果为:0.0000000000000000000000000000000000000029387358770557188000000000000000000000000000000000000000000000
}
输出结果
无穷分为正无穷(Infinity)和负无穷(-Infinity),他们分别代表了一个精度的最大值和最小值,可以理解为它们是两个范围,单精度浮点类型的 -Infinity 是它的最小值,2的-128次方这个值并不没有达到负无穷的范围,即便它非常小了,但是它依然是正数,单精度的 规格数 最小值应该是-2的127次方,比这还小的数是-2的128次方(2.9387358……E-39),所以-2的128次方才是负无穷(-Infinity)。
无论数值是多少,只要被纳入到无穷的范围中,那它存储时的指数位与尾数位固定位全1和全0。
NaN
NaN表示“不是一个数”,0除以0的时候会得到NaN,给负数开根号时会得到NaN,它是没有正负之分的,它存储形式为:符号可正可负,指数位全为1,尾数位不全为0即可。
±Infinity与NaN一些运算
-
无穷与任何非特殊数做运算后得到的结果都是无穷
public static void main(String[] args) { float newInf1 = Float.NEGATIVE_INFINITY + 2; System.out.println("负无穷加2后打印:" + newInf1); float newInf2 = Float.NEGATIVE_INFINITY - 2; System.out.println("负无穷减2后打印:" + newInf2); float newInf3 = Float.NEGATIVE_INFINITY * 2; System.out.println("负无穷乘2后打印:" + newInf3); float newInf4 = Float.NEGATIVE_INFINITY / 2; System.out.println("负无穷除2后打印:" + newInf4); System.out.println("========================"); float newInf5 = Float.POSITIVE_INFINITY + 2; System.out.println("正无穷加2后打印:" + newInf5); float newInf6 = Float.POSITIVE_INFINITY - 2; System.out.println("正无穷减2后打印:" + newInf6); float newInf7 = Float.POSITIVE_INFINITY * 2; System.out.println("正无穷乘2后打印:" + newInf7); float newInf8 = Float.POSITIVE_INFINITY / 2; System.out.println("正无穷除2后打印:" + newInf8); }
输出结果
-
正无穷加负无穷等于NaN
public static void main(String[] args) { float notANum=Float.POSITIVE_INFINITY+Float.NEGATIVE_INFINITY; System.out.println(notANum); }
输出结果
-
NaN不等于NaN
public static void main(String[] args) { float notANum = Float.NaN; System.out.println("变量notANum"+(notANum == Float.NaN ? "等于" : "不等于")+"NaN"); }
输出结果
-
NaN做任何运算后得到结果都是NaN
public static void main(String[] args) { float f1 = Float.NaN + 2; System.out.println("NaN加2后打印:" + f1); float f2 = Float.NaN - 2; System.out.println("NaN减2后打印:" + f2); float f3 = Float.NaN * 2; System.out.println("NaN乘2后打印:" + f3); float f4 = Float.NaN / 2; System.out.println("NaN除2后打印:" + f4); float f5 = Float.NaN + Float.NEGATIVE_INFINITY; System.out.println("NaN加负无穷后打印:" + f5); float f6 = Float.NaN - Float.NEGATIVE_INFINITY; System.out.println("NaN减负无穷后打印:" + f6); float f7 = Float.NaN + Float.POSITIVE_INFINITY; System.out.println("NaN加正无穷后打印:" + f7); float f8 = Float.NaN - Float.POSITIVE_INFINITY; System.out.println("NaN减正无穷后打印:" + f8); }
输出结果
-
用Float的compare方法比较两个NaN,结果为相等
public static void main(String[] args) { /*输出的值等于0,表示f1在数值上等于f2 ; 输出的值小于0,表示f1在数值上小于f2 ; 输出的值大于0,表示f1在数值上大于f2 */ System.out.println(Float.compare(Float.NaN, Float.NaN)); }
输出结果
32位浮点类型有效数字
单精度浮点类型有效数字为7位,是指数字存储过大导致尾数位存储溢出而失去精度,成功保留下来的真实数字。例如:存储16777217(2^24+1)时,程序中实际存储的是其实是16777216,而 16777217 与 16777216 只有最后1位数字是不相同的,其余的7位数字都是相同的,这代表单精度浮点类型的有效数字为7位。
精度丢失
存储16777216(2^24)时,尾数位已经达到了24位,超出1位了,而超出的那1位将会被省略掉,但是指数位上却依然记住它的是24位,因此在存储时会将省略掉的尾数当做零处理;即便你存储的是11111111,它也会全部把它当做0处理,即00000000,所以此时就有可能出现存储的数据与实际浮点是的值有差异,但是因为被省略掉的是0的原因,所以 16777216 并没有受到影响。
但是,如果我们存储 16777217 ,此时被省略掉的那一位是1,所以它的值已经发生变化了。
我们用格式化输出方法打印一下,发现它打印出来的值竟然是16777216,最后1位数字的精度已经丢失。
这个原因很简单,不管是16777216还是16777217,它们都被省略掉了最后一位;不管最后一位是1还是0,程序都把它当做0来处理,所以它们在程序中是没有差别的。因此我们得出一个结论,那就是不管是整数还是小数,存储到单精度浮点类型中,只要规范化后的尾数位超过24位,就有可能出现精度丢失的问题。
浮点数间隔
既然我们已经初步了解了精度丢失问题,不妨我们想一下,“这个精度丢失是不是可控的?它会以一个什么样趋势开始丢失精度?这其中有没有规律?”,要解答着这些问题,我们需要了解一些概念,那就是浮点数间隔。浮点数在计算机程序中是有间隔的,这个间隔是指在小数中的某个范围内的两个相邻的小数之间是存在的间隔,我们在计算机中只能存储这间隔之外的小数,无法存储间隔内的小数,例如:0.1、0.3、0.5,它们之间的间隔是0.2,我可以存储0.1,也可以存储0.3和0.5,但不能存储0.2和0.4,因为0.2和0.4与其它三个小数之间的间隔不是0.2。为什么要这样做?,因为小数是无限的,我们可以知道1 ~ 1000000之间有多少整数,但我们永远无法计算出0 ~ 1之间有多少小数,小数是无限的,小数可以无限往“下沉”,0.1下面有0.01,0.01下面又有0.001,没有一个固定的范围,而我们的精力却是有限的,我们永远无法知道它里面究竟包还含多少小数,不光我们人的精力是有限的,计算容量的内存容量和硬盘容量也是有限的,先不说记录0 ~ 1之间的所有小数,它们之间单纯的一个小数0.000…中间10w个零…001计算机就很难记录,如果说你的计算机可以轻松应付,那就再翻倍变成一百万个零呢,继续翻倍,又变成一千万、一亿、十亿……;这样无限的“下沉”,永远都不会“沉入底”,所以再强大的计算机终究还是无法应付这种无限的数据。
在数学中,整数之间都是固定的,小数却是连续的,之有提到过,它可以无限“下沉”。1 ~ 1000000之间不过也只有1000000种不同的数据形态而已,而小数之间这种连续的关系,就拿0 ~ 1之间的小数来说,它们之间有多少小数,任何计算机都无法完全的计算,以后也不会有。一个数要统计它里面可以包含多少位小数,就是把这个数无限循环的除以10,我们可以知道1里面有10个0.1,有100个0.01,这似乎非常容易,但是以这个规律无限“下沉”,“下沉”到0.00000000001 、0.000…中间10000个零…001、0.000…中间100w个零…001……,我们始终无法应付,计算机也会因内存不足而终止,因此这种无限制的东西我们去计算它显然没有太大的必要,而计算机在处理这种问题时,也不得不将它们变为一个个固定的值,“既然你是无限的,那我就给你计算出个大概”。这个“大概”是根据浮点精度来计算的,精度越高计算得越仔细,而在两个没有丢失精度的浮点数之间的差值就是浮点数间隔,上面所提到的 16777216 和 16777217 它们之间的间隔是1,而 16777216~33554430所有的浮点数之间的间隔是2(可以理解为16777216~33554430之间两个相邻的浮点数之间所有间隔在2以下的浮点数全部丢弃掉,只有间隔达到2时计算机才会给它设有一个固定的值,如16777216、16777218、16777220……)。所以16777217 不会正常存储,而 16777218 却可以正常存储,因为16777218 比16777216大2,正好符合间隔为2的规律,16777217因为精度后面被省略掉的那个1(规范化后的二进制第24位是1),所以在单精度浮点类型中并不会设有一个固定的值,而在16777218中被省略掉的那个是0(规范化后的二进制第23位是1,24位是0);16777216是2的24次方,二进制是1000000000000000000000000,16777218比它多2,二进制是1000000000000000000000010,多出来的2会 “逢二进一”,进到第23位,所以没有被省略,16777217比16777216多1,二进制是1000000000000000000000001,多出来的1不会”逢二进一“,依然存储到24位,因此被省略。
间隔也不一定都是固定的,它会根据指数位的值来变化的,同一指数下存储的所有浮点数中两个相邻的浮点数它们之间的间隔都是一样的,不同指数它们直接的间隔是不一样的,单精度浮点类型的所有的间隔都是在被隐藏的尾数位尾数位的24位(第24个格子)“逢二进一”后,“进”到第23位(第23个格子)时所产生的值变化后的差异,例如16777217,如果二进制(规范化后)最后一位没有“逢二进一”(没有进到第23位的都会被当做0处理),那么它的值将会被存储为16777216,想要“逢二进一”就得是2,没达到2的全部为0,要么0要么2,“这个2就是他们的间隔?不是!”,这个2直接还不能之间拿来当间隔,因为 指数位还记录这个尾数规范化时有没有被位移,举个例子:这个2就好比是一个商品的原价,指数位记录着这个商品有没有打折,右移位数加上尾数位数就是它打折幅度,如果是2价格就是2.5(2的-2次方),同时它也可能价格上涨,左移位数减去尾数位数就是它价格上涨的倍数,如果是2价格就是4(2的2次方),记住下面这个公式
假设我们的计算机的能力有限,在计算0 ~ 1之间的小数时,认为它除了零以外只有8种不同数据形态,就像下图这个表盘一样,表盘的指针只能指向 ● ,无法停留在两个 ● 之间。如果我们要存储0.127,它的精度也会丢失,数值就会遵循就近原则,指向离他最近差值最小的那个数,也就是0.125,我们如果想要改变这一点就需要在表盘中加入更多的 ●
前面所提到的 16777217,就是在 16777216~33554430 这个表盘中属于 16777216 和 16777218 这两个 ● 之间的位置,所以无法被存储,出现了精度丢失。
把单精度浮点类型比作表盘的话,那么 指数位 的可存储范围就是表盘的数量,所以规格数一共有254个表盘,尾数位的大小就是每个 表盘的 ● 的数量,每个表盘有2^23个表盘;我们以规格数为例,说明一下表盘的间隔,不考虑正负,且认为的符号位全为0,最小的规格数是 0 0000 0001 0000 0000 0000 0000 0000 000 ,这就是一个 ● , 0000 0001 代表一个表盘,它的尾数从0000 0000 0000 0000 0000 000 到 1111 1111 1111 1111 1111 111的所有的 ● 都属于这个表盘,最小规格数的下一个相邻的 ● 就是0 0000 0001 0000 0000 0000 0000 0000 001 ,它们之间的间隔就是尾数位的差值,我们肉眼就能看出差值是0000 0000 0000 0000 0000 001,“那不就跟16777216 ~33554430 所有的浮点数之间的间隔是一样的吗?也是最后因为二进制进1了才被保留下的“,”错!不一样”,指数位不一样,最小规格数的指数位是 0000 0001(-126),16777216 和 16777218 的指数是 0001 1000(24);使用公式计算过后,前者的间隔位≈1.4012984……E-45(2^(-126-23)),后者的间隔是2(2^(24-23)),所以它会根据指数而发生变化。规格数所有的间隔都是尾数位的最后一位进1时被保留下来计算后所产生的差值,单精度浮点类型的差值(间隔)有254种。
下面是我总结的一部分规格数的浮点数间隔
指数 | 真实指数 | 规格数范围 | 间隔 |
---|---|---|---|
1 | -126 | 1.1754943……E-38~2.3509884……E-38 | ≈1.4012984……E-45 |
27 | -100 | 7.8886090……E31~1.5777216……E-30 | ≈9.4039548……E-38 |
77 | -50 | 8.8817841……E16~1.7763566……E-15 | ≈1.0587911……E22 |
126 | -1 | 0.5~0.9999999…… | ≈5.9604644……E-8 |
127 | 0 | 1~1.9999998…… | ≈1.1920928……E7 |
137 | 10 | 1024~2047.9998779…… | 0.0001220703125 |
147 | 20 | 1048576~2097151 | 0.125 |
148 | 21 | 2097152~4194303 | 0.25 |
149 | 22 | 4194304~8388607 | 0.5 |
150 | 23 | 8388608~16777215 | 1 |
151 | 24 | 16777216~33554430 | 2 |
155 | 28 | 268435456~536870911 | 32 |
160 | 33 | 8589934592~17179869183 | 1024 |
170 | 43 | 8796093022208~17592186044415 | 1048576 |
177 | 50 | 1.1258999…… E16~22517996……E16 | 134217728 |
227 | 100 | 1.2676506……E31~2.5353012……E31 | 1.5111572……E24 |
254 | 127 | 1.7014118…… E38~3.4028232…… E38 | 2.0282409603651670423947251286016 |
有效数字7位理解
这个概念有点复杂,我们只知道有效数字是7位,但我们却弄不明白为什么是7位,上文说了那么多,始终还是无法有力的解释,我个人认为它是一种概念,是一种理解,就像陆地上跑的交通工具大部分都是四个轮子,天上飞的动物大部分都是两个翅膀,我们可以说这种设定不是必要,像六个轮子的货车,四个翅膀的蜻蜓,它们也可以正常的在陆地行驶或天空飞翔;但是存在即合理,我们没有必要去“淘汰”掉其中的一种,应该去了解它的,物尽其用。所以我“委曲求全”的稍微解释一下吧,就说说我对它的理解。
我们先仔细想一下,有效数字7位是什么?有效数字7位是指在存储浮点数时出现精度丢失后保留下来的7位十进制的准确数字,这个七位包含整数和小数,从左边第一位非零的数字开始,一直到第七位数字就是有效数字,单精度浮点类型数据其实主要存储在尾数位,尾数位一共有24位(23位加上隐藏的1.或者0.),也就是2的24次方(16777216),16777216并不能包含十进制8位所有的值,但是可以覆盖十进制7位所有的值。
我们再结合一下网上的解释,下图是的解释是“10^7 < 16777216 < 10^8”,这个解释很合理,但是别被搞混了,10的7次方并不是7位的十进制,但是它可以覆盖所有的7位十进制数。
尾声
至此,IEEE-754的所有内容都已经讲完了。如果有不懂的地方,欢迎留言评论,可以大家一起讨论,也建议大家关注后续内容。创作不易,画图更是麻烦,厚着脸皮跟各位要一些点赞,虽然不能带来什么实际收益,但也是大家的一种肯定,谢谢各位,祝大家生活愉快!再见。