由IEEE 754深入理解浮点数

2018-07-05  本文已影响0人  isjinhao

  本文又臭又长,实用价值不大。只是小码农最近脑子抽风突然想搞明白为什么0.1+0.2=0.30000000000000004才查阅资料写的。嫌浪费时间者可以不看...
  在说这个问题之前,我们先看两个常识。第一个是n位二进制能最大表示的无符号整数?这个大家应该都知道是20 + 21 + … + 2n-1,即2n – 1。第二个是二进制的科学计数法(这是为了之后好分析,小码农自创的一种说法)。小学我们就知道十进制中有一个科学计数法,即如825.25会被表示成8.2525 * 102。那么在二进制的世界中也可以有一个科学计数法。如8.25在二进制被表示为1010.01,写成科学计数法就可以表示成1.01001 * 23。120.5在二进制中为1110110.1,写成科学计数法就是1.1101101 * 26。所以是不是发现在二进制的科学计数法中浮点数可以被表示成如1.xxx * 2t的形式呢。而事实上也如此,计算机内部表示一个浮点数会将其分成三个部分:符号位(0为正、1位负。记为S)、尾数部分(1.xxx。记xxx部分为M)和指数部分(2t。记t为E)。所以二进制的科学计数法就是: (-1)S * (1.M) * 2E
  明白这两个概念之后我们就可以来解释Java浮点数的表示范围了。首先Java使用的浮点数运算规则是IEEE 754。那么什么是IEEE 754呢?简单的说就是一个美国定义的被广泛采用的浮点数运算标准。再按照上段的格式可得,在32位的float中分配规则为:1位符号位(S)、8位指数位(E)、23位尾数位(M)。在64位的double中为:1位符号位(S)、 11位指数位(E)、52位尾数位(M)。
  我们刚才说到的二进制的科学计数法为(-1)S * (1.M) * 2E。这个其实是不准确的,为什么不准确呢?请继续看下去。
  大家都知道在一串连续的二进制位中想正数和负数都表示出来一般采用的方法是在最高位加一个符号位。但是这样在运算的时候就需要采用补码进行运算。比如在4bit的情况下-5的二进制表示为1101,+4的二进制表示为0100。直接使用原码进行相加后得到的值为10001,竟然直接溢出了!换算成补码后-5的补码为1011,+4的补码为0100,运算后再转换为原码可得1001,恰是-1。即符号位会造成限制的。
  所以由以上所有解释可得:n位二进制位能表示的范围为:[-2n-1 , 2n-1-1],也就是说在E中还有一位符号位。但IEEE 754并没有采用这种存储方式,而是采用了无符号数值减去一个偏正值的形式来存储整数。即若指数部分存储的为00000111,实际上表示的是:00000111 – 偏正值。偏正值为2n-1-1。这种方式被称为指数偏差。为什么呢?我们不妨考虑一个情况,在需要对指数部分做比较时,-5和-4做比较,符号位相同,其他位从高至低谁先为0谁较大,但+5和+4作比较,符号位相同,其他位从高至低谁先为1谁较大。这两种比较方式恰是相反的,在硬件上更难实现。既然这样,那为什么还需要存在符号位呢,不如都使用这种指数偏差的形式?小码农认为这是由于IEEE 754仅规定了四种精度的浮点数,偏正值都是相对固定的。而对于任意n位二进制位的数值来说,采用指数偏差的形式存储每次都需要计算偏正值,时间效率上是难以接受的。
  这个时候我们就会发现若指数位为8位,则采用了指数偏移形式来存储的指数部分实际表达的数值为[-127, 128]。我靠,这竟然违反客观事实了!哈哈哈,其实不是这样子的,IEEE 954规定指数部分在区间[1, 2n-2]中才时减去偏正值。那么0和2n-1该怎么处理呢?这个我们等下再分析。现在更新二进制形式的科学计数法为(-1)S * (1.M) * 2E-偏正值
  解释完指数位之后,尾数位就很好理解了,就是n位二进制位表示的无符号整数,区间为[0, 2n-1]。现在我们就可以求出浮点数的范围了。

  其实说到这里我们一直在解释的都是规格形式的浮点数。也就是E的取值区间为 [1, 2n-2]且尾数的整数部分为1的情况。那么,对于超出这个限定的情况会如何处理呢?如果大家对学过高级程序设计语言会知道,一般某个数值类中会有无穷大、NAN(Not A Number)这种特殊的情况,如Java中的Double.NAN,Double.NEGATIVE_INFINITY、Double.POSITIVE_INFINITY。而其实超出这个限定的部分是被用来处理特殊情况的。
  有了规格化浮点数,肯定还有非规格化浮点数。但注意:非规格化值域和规格化值域不是互补的,除了这两种还有特殊值!定义非规格式浮点数的E为0,尾数非零且尾数的整数部分为0。这样的话其实它就不符合科学计数法的形式了,但我们仍然按科学计数法的形式来表达。在IEEE 954标准中规定除了规格化情况下之外的其他所有情况偏移后的值为规格情况下偏移后再加1。
  呵呵,看到这里有人不禁的发出一声冷笑。IEEE 754是有病啊,弄的这么恶心。其实不是这样的,这样做是为了解决“突然式下溢出”而诞生的(维基百科说是INTEL力荐的)。咱们先来看看不采用IEEE 754标准所产生的一种恶果。假如不采用 IEEE 754,E最小可以取0,以正单精度浮点数举例。最小规格浮点数为2-127,次小规格浮点数为2-127 * (1 + 2-23)。它俩之间的差值为2-127 * 2-23(此时是最小差值)。而最小规格浮点数与0之间的差值为2-127。就会出现一种情况,两个相近的不相等的规格浮点数做差值时可能得到的数值在0和最小规格浮点数之间,这是不能被真实表示出来的情况,就会出现突然下降至0的情况。但如果采用了IEEE 754,最小和次小规格浮点数之间的差值为2-126 * 2-23,但在2-126至0之间还有能被实际表示出来的浮点数。我们不妨来求一下最小的非规格浮点数:2-126 * 2-23。恰好等于最小相邻规格浮点数之差。所以这种神奇的规定是为了解决“突然式下溢出”的情况。
  这时候,大家肯定有一个疑问,无论采不采用IEEE 754,能表示的范围都是一定的,为什么会有这种区分呢?实际上就像我们推导出来的float和double范围,超出这个范围的都被认为是非法的情况。在被规定为合法的精度范围内做运算时不会发生错误。
  除了规格化浮点数和非规格化浮点数,IEEE 954还规定了几种特殊值。不过我们先看看这两种非特殊值能表示的最大最小值(仅举正数)。从下图这张表中也可以看出非规格化浮点数的绝对值小于所有的规格化浮点数的绝对值。

规格化 非规格化
最大 E=254,M=223-1,S=0
(2-2-23) * 2127
E=0,M=223-1,S=0
((1-2-23) * 2-126
最小 E=-126,M=0,S=0
(2-126
E=0,M=1,S=0
(2-23 * 2-126

  说完了主要的部分,我们就可以来看看最后这些特殊情况了。(单精度为例)

类型 E M S
正无穷 255 0 0 +∞
负无穷 255 0 1 -∞
NAN 255 非零 * NAN

  讲到这里,理论部分已经全部结束了。下面用Java来证明一下。(注:Java中是双精度的,数据不一致,但道理一致)

测试
上一篇下一篇

猜你喜欢

热点阅读