浮点数与IEEE浮点标准

1 IEEE浮点数

1.1 格式

IEEE定义了多种浮点格式,但最常见的是三种类型:单精度、双精度、扩展双精度,分别适用于不同的计算要求。一般而言,单精度适合一般计算,双精度适合科学计算,扩展双精度适合高精度计算。一个遵循IEEE 754标准的系统必须支持单精度类型(强制类型)、最好也支持双精度类型(推荐类型),至于扩展双精度类型可以随意。

 

长度

符号

指数

尾数

有效位数

指数偏移

说明

单精度

32

1

8

23

24

127

1个隐含位

双精度

64

1

11

52

53

1023

1个隐含位

扩展双精度

80

1

15

64

64

16383

没有隐含位

需要特别注意的是,扩展双精度类型没有隐含位,因此它的有效位数与尾数位数一致,而单精度类型和双精度类型均有一个隐含位,因此它的有效位数比位数位数多一个。

为了强制定义一些特殊值,IEEE标准通过指数将表示空间划分成了三大块:最小值指数(所有位全置0)用于定义0和弱规范数,最大指数(所有位全值1)用于定义±∞和NaNNot a Number),其他指数用于表示常规的数。这样一来,最大(指绝对值)常规数的指数不是全1的,最小常规数的指数也不是0,而是1

1.1.1 单精度类型

C/C++中的floatFORTRAN中的REAL*4Visual Basic中的SingleJava中的float等均是单精度类型。VC6float的支持是打了折扣的,虽然VC6使用float存储数据,但在函数调用过程中,所有float都会被转换为double。这导致在Vc6中,float的效率还不如double(多了层转换),而且有些细微的差异。不过,在一般计算中问题不大,但最好避免在VC6中使用float。除非内存过于紧张,否则没有什么好处。

 

1)格式参数

定义一个数据结构描述单精度类型的存节:

typedef struct _FP_SIGLE

{

    unsigned __int32 nFraction : 23;

    unsigned __int32 nExponent :  8;

    unsigned __int32 nSign     :   1;

} FP_SINGLE;

 

下列语句输出0.5的符号、指数和尾数:

float a = 0.5;

FLOAT_SINGLE* p = (FLOAT_SINGLE*)&a;

printf( "%d/n", p->nSign );

printf( "%d/n", p->nExponent - 127 );       // 注意要减去偏移

printf( "%d/n", p->nFraction );

输出结果是0,-1,00.5是正数,因此符号位是0,它的二进制表示是0.12,标准化后是 ,因此指数是-1,尾数部分全是0(那个1是隐含的)。

下面将一个实数格式化为单精度类型。例如:

-235.125= -100100101001.0012 = -1.001001010010012×211

所以符号位是1,指数是:

11+127=138=10001012

而尾数是001001010010010000000002

 

2)表示范围

 

指数

尾数

数值

最大数

0xFE

0x7FFFFF

3.4028234663852886E+38

最小数

0x01

0x000000

1.1754943508222875E-38

最小弱规范数

0x00

0x000001

1.4012984643248171E-45

 

3)有效数字

单精度类型有24位有效位,因此有效数字是0.301×24=7.2,即单精度类型有7~8位有效数字。

1.1.2 双精度类型

C/C++中的doubleFORTRAN中的REAL*8Visual Basic中的DoubleJava中的double等均双是精度类型。

 

1)参数格式

定义一个数据结构描述双精度类型的存节:

typedef struct _FP_DOUBLE

{

    unsigned __int64 nFraction  : 52;

    unsigned __int64 nExponent  : 11;

    unsigned __int64 nSign      :  1;

} FP_DOUBLE;

 

例如下列语句生成一个双精度浮点数0.5

BYTE k[8];

FLOAT_DOUBLE* p = (FLOAT_DOUBLE*)k;

p->nSign = 0;

p->nFraction1 = 0;

p->nFraction2 = 0;

p->nExponent = 1023 - 1;  // 2-1

 

2)表示范围

 

指数

尾数

数值

最大数

0x7FE

0xFFFFFFFFFFFFF

1.7976931348623157E+308

最小数

0x01

0x0000000000000

2.2250738585072014E-308

最小弱规范数

0x00

0x0000000000001

4.9406564584124654E-324

 

3)有效数字

双精度类型有53位有效位,因此有效数字是0.301×53=15.9,即单精度类型有15~16位有效数字。

1.1.3 扩展双精度类型

表面上看,常见的计算一般无需使用扩展双精度类型,因此扩展双精度类型很少见,但这很可能是个错觉。一些浮点硬件(例如 Intel x87 FPU)将扩展双精度类型作为内部格式,即在计算前,其他浮点格式均需转换为扩展双精度类型,然后才进行计算,在输出计算结果时,又将扩展双精度类型转换为其他格式。甚至可以说,扩展双精度类型是最常用的格式,只是一般用户不常见到而已。在C/C++中,long double一般对应于扩展双精度类型,但由于这不是C/C++标准中约定的(C/C++标准只约定long double的精度和范围高于double),因此并不一定是,例如在VC6中,long doubledouble一样是双精度类型。

 

1)格式参数

定义一个数据结构描述扩展双精度类型的存节:

typedef struct _FP_DOUBLE_EXT

{

    unsigned __int64 nFraction;

    unsigned __int16 nExponent  : 15;

    unsigned __int16 nSign      :  1;

} FP_DOUBLE_EXT;

 

例如下列语句生成一个双精度浮点数0.5

BYTE k[10];

FLOAT_EXTENDED* p = (FLOAT_EXTENDED*)k;

p->nSign = 0;

p->nFraction = 0x8000000000000000;  // 注意此处的63位(整数位)总是1

p->nExponent = 16383 - 1;    // 2-1

 

2)表示范围

 

指数

尾数

数值

最大数

0x7FFE

0xFFFFFFFFFFFFFFFF

1.18973149535723176E+4932

最小数

0x0001

0x0000000000000000

3.36210314311209552E-4932

最小弱规范数

0x0000

0x0000000000000001

1.82259976594123730E-4951*

 

3)精度

扩展双精度类型有64位有效位,因此有效数字是0.301×64=19.2,即扩展双精度类型有19~20位有效数字。

1.2 分类

前面已大致提及IEEE为了定义一些特殊值以提高特殊情形下的处理能力,通过指数将表示空间划分成了三类。其实这只是大致划分,实际上更为详细的划分如下表所示:

指数

 

隐含位

尾数

说明

 

111...111

 

QNaN

1

1XX...XXX

尾数高位为1,但尾数不为0

SNaN

1

0XX...XXX

尾数高位为0,但尾数不为0

无穷(∞)

1

0

尾数不0

 

有限数

1

XXX...XXX

指数非0、非全1

000...000

弱规范数

 

XXX...XXX

没有隐含位,尾数不是0

零(0

0

0

尾数为0

可以看出,IEEE定义了6类数(暂且认为NaN或∞是数吧,不然不好措辞):QNaNSNaN、∞、有限数、0和弱规范数。如果考虑符号,那么还可以划分更多,例如∞还可以分为+∞和-∞。虽然这些数均有严格清晰的描述,但到底是多少种,却不大说得清。你可以认为+∞和-∞是一类,也可以认为它们是两类,还有SNaNQNaN,虽然有时也统称为NaN,但它们在用途上有较大区别。因此,IEEE的数到底有几类,往往取决于你的观点。

其实,还有一种类型,就是违反IEEE 754标准格式的数。由于符合标准的数已经占据了全部单精度类型和双精度类型所能表示的范围,因此,违反标准格式只在扩展双精度类型中出现,例如指数全1但隐含位却是0、指数全0而隐含位是1。对于这种数,某些硬件系统和软件系统可能也会处理(例如x87 FPU),但应该禁止使用这种超出标准的格式。

使用这些特殊的数有时是非常困难的,不同的系统有不同的方式,并非都遵循IEEE 754标准。例如,如果这些数参与运算,那么结果是什么?什么时候应该触发异常?触发什么异常?如何使用NaN?那些操作是非法的,而那些操作是合法的,但结果却是NaN一类的东西?诸如此类。

使用特殊值(0除外)在某种程度上意味着进入灰色地带。对于一些情形,IEEE 754标准并未做出规定,或者相应的规定在实际应用中不切实际。即使在IEEE 754标准规定得很好的情形下,一些系统也出于各种考虑,并不遵循。例如,IEEE 754标准对Sin()没有规定。对表达式x!=yxyNaN时的值做出的规定却难以应用。如果执行IEEE 754标准则意味着,要么浮点比较指令相当复杂,要么编译器在计算这个表达式时进行特殊处理。不管怎样,都会导致效率降低,却没有明显的益处(毕竟,NaN参与运算是极其罕见的情形,正常计算中是不应该出现的)。在NaN的格式中,IEEE 754标准提倡编译器或硬件系统在NaN中加入一些信息以支持调试,但几乎没有系统响应这个倡导。

通过处理异常似乎可以避开这片暗礁,但有时是不行的。例如,当你搜索一个超越方程的根的时候。如果你不知道根的分布区间(这是常有的事),那么在搜索过程中极可能遇到无穷、溢出、NaN和异常之类。如果程序要自动完成根的搜索,它就必需能够处理这些问题,在每次尝试失败后能重新设置初值。可见,有时候这些麻烦是无法避免的。

 

1)有限数

在这几类数中,有限数是最常用的,也是唯一以常规方式解释的数。有限数的特征是指数在最大值和最小值之间,且整数位恒是1。例如对于单精度类型,它的指数有8位,考虑偏移后,指数最大值是255,最小值是0,而有限数的指数就在[1254]区间。整数位是隐藏的,恒是1它的形式是:

±( 1 + f )×2E-OFFSET

其中,E是指数,OFFSET是指数偏移,1是被隐含的整数位,f是尾数其他部分。

有限数的使用除了遵循数学规则之外,没有其它规则。当然,有些出于硬件考虑附加的限制是存在的,例如计算正弦函数的FSIN指令就对输入的数据范围施加了限制。

除了违反数学规则之外,在一般使用有限数的过程中,最常见的问题是溢出,即运算结果超出了有限数的表示范围。浮点数长度越小这个问题越常见,例如单精度类型。不过,只要稍微注意一下,一般不是大问题。

 

20

0的特征是指数、尾数、整数位全0,只有符号位可能不是0。它的形式是:

±( 0 + 0 )×20-OFFSET

与数学中0无正负不同,IEEE 754标准定义的0有正负,即0有两种:+0-0。之所以如此,有几个原因:

[1] 被零除通常产生无穷,而无穷有正负无穷两类;

[2] CopySign函数可以无需特别处理;

以上每个原因都不是绝对要求(毕竟,数学上0无正负就意味着0可以没有符号),但在软硬件实现上给0加上符号却带来一些方便。不过,这也意味着比较指令需要特别注意,因为+0-0应相等,而不是+0大于-0

 

3)弱规范数

若规范数的指数与0一样是0,它整数位也是0,但尾数部分不是0。它的形式是:

±( f )×20-OFFSET

但此处的f不局限于[0,1),而是(0,2)

IEEE标准引入弱规范数的目的是实现一种称为“逐渐下溢”的技术。在计算过程中,如果中间结果小于最小的有限数却不是0(即出现下溢),当作0处理会导致计算终止(例如病态矩阵)。引入弱规范数以后,在0和最小的有限数之间相当一部分数可以表示为弱规范数,从而提高了计算能力。例如单精度类型最小的有限数是1.1754943508222875E-38,而最小的弱规范数是1.4012984643248171E-45

 

4)∞

∞的指数部分是最大值,整数位是1,尾数部分是0。它的形式是:

±( 1 + 0 )×2MAX-OFFSET

因此∞有两类,即+∞和-∞。产生∞的一般情形有:

[1] ∞自身运算,例如-+1.0得到-∞;

[2] 0除,例如1/+0得到+∞;

[3] 上溢,即计算结果超出了类型范围,通过舍入得到∞。

由于在一般数学中,∞是不能参与运算的,因此IEEE的这些规定可以说是某种扩展。

 

5NaN

NaN的意思是Not a Number或者Not any numberNaN之所以显得比较奇怪,是因为数学上本没有这么一个数或符号,它纯粹是为了方便处理而提出来的,但它的历史可不短。早在三十年代后期就有人提出了类似NaN的概念。1963年的CDC 6600系统实现了它,但将它视为“没有定义”。后来,DECPDP-11VAX系统也使用它,但将它用作“保留的操作数”。时至今日,虽然IEEE明确地定义了NaN,但在实际使用过程中,NaN经常被误解误用,需要特别小心。

与∞一样,NaN的指数部分是最大值,整数位是1,但它尾数部分不是0。它的形式是:

±( 1 + f )×2MAX-OFFSET

其中,f0

NaN有两类,一类是QNaNQuiet NaN),一类是SNaNSignal NaN)。两者的不同在于IEEE标准要求,如果SNaN参与运算要触发非法操作异常,而QNaN参与运算可以不触发异常。两者在格式上的区别在于,QNaN的尾数最高位是1,而SNaN的尾数最高位是0。一般情形下,如果不特别声明,NaN指的是QNaN

IEEE标准引入NaN的目的是希望给编译器等系统一个约定的值设置未初始化的数据,或者在计算出问题时可以返回一个东西提示计算出现了问题。

2 两个问题

2.1 遵循IEEE标准?

在多大程度上遵循IEEE标准与目标系统的性质有关。如果目标系统是个面向广泛用户的、商业化的产品,例如公开发售的浮点芯片或某个语言的编译器,那么尽可能严格地遵循IEEE标准是必需的。如果目标系统中存在与IEEE标准相抵触的特性,这些特性可能会给用户的开发或其上的代码带来问题,因为那些系统通常会假设目标系统是遵循IEEE标准的。但如果目标系统只是一个受到严格控制的、只在有限范围内应用的系统,例如某款手持设备的浮点仿真库,用户只是有限的几个产品开发商,那么全面遵循IEEE标准是不必要地耗费精力和金钱。对这类目标系统,如何达到性能指标和开发速度要求才是主要问题,试图严格遵循IEEE标准会给系统开发带来阻碍,而且没有可观的回报。有选择地遵循一些IEEE标准的主要特性(例如浮点格式)、忽略那些几乎不可能给目标系统带来任何好处的特性(例如QNaNSNaN的区分、NaN参与逻辑比较运算时的琐碎约定)是这类系统的理性选择。

举个例子说明一些严格遵循IEEE标准可能会带来的问题。例如IEEE建议的函数:

hypot( x, y ) = sqrt( x2 + y2 )

这个函数简单的实现代码如下:

double hypoy( double x, double y )

{

    return sqrt( x*x + y*y );

}

但这个实现不符合IEEE标准的要求。按照一些人对IEEE标准的理解,由于sqrt(x2+2)=+∞在x取任何有限数、弱规范数、时都成立,因此也要求sqrt(NaN2+2)=+∞成立。然而,在上述实现代码中,当x=NaN时,返回值是NaN。因此,需要对参与运算的情形作特殊处理。伪码如下:

double hypoy( double x, double y )

{

    if( isinfinite( x ) || isinfinite( y ) )

        return infinite( 0 );

    return sqrt( x*x + y*y );

}

这只是一个简单的例子,许多函数的特殊情形处理远比这个函数要复杂(参见附带源码)。这些特殊处理代码给维护带来困难,降低了代码的效率,而且看不出这些代码带来了什么好处。毕竟,数学上没有NaN、数值分析中没有NaN和∞,这些特殊情形处理代码在一般计算中几乎没有用处。而且,有些约定的返回值并不比返回其他值更有理由,为什么需要特别添加这些代码呢?例如在上述代码中,实在看不出,当有∞参与运算(在数值分析中,这是不可能的,因为∞的出现就意味着计算出现了溢出,计算通常应该停止)时,返回+∞比返回NaN有什么好处。甚至,由于返回+∞而不是NaN,掩盖了NaN带来的计算异常(未初始化数据、执行了非法操作等)警告,更为不妥。

当然,以上观点只是一家之言。

2.2 弱规范数格式的解释

无穷只参与有限的运算,没有具体值,但弱规范数不同,它有具体的值而且像有限数一样参与运算,因此如何解释弱规范数的格式(即确定与它对应的数值)非常重要。前面将弱规范数记为:

±( f )×20-OFFSET

其中f是尾数部分,E是指数部分,OFFSET是指数偏移。需要特别指出的是,弱规范数没有0,它的整数位就是尾数的最高位,因此f的取值范围是[0,2),这导致弱规范数与有限数的解释不一样,相当晦涩,下面以双精度类型为例说明一下。

假设若规范数有一个为0的隐含位,那么2-1023将无法用任何方式记录下来。因为如果用有限数格式,即:

sign = 0;

exponent = 0x000

fraction = 0x0000000000000

这个数竟然变成了0的格式!如果使用弱规范数表示(有一个隐含的0),即:

sign = 0;

exponent = 0x001

fraction = 0x8000000000000

可是既然它的指数不是0,它自然也就不能被视为弱规范数了。显然,哪儿出了问题。问题就出在认为弱规范数有一个为0的隐含位。

如果认为弱规范数没有隐含位,而是以最高尾数位作为整数位,那么2-1023是:

sign = 0;

exponent = 0x000

fraction = 0x8000000000000

这意味着,当从弱规范数形式转换为有限数形式时,不仅要向左移位以产生隐含位,而且在移位时,指数是不减1的(因为只是整数位移动,值不变)。下列代码取自VC6浮点库frexp()反汇编代码。frexp()分解double类型的指数和尾数部分(以一个有限数形式返回)。它使用下列循环产生尾数部分的隐含位以及修正指数部分:

while( px->nExponent & 1 == 0 )

{

    *(unsigned __int64*)&x = *(unsigned __int64*)&x << 1;

    *expptr --;

}

通过向左移位寻找一个1设置隐含位,由于向左移位意味着乘以2,因此需要同时减小指数。问题的关键在于指数初值:

*expptr = -1021;

一个规范好的弱规范数的指数必然是-1023,由于frexp()返回的尾数部分需要显示隐含位,因此尾数部分被除以了2(即通过右移一位以使隐含位出现),因此指数部分需要加1,由此得到-1022。由于上述循环每次移位指数均减1,但实际上最后一次移位时,只是将弱规范数的整数位设置为隐含位,没有发生数值变化,不应该减1。也就是说,循环多减了一次。因此初值要补足一个1,于是得到-1021

还有一个细节需要注意,就是弱规范数在单精度类型或双精度类型与扩展双精度类型之间进行转换时,扩展双精度的整数位来自尾数的最高位,而不是有限数中的隐含位。再次强调一下,无论在任何各格式中,若规范数没有隐含位,尾数最高位就是它的整数位。

 

你可能感兴趣的:(C/C++)