浮点数的表⽰及范围IEEE754
(<-reference link)
浮点数
1.  什么是浮点数
在计算机系统的发展过程中,曾经提出过多种⽅法表达实数。典型的⽐如相对于浮点数的定点数(Fixed Point Number)。在这种表达⽅式中,⼩数点固定的位于实数所有数字中间的某个位置。货币的表达就可以使⽤这种⽅式,⽐如 99.00 或者 00.99 可以⽤于表达具有四位精度(Precision),⼩数点后有两位的货币值。由于⼩数点位置固定,所以可以直接⽤四位数值来表达相应的数值。SQL 中
的 NUMBER 数据类型就是利⽤定点数来定义的。还有⼀种提议的表达⽅式为有理数表达⽅式,即⽤两个整数的⽐值来表达实数。
定点数表达法的缺点在于其形式过于僵硬,固定的⼩数点位置决定了固定位数的整数部分和⼩数部分,不利于同时表达特别⼤的数或者特别⼩的数。最终,绝⼤多数现代的计算机系统采纳了所谓的浮点数表达⽅式。这种表达⽅式利⽤科学计数法来表达实数,即⽤⼀个尾数(Mantissa,尾数有时也称为有效数字——Significand;尾数实际上是有效数字的⾮正式说法),⼀个基数(Base),⼀个指数(Exponent)
以及⼀个表⽰正负的符号来表达实数。⽐如 123.45 ⽤⼗进制科学计数法可以表达为 1.2345 × 102 ,其中 1.2345 为尾数,10 为基数,2 为指数。浮点数利⽤指数达到了浮动⼩数点的效果,从⽽可以灵活地表达更⼤范围的实数。
2.  IEEE 浮点数
计算机中是⽤有限的连续字节保存浮点数的。在 IEEE 标准中,浮点数是将特定长度的连续字节的所有⼆进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别⽤于表⽰给定⼆进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。
IEEE 754 指定:
n  两种基本的浮点格式:单精度和双精度。
Ø  IEEE 单精度格式具有 24 位有效数字精度,并总共占⽤ 32 位。
Ø  IEEE 双精度格式具有 53 位有效数字精度,并总共占⽤ 64 位。
n    两种扩展浮点格式:单精度扩展和双精度扩展。此标准并未规定这些格式的精确精度和和⼤⼩,但它指定了最⼩精度和⼤⼩。例
如,IEEE 双精度扩展格式必须⾄少具有 64 位有效数字精度,并总共占⽤⾄少 79 位。
具体的格式参见下⾯的图例:
3.  浮点格式
浮点格式是⼀种数据结构,⽤于指定包含浮点数的字段、这些字段的布局及其算术解释。浮点存储格式指定如何将浮点格式存储在内存中。IEEE 标准定义了这些格式,但具体选择哪种存储格式由实现⼯具决定。
汇编语⾔软件有时取决于所使⽤的存储格式,但更⾼级别的语⾔通常仅处理浮点数据类型的语⾔概念。这些类型在不同的⾼级语⾔中具有不同的名称,并且与表中所⽰的IEEE 格式相对应。
IEEE 精度C、C++Fortran (仅限 SPARC)
单精度float REAL 或 REAL*4
双精度double DOUBLE PRECISION 或 REAL*8
双精度扩展long double REAL*16
IEEE 754 明确规定了单精度浮点格式和双精度浮点格式,并为这两种基本格式分别定义了⼀组扩展格式。表中显⽰的long
double 和 REAL*16 类型适⽤于 IEEE 标准定义的⼀种双精度扩展格式。
3.1.单精度格式
IEEE 单精度格式由三个字段组成:23 位⼩数 f ; 8 位偏置指数 e ;以及 1 位符号 s。这些字段连续存储在⼀个 32 位字中(如下图所⽰)。
单精度
Ø  0:22 位包含 23 位⼩数 f,其中第 0 位是⼩数的最低有效位,第 22 位是最⾼有效位。
IEEE 标准要求浮点数必须是规范的。这意味着尾数的⼩数点左侧必须为 1,因此我们在保存尾数的时候,可以省略⼩数点前⾯这个 1,从⽽腾出⼀个⼆进制位来保存更多的尾数。这样我们实际上⽤ 23 位长的尾数域表达了 24 位的尾数。
Ø  23:30 位包含 8 位偏置指数 e,第 23 位是偏置指数的最低有效位,第 30 位是最⾼有效位。
阶码⽤移码表⽰
8 位的指数为可以表达 0 到 255 之间的 256 个指数值。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上⼀个偏差(Bias)值作为保存在指数域中的值,单精度数的偏差值为 127;偏差的引⼊使得对于单精度数,实际可以表达的指数值的范围就变成 -127(0 -  偏差值127) 到 128 (255 -  偏差值127)之间(包含两端)。在本⽂中,最⼩指数和最⼤指数分别⽤ e min 和 e max 来表达。稍后将介绍实际的指数值 -127(保存为全0)以及 +128(保存为全 1)保留⽤作特殊值的处理。
Ø  最⾼的第 31 位包含符号位s。s为0表⽰数值为正数,⽽s为1则表⽰负数。
3.2.双精度格式
IEEE 双精度格式由三个字段组成:52 位⼩数 f ; 11 位偏置指数 e ;以及 1 位符号s。这些字段连续存储在两个 32 位字中(如下图所⽰)。在 SPARC 体系结构中,较⾼地址的 32 位字包含⼩数的 32 位最低有效位,⽽在 x86体系结构中,则较低地址的 32-位字包含⼩数的 32 位最低有效位。
双精度
如果⽤ f[31:0] 表⽰⼩数的 32 位最低有效位,则在这 32 位最低有效位中,第 0 位是整个⼩数的最低有效位,⽽第 31 位则是最⾼有效位。在另⼀个 32 位字中, 0:19 位包含 20 位⼩数的最⾼有效位 f[51:32],其中第 0 位是这20 位最⾼有效位中的最低有效位,⽽
第 19 位是整个⼩数的最⾼有效位; 20:30 位包含11 位偏置指数 e,其中第 20 位是偏置指数的最低有效位,⽽第 30 位是最⾼有效位;最⾼的第 31 位包含符号位 s。
上图将这两个连续的 32 位字按⼀个 64 位字那样进⾏了编号,其中
Ø  0:51 位包含 52 位⼩数 f,其中第 0 位是⼩数的最低有效位,第 51 位是最⾼有效位。
IEEE 标准要求浮点数必须是规范的。这意味着尾数的⼩数点左侧必须为 1,因此我们在保存尾数的时候,可以省略⼩数点前⾯这个 1,从⽽腾出⼀个⼆进制位来保存更多的尾数。这样我们实际上⽤ 52 位长的尾数域表达了 53 位的尾数。
Ø  52:62 位包含 11 位偏置指数 e,第 52 位是偏置指数的最低有效位,第 62 位是最⾼有效位。
11 位的指数为可以表达 0 到 2047 之间的2048个指数值。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上⼀个偏差(Bias)值作为保存在指数域中的值,单精度数的偏差值为1023;偏差的引⼊使得对于单精度数,实际可以表达的指数值的范围就变成 -1023到1024之间(包含两端)。在本⽂中,最⼩指数和最⼤指数分别⽤ e min 和 e max 来表达。稍后将介绍实际的指数值-1023(保存为全0)以及 +1024(保存为全 1)保留⽤作特殊值的处理。
Ø  最⾼的第 63 位包含符号位s。s为0表⽰数值为正数,⽽s为1则表⽰负数。
3.3.双精度扩展格式 (SPARC)
SPARC 浮点环境的四倍精度格式符合双精度扩展格式的 IEEE 定义。四倍精度格式占⽤ 32 位字并包含以下三个字段:112 位⼩数 f、
15 位偏置指数 e 和 1 位符号 s。这三个字段连续存储,如图2-3 所⽰。
地址最⾼的 32 位字包含⼩数的 32 位最低有效位,⽤ f[31:0] 表⽰。紧邻的两个 32 位字分别包含 f[63:32]和 f[95:64]。下⾯
的 0:15 位包含⼩数的 16 位最⾼有效位 f[111:96],其中第 0 位是这 16 位的最低有效位,⽽第 15 位是整个⼩数的最⾼有效位。
16:30 位包含 15 位偏置指数 e,其中第 16 位是该偏置指数的最低有效位,⽽第 30 位是最⾼有效位;第 31 位包含符号位 s。
下图将这四个连续的 32 位字按⼀个 128 位字那样进⾏了编号,其中 0:111 位存储⼩数 f ; 112:126 位存储15 位偏置指数 e ;⽽
第 127 位存储符号位 s。
3.4.双精度扩展格式 (x86)
该浮点环境双精度扩展格式符合双精度扩展格式的 IEEE 定义。它包含四个字段:63 位⼩数 f、1 位显式前导有效数位 j、15 位偏置指
数 e 以及 1 位符号 s。
在 x86 体系结构系列中,这些字段连续存储在⼗个相连地址的 8 位字节中。由于 UNIXSystem V Application Binary Interface Intel 386 Processor Supplement (Intel ABI) 要求双精度扩展参数,从⽽占⽤堆栈中三个相连地址的 32 位字,其中地址最⾼字的 16 位最⾼有效位未⽤,如下图所⽰。
地址最低的 32 位字包含⼩数的 32 位最低有效位 f[31:0],其中第 0 位是整个⼩数的最低有效位,⽽第 31 位则是 32 位最低有效位的最⾼有效位。地址居中的 32 位字中,0:30 位包含⼩数的 31 位最⾼有效位 f[62:32](其中第 0 位是这 31 位最⾼有效位的最低有效位,⽽第 30 位是整个⼩数的最⾼有效位);地址居中 32 位字的第 31 位包含显式前导有效数位 j。
地址最⾼的 32 位字中,0:14 位包含 15 位偏置指数 e,其中第 0 位是该偏置指数的最低有效位,⽽第 14 位是最⾼有效位;第 15 位包含符号位 s。虽然地址最⾼的 32 位字的最⾼ 16 位未被 x86 体系结构系列使⽤,但如上所述,它们对于符合 Intel ABI 规定是⾄关重要的。
4.  将实数转换成浮点数
4.1  浮点数的规范化
同样的数值可以有多种浮点数表达⽅式,⽐如上⾯例⼦中的 123.45 可以表达为 12.345 × 101,0.12345 × 103 或者 1.2345 ×102。因为这种多样性,有必要对其加以规范化以达到统⼀表达的⽬标。规范的(Normalized)浮点数表达⽅式具有如下形式:
±d × βe, (0 ≤ d i< β)
其中 d 即尾数,β 为基数,e 为指数。尾数中数字的个数称为精度,在本⽂中⽤ p 来表⽰。每个数字 d 介于 0 和基数之间,包
括 0。⼩数点左侧的数字不为 0。
基于规范表达的浮点数对应的具体值可由下⾯的表达式计算⽽得:
±(d0 + d1β-1 + ... + d p-1β-(p-1))βe, (0 ≤ d i< β)
对于⼗进制的浮点数,即基数 β 等于 10 的浮点数⽽⾔,上⾯的表达式⾮常容易理解,也很直⽩。计算机内部的数值表达是基于⼆进制的。从上⾯的表达式,我们可以知道,⼆进制数同样可以有⼩数点,也同样具有类似于⼗进制的表达⽅式。只是此时 β 等于 2,⽽每个数字 d 只能在 0 和 1 之间取值。⽐如⼆进制数1001.101 相当于 1 × 2 3 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1 ×2-3,对应于⼗进制的 9.625。其规范浮点数表达为 1.001101 × 23。
4.2  根据精度表⽰浮点数
以上⾯的9.625为例,其规范浮点数表达为 1.001101 × 23,
因此按单精度格式表⽰为:
1 10000010 00110100000000000000000
同理按双精度格式表⽰为:
1 10000000010 0011010000000000000000000000000000000000000000000000
5.  特殊值
通过前⾯的介绍,你应该已经了解的浮点数的基本知识,这些知识对于⼀个不接触浮点数应⽤的⼈应该⾜够了。不过,如果你兴趣正浓,或者⾯对着⼀个棘⼿的浮点数应⽤,可以通过本节了解到关于浮点数的⼀些值得注意的特殊之处。
我们已经知道,单精度浮点数指数域实际可以表达的指数值的范围为 -127 到 128 之间(包含两端)。其中,值-127(保存为全0)以
及 +128(保存为全1)保留⽤作特殊值的处理。本节将详细 IEEE 标准中所定义的这些特殊值。
浮点数中的特殊值主要⽤于特殊情况或者错误的处理。⽐如在程序对⼀个负数进⾏开平⽅时,⼀个特殊的返回值将⽤于标记这种错误,该值为 NaN(Not a Number)。没有这样的特殊值,对于此类错误只能粗暴地终⽌计算。除了 NaN 之外,IEEE 标准还定义了 ±0,±∞ 以及⾮规范化数(Denormalized Number)。
对于单精度浮点数,所有这些特殊值都由保留的特殊指数值 -127 和 128  来编码。如果我们分别⽤ e min 和 e max 来表达其它常规指数值范围的边界,即 -126 和 127,则保留的特殊指数值可以分别表达为 e min - 1 和 e max + 1; 。基于这个表达⽅式,IEEE 标准的特殊值如下所⽰:
其中 f 表⽰尾数中的⼩数点右侧的(Fraction)部分。第⼀⾏即我们之前介绍的普通的规范化浮点数。随后我们将分别对余下的特殊值加以介绍。
5.1  NaN
NaN ⽤于处理计算中出现的错误情况,⽐如 0.0 除以 0.0 或者求负数的平⽅根。由上⾯的表中可以看出,对于单精度浮点数,NaN 表⽰为指数为 e max + 1 = 128(指数域全为 1),且尾数域不等于零的浮点数。IEEE 标准没有要求具体的尾数域,所以 NaN 实际上不是⼀个,⽽是⼀族。不同的实现可以⾃由选择尾数域的值来表达NaN,⽐如 Java 中的常量 Float.NaN 的浮点数可能表达
为 01111111110000000000000000000000,其中尾数域的第⼀位为 1,其余均为 0(不计隐藏的⼀位),但这取决系统的硬件架构。Java 中甚⾄允许程序员⾃⼰构造具有特定位模式的 NaN 值(通过 Float.intBitsToFloat() ⽅法)。⽐如,程序员可以利⽤这种定制的 NaN 值中的特定位模式来表达某些诊断信息。
定制的 NaN 值,可以通过 Float.isNaN() ⽅法判定其为 NaN,但是它和 Float.NaN 常量却不相等。实际上,所有的 NaN 值都是⽆序的。数值⽐较操作符 <,<=,> 和 >= 在任⼀操作数为 NaN 时均返回 false。等于操作符== 在任⼀操作数为 NaN 时均返回 false,即使是两个具有相同位模式的 NaN 也⼀样。⽽操作符 != 则当任⼀操作数为 NaN 时返回 true。这个规则的⼀个有趣的结果是 x!=x 当 x 为 Na
N 时竟然为真。
可以产⽣ NaN 的操作如下所⽰:
此外,任何有 NaN 作为操作数的操作也将产⽣ NaN。⽤特殊的 NaN 来表达上述运算错误的意义在于避免了因这些错误⽽导致运算的不必要的终⽌。⽐如,如果⼀个被循环调⽤的浮点运算⽅法,可能由于输⼊的参数问题⽽导致发⽣这些错误,NaN 使得 即使某次循环发⽣了这样的错误,也可以简单地继续执⾏循环以进⾏那些没有错误的运算。你可能想到,既然 Java 有异常处理机制,也许可以通过捕获并忽略异常达到相同的效果。但是,要知道,IEEE 标准不是仅仅为 Java ⽽制定的,各种语⾔处理异常的机制不尽相同,这将使得代码的迁移变得更加困难。何况,不是所有语⾔都有类似的异常或者信号(Signal)处理机制。
注意: Java 中,不同于浮点数的处理,整数的 0 除以 0 将抛出 java.lang.ArithmeticException 异常。
5.2  ⽆穷
和 NaN ⼀样,特殊值⽆穷(Infinity)的指数部分同样为 e max + 1 = 128,不过⽆穷的尾数域必须为零。⽆穷⽤于表达计算中产⽣的上溢(Overflow)问题。⽐如两个极⼤的数相乘时,尽管两个操作数本⾝可以⽤保存为浮点数,但其结果可能⼤到⽆法保存为浮点数,⽽必须进⾏舍⼊。根据 IEEE 标准,此时不是将结果舍⼊为可以保存的最⼤的浮点数(因为这个数可能离实际的结果相差太远⽽毫⽆意义),⽽是将其舍⼊为⽆穷。对于负数结果也是如此,只不过此时舍⼊为负⽆穷,也就是说符号域为 1 的⽆穷。有了 NaN 的经验我们不难理解,特殊值⽆穷使得计算中发⽣的上溢错误不必以终⽌运算为结果。
⽆穷和除 NaN 以外的其它浮点数⼀样是有序的,从⼩到⼤依次为负⽆穷,负的有穷⾮零值,正负零(随后介绍),正的有穷⾮零值以及正⽆穷。除 NaN 以外的任何⾮零值除以零,结果都将是⽆穷,⽽符号则由作为除数的零的符号决定。
回顾我们对 NaN 的介绍,当零除以零时得到的结果不是⽆穷⽽是 NaN 。原因不难理解,当除数和被除数都逼近于零时,其商可能为任何值,所以 IEEE 标准决定此时⽤ NaN 作为商⽐较合适。
5.3  有符号的零
因为 IEEE 标准的浮点数格式中,⼩数点左侧的 1 是隐藏的,⽽零显然需要尾数必须是零。所以,零也就⽆法直接⽤这种格式表达⽽只能特殊处理。
实际上,零保存为尾数域为全为 0,指数域为 e min - 1 = -127,也就是说指数域也全为 0。考虑到符号域的作⽤,所以存在着两个零,
即 +0 和 -0。不同于正负⽆穷之间是有序的,IEEE 标准规定正负零是相等的。
零有正负之分,的确⾮常容易让⼈困惑。这⼀点是基于数值分析的多种考虑,经利弊权衡后形成的结果。有符号的零可以避免运算中,特别是涉及⽆穷的运算中,符号信息的丢失。举例⽽⾔,如果零⽆符号,则等式 1/(1/x) = x 当x = ±∞ 时不再成⽴。原因是如果零⽆符
号,1 和正负⽆穷的⽐值为同⼀个零,然后 1 与 0 的⽐值为正⽆穷,符号没有了。解决这个问题,除⾮⽆穷也没有符号。但是⽆穷的符号表达了上溢发⽣在数轴的哪⼀侧,这个信息显然是不能不要的。零有符号也造成了其它问题,⽐如当 x=y 时,等式1/x = 1/y 在 x 和 y 分别为 +0 和 -0 时,两端分别为正⽆穷和负⽆穷⽽不再成⽴。当然,解决这个问题的另⼀个思路是和⽆穷⼀样,规定零也是有序的。但是,如果零是有序的,则即使 if (x==0) 这样简单的判断也由于 x 可能是 ±0 ⽽变得不确定了。两害取其轻者,零还是⽆序的好。
5.4  ⾮规范化数
我们来考察浮点数的⼀个特殊情况。选择两个绝对值极⼩的浮点数,以单精度的⼆进制浮点数为例,⽐如 1.001 × 2-125和 1.0001 ×2-125 这两个数(分别对应于⼗进制的 2.6448623 × 10-38 和 2.4979255 × 10-38)。显然,他们都是普通的浮点数(指数为 -125,⼤于允许的最⼩值 -126;尾数更没问题),按照 IEEE 754 可以分别保存为
00000001000100000000000000000000(0x1100000)
和 00000001000010000000000000000000(0x1080000)。
现在我们看看这两个浮点数的差值。不难得出,该差值为 0.0001 × 2-125,表达为规范浮点数则为 1.0 × 2-129。问题在于其指数⼤于允许的最⼩指数值,所以⽆法保存为规范浮点数。最终,只能近似为零(Flush to Zero)。这中特殊情况意味着下⾯本来⼗分可靠的代码也可能出现问题:
浮点数的基数什么意思if (x != y) {
z = 1 / (x -y);
}
正如我们精⼼选择的两个浮点数展现的问题⼀样,即使 x 不等于 y,x 和 y 的差值仍然可能绝对值过⼩,⽽近似为零,导致除以 0 的情况发⽣。
为了解决此类问题,IEEE 标准中引⼊了⾮规范(Denormalized)浮点数。规定当浮点数的指数为允许的最⼩指数值,即 e min 时,尾数不必是规范化的。⽐如上⾯例⼦中的差值可以表达为⾮规范的浮点数 0.001 × 2-126,其中指数-126 等于 e min。注意,这⾥规定的是"不必",这也就意味着"可以"。当浮点数实际的指数为 e min,且指数域也为e min 时,该浮点数仍是规范的,也就是说,保存时隐含着⼀个隐藏的尾数位。为了保存⾮规范浮点数,IEEE 标准采⽤了类似处理特殊值零时所采⽤的办法,即⽤特殊的指数域值 e min - 1 加以标记,当然,此时的尾数域不能为零。这样,例⼦中的差值可以保存为 00000000000100000000000000000000(0x100000),没有隐含的尾数位。
有了⾮规范浮点数,去掉了隐含的尾数位的制约,可以保存绝对值更⼩的浮点数。⽽且,也由于不再受到隐含尾数域的制约,上述关于极⼩差值的问题也不存在了,因为所有可以保存的浮点数之间的差值同样可以保存。
6.  范围和精度