一.浮点数与IEEE 754
浮点数可以表示成:
[-](-1)S(d0.d1d2…)bE
浮点数的二进制格式由两部分组成。一是尾数部分,就是上式中的[-](-1)S(d0.d1d2…),其次是指数部分,就是上式中的bE。
[-]表示这个负号是可选的。早期的机器数,符号位S用1表示正数,0表示负数。要加上这个负号。后来发现,如果反过来,以0表示正数,计算会方便得多。现在的机器都是这样,就没有这个负号。
浮点数与科学记数法相似,同样大小的一个数,可以有无数种表示(尾数小一点指数大一点,或相反)。这会造成精度损失。譬如,1011101110111011×22有16位精度,可是如果把它表示成0000000000000001×217,就只有1位精度了。为此,定义了一种归一化小数,使尾数的绝对值符合归一化规范。常见的归一化小数有两种:
0.1d1d2…
1.d1d2…dp
IEEE 754采用第二种。由于其整数恒为1,就不为它安排存储位了。
尾数归一化后,每个浮点数的表示就是唯一了。
为了解决负数问题,尾数可以采取原码、反码、补码三种编码之一。
对于现在的大多数CPU来说,指数部分的底都是2。以前也有10和16(IBM S/370就是16)。
显然,为了使浮点数能以给定的精度表示给定范围内的任意值,各段(同一指数为一段)之间既不脱节也不重叠,尾数绝对值的最大值与最小值之比应等于指数的底。譬如,若底是2,则尾数在[1, 2]区间能满足上述要求。
指数E一般是整数。现在多采用移码来解决负数问题。所谓移码,就是加上一个数,使范围移至全正数区域。譬如,指数为-128~127,全部加128,变成移码就是0-255。
下面以IEEE 754为例,说明浮点数的一些基础知识。
原始的IEEE 754定义了两种浮点数:single和double。下面以single(相当于C里的float)为例,说明该标准的格式(括号内的是double的属性)。
32(64)位长。b31 (b63)是尾数的符号位,0为正。b30~b23 (b62~b52)是表示指数的移码。加权值是127(1023)。0~255(0~2047)的值域,实际上指数是-127~128(-1023~1024)。b22~b0 (b51~b0)是尾数绝对值的小数部分,其整数部分隐含,固定为1,不占位置。
因此,在指数域里存放的不是指数E本身,而是指数的移码(E+Δ)。在IEEE 754里Δ是127(1023)。
书里说,尾数占23(52)位,再加上隐含的那个1,single(double)有24(53)位有效数(二进制)。这种说法是错的。小数点左边的那一位恒为1,对浮点数的精度没有任何贡献,不应计入有效数位。对不对?不过,说“有24(53)位有效数”,没错。因为符号位对精度有贡献,应该计入有效数位。如果说绝对值,那有效数位应该是23(52)位。
由此可见,尾数的绝对值范围是1.00…0~1.11…1。写成十进制,值域就是:[1,2]。因为指数部分的底规定为2,故合起来可以表示整个浮点数值域范围内的任何数。
有四种特殊情况。
1)如果移码等于255(2047),而b22~b0 (b51~b0)中至少有一位不为零,则该浮点数为NaN(Not a Number)。取决于不同的编译器及编译器设置,NaN有SnaN(Signaling NaN)和QnaN(Quiet NaN)两种。前者会引发异常(关于异常,可参见“进程与存储分配”一文)。
2)如果移码等于255(2047),且b22~b0 (b51~b0)为全0,则表示+∞(符号位S为0)或-∞(S为1)。
3)如果移码等于0,且b22~b0 (b51~b0)为全0,则表示+0(符号位S为0)或-0(S为1)。
4)如果移码等于0,而b22~b0 (b51~b0)中至少有一位不为零,则浮点数的值是:(-1)S(0.d1d2…)b-126((-1)S(0.d1d2…)b-1022)。
二.关于浮点数的一些实验
你可以利用union做实验,
2.1 浮点数的分解与合成
如果已知浮点数格式,可以利用union来取出某浮点数的尾数和指数。
当然,你也可以反过来,用尾数和指数来合成浮点数。
2.2 比对本机浮点数格式是否符合某一标准
以上两部分相对于下面是比较简单的。如果看懂了2.3节,就不用说了。
2.3 分析本机的浮点数格式。
这个比较复杂。先要熟悉第一章的内容,也就是搞清浮点数格式的一般规律,然后设计一连串实验,由易到难,一步一步摸索出来。
以下以IA-32/RedHat Linux/GCC平台为例。
2.3.1 测浮点数的长度
很简单,只要使用sizeof()运算符就可以了。
#include <stdio.h>
int main()
{
float f;
printf(“%d/n”, sizeof(f));
}
结果是4字节。
这个是必须首先做的。连总长度也不知道,没法往下测。在以后的测试里,也要使用这个字节数。浮点数占几个字节,以后union里的c[]就定义几个元素。这样才能把浮点数的每一个bit的值反映出来。下面都是c[4]。
2.3.2 判断尾数符号位的位置和编码
比较一下两串字节数,应该只有一个bit是不同的。这个bit就是符号位。既判知了哪个是符号位,同时也判知了0表示正数,还是1表示正数。
union {
float f;
unsigned char c[4];
}u;
u.f = 1;
printf(“%X/t%X/t%X/t%X/n”, u.c[3], u.c[2], u.c[1], u.c[0]);
u.f = -1;
printf(“%X/t%X/t%X/t%X/n”, u.c[3], u.c[2], u.c[1], u.c[0]);
结果是
3F 80 00 00
BF 80 00 00
翻译成二进制,就是0011 1111 1000 0000 0000 0000和1011 1111 1000 0000 0000 0000。显然,只有最高位不一样。由此可推知,符号位是最高位b31,且0表示正数。符合IEEE 754。
2.3.3 判断指数移码的位置、长度和加权数
还是利用前面的代码段,把测试数据u.f的值分别设为8、64。出来的结果是
41 00 00 00
42 80 00 00
翻译成二进制,就是0100 0001 0000 0000 0000 0000和0100 0010 1000 0000 0000 0000。
1、8、64分别正好是2的0、3、6次方。它们的尾数应该是一样的(假如你的系统符合IEEE 754,那么尾数应该都是1.00…0),不同的只是指数部分。从三者的二进制数,发现其不同处是b30~b23(指数域是8bits)。分别是:01111111、10000010、10000101,翻译成十进制是127、130、133,对应的指数是0、3、6。显然,指数移码的加权是127。指数域的位置、长度及编码法则也符合IEEE 754。
2.3.4 判断尾数的编码
符号位和移码位确定后,剩下的就是尾数(不包括符号)了。对这个平台来说,就是b22~b0。需要分析的是:采用哪种编码?小数点的位置?有没有像IEEE 754那样的隐含位?
从2.3.2的测试结果可以看出,当数值为1时,尾数域为全0。这意味着,我们的平台很可能和IEEE 754一样,尾数范围也是1.00…0~1.11…1且小数点左边的1是隐含的,不占位置。
仍然是同样的代码,用1.23和-1.23测试,结果是
3F 9D 70 A4
BF 9D 70 A4
前面一个是3(0011),一个是B(1011),那是由于符号位不同的缘故。只有最高位不同,说明尾数部分采用的不是补码,而是原码。绝对值相同,符号不同的两个数的数值位,只有原码是相同的。补码和反码都不同。
取b22~b0,两数都是:00111010111000010100100。加上隐含的整数和小数点,就是1.00111010111000010100100。我们手工把1.23翻译成二进制,大致是:1.00111010111000010…。和程序结果是吻合的。
这一部分的分析还没有完。不往下做了,因为你自己也可以继续的。
2.3.5 分析小结
1)首先要对基础知识有所了解。完全瞎摸是不行的。
2)其次是要找到有效的工具。譬如本文的union。
3)精心设计试验数据。实验设计得好,会大大减少工作量。
4)对实验的结果,要分析到底。即要充分利用实验结果。
5)最后考察整体,看能不能自圆其说。
附注(关于二进制浮点运算标准):
自浮点数出现以来,有过许多种二进制格式,给代码的可移植性造成了障碍。
为此,IEEE于1985年制订了二进制浮点运算标准IEEE 754。该标准限定指数的底为2。同年,被美国引用为ANSI标准。
考虑到IBM System/370的影响,IEEE于1987年推出了与底数无关的二进制浮点运算标准IEEE 854。同年,被美国引用为ANSI标准。
1989年,国际标准组织IEC批准IEEE 754/854为国际标准IEC 559:1989。后来经修订后,标准号改为IEC 60559。
现在,几乎所有的浮点处理器完全或基本支持IEC 60559。C99的浮点运算也支持IEC 60559。