浮点数不能判等研究

时间:2022-10-18 07:40:53

浮点数不能判等研究

摘要:在软件开发过程中,浮点数的表示和运算一直是重点也是难点。基于目前计算机界广泛使用的IEEE754浮点标准,针对C/C++语言中“浮点数为什么不能判等”这个典型问题进行了分析和研究,力求给软件开发及测试人员一个清晰的阐述,研究中得出了两个有趣的结论。

关键词:软件;编程准则;浮点数;IEEE754

中图分类号:TP301

文献标识码:A 文章编号:1672-7800(2014)003-0046-03

0 引言

国军标5369中,明确提出“禁止对实数类型的量做是否相等的比较”。至于为什么不能进行浮点数判等,很少有软件设计人员真正明白其深层原因。工程实践中,往往有许多软件开发者会不小心忘记这条强制性编码准则,在编制程序时出现错误,从而导致了意想不到的严重后果。

1 典型案例描述

某软件中有如下违背该准则的代码:

经调试发现,程序并未执行软件编码人员意想之中的flag=1的分支,而是执行了flag=2的分支。

2 浮点不能判等原因分析

为什么浮点数不能直接判等?这与浮点数的表示方法、范围和精度、舍入方式有关,而这些随计算机所遵循的浮点运算标准的不同而不同。

2.1 IEEE754标准

任何数据在计算机内存中都以二进制形式顺序存储,每一个1或0被称为1位,一个字节是8位。比如一个16位的short int型变量的值是3000,那么它的二进制表达式为00001011 10111000。而对于浮点数,同样的数值可以有多种表达方式,比如213.14可以表达为21.314×101、2.1314×102或者0.21314×103,小数点的浮动使得数值的表示不能惟一,从而给数据处理带来困难。

为便于软件移植,浮点数的表示格式应该有统一标准。1985年IEEE(Institute of Electrical and Electronics Engineers)提出了IEEE754标准。目前绝大部分C/C++编译器都遵照该标准进行浮点运算。

2.2 浮点数表示

IEEE754标准采用科学计数法,用符号位、阶码和尾数来表示一个浮点数,它规定底数为2,即把浮点数表示成尾数乘以2的阶码次方再添上符号位,阶码采用移码表示,尾数采用原码表示。

标准中几种典型的浮点数格式如表1所示。前两种数据类型正好对应C/C++语言中的float和double精度类型,或者FORTRAN语言中的real和double精度类型。限于篇幅,本文仅对单精度和双精度这两种常用的浮点格式进行介绍。

下面讨论浮点代码与其真值的关系。浮点类型数据在内存中的存储格式如图1所示。对于单精度数,最高位(bit31)是符号位S,其后8位(bit30-bit23)是阶码E,其余23位(bit22-bit0)是尾数M。对于双精度数,最高位(bit63)是符号位S,其后11位(bit62-bit52)是阶码E,其余52位(bit51-bit0)是尾数M。

其中,E为浮点代码中的阶码,M为尾数,bias为阶码的偏置值。阶码为移码形式,因而阶码的真值为:阶码-阶码偏置。对于单精度数,阶码偏置为127(2(8-1)-1),阶码1~254分别对应的阶码真值是-126~+127。对于双精度数,阶码偏置为1023(2(11-1)-1),阶码1~2046分别对应的阶码真值是-1022~+1023。在IEEE754标准中,阶码全为0以及全为1被保留用作特殊处理,本文不讨论标准中定义的这些特殊值。文中介绍的浮点数,其阶码为正常值,因而均属于规格化浮点数。尾数为原码形式,根据原码的规格化方法,最高数字位总是1,IEEE754标准采用隐含尾数最高数位1的方法,将这个1缺省存储,使得尾数表示范围比实际存储多一位。应注意尾数隐含的1是一位整数(即位权为20),浮点格式表示出来的尾数是纯小数并为原码形式,因而尾数的真值为:1+尾数。接下来通过两个例子进一步阐述浮点数表示。文中的(data)10代表十进制数data,(data)b代表二进制数data。例1:若采用IEEE短实数格式,试求出32位浮点数代码0xCC968000的真值。解:将十六进制码转换成二进制浮点代码形式:1,10011001,00101101000000000000000由于符号位S是1,所以该数为负数。阶码真值=(10011001)b-(127)10=(153)10-(127)10=(26)10尾数真值=1+(0.00101101)b=1+(2-3+2-5+2-6+2-8)10=1+(0.17578125)10=(1.17578125)10故该浮点数的真值为-226×1.17578125例2:将-(0.1011101)b用IEEE短实数浮点数格式表示出来。解:-(0.1011101)b=-0.1011101×20=-1.011101×2-1=-(1+0.011101)×2-1该数为负数,所以符号位是1。阶码=阶码真值+127=-1+127=126=(01111110)b尾数=0.011101000……0所以其浮点数代码为1,01111110,0111010000000 0000000000

2.3 范围和精度

很多小数根本无法在二进制计算机中精确表示,由于浮点数尾数域的位数有限,为此,计算机的处理办法是持续计算直到得到的尾数足以填满尾数域,之后对多余的位进行舍入。换句话说,十进制到二进制的变换并不能保证总是精确的,而只能是近似值,这就涉及到浮点数的范围和精度问题。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达,再加上浮点运算过程中的误差积累,许多看来非常简单的十进制运算在计算机上的结果往往出人意料。浮点数的表示范围与阶码和尾数的位数以及采用的浮点数表示格式有关,而精度则主要由尾数决定。IEEE754标准中,单精度数所能表示的最大正规格化数,其阶码和尾数的值分别为(11111110)b、(111 1111 1111 1111 1111 1111)b,该数二进制数值为1.(23个1)×2127,而所能表示的最小正规格化数,其阶码和尾数部分的二进制值分别为1和0,该数二进制数值为1.(23个0)×2-126。同理,可计算出双精度数所能表示的最大正规格化数和最小正规格化数,其二进制数值分别为1.(52个1)×21023和1.(52个0)×2-1022。经归纳总结,将C/C++语言中经常使用的float和double类型这两种浮点存储格式的范围和精度列入表2。

2.4 舍入方式

在两个可以精确表示的相邻浮点数之间,必定存在无穷多实数是IEEE浮点格式所无法精确表示的数。如何用浮点格式表示这些数,IEEE754默认的方法是用距离该实数最近的浮点数来近似表示。

对于单精度数(float型),尾数是24位,其中1位隐藏,所以可以表达的最大尾数为224-1=16 777 215。如表3所示,偶数16 777 216可以被精确保存,但数值16 777 217无法被精确保存,由此可以看出单精度浮点数可以表达的十进制数值中,真正有效的数字不高于8位。事实上,对相对误差的数值分析结果显示有效的精度大约为7.22位。

IEEE754标准默认要求无法精确保存的值必须向最接近的可保存的值进行舍入,这有点类似十进制的四舍五入,即不足一半则舍,一半以上(包括一半)则进。不过对于二进制浮点数而言,还多出一条规矩,就是当需要舍入的值刚好是一半时,不是简单地进,而是在前后两个等距接近的可保存的值中,取其中最后一位有效数字为零者。从表3可以看出,奇数都被舍入为偶数,且有舍有进,可以将这种舍入误差理解为“半位”的误差。为了避免7.22位对很多人造成困惑,有些文章以7.5位说明单精度浮点数精度问题。IEEE754标准采用的浮点数舍入规则有时也被称为:舍入到偶数(Round to Even)。相比简单的逢一半则进的舍入规则,舍入到偶数有助于从某些角度减小计算中产生的舍入误差累积。

2.5 十进制浮点数转换二进制浮点数

如何将浮点类型十进制数转换成二进制数?办法是把整数部分和小数部分分别转换。整数部分用2除,取余数,小数部分用2乘,取整数位。

例3:将(99.1)10用IEEE短实数浮点数格式表示出来。

解:整数部分:(99)10=(1100011)b

小数部分:0.1×2=0.2,整数位是0

0.2×2=0.4,整数位是0

0.4×2=0.8,整数位是0

0.8×2=1.6,整数位是1

0.6×2=1.2,整数位是1

0.2×2=0.4,整数位是0

0.4×2=0.8,整数位是0

0.8×2=1.6,整数位是1

0.6×2=1.2,整数位是1

……

得到一个无限循环的小数0.000110011……

因此,(99.1)10=(1100011.000110011……)b

=(1.100011000110011……)×26

=(1+0.100011000110011……)×26该数为正数,所以符号位是0。阶码=阶码真值+127=6+127=133=(10000101)b尾数=0.100011000110011……所以十进制数99.1的浮点代码为0,10000101,10001100011001100110011

2.6 案例详细解剖

介绍完上面的理论知识,再回过头来看第2节典型案例描述中的代码:double Ia = 1.345;double Ib = 1.123;double I_expected = 2.468;double I_sum = Ia + Ib;调试这段程序,在内存中查看到Ia的IEEE754浮点代码是0x3ff5851ec0000000,将这个16进制数转换为二进制格式如下:00111111 11110101 10000101 00011110 11000000 00000000 00000000 00000000由于阶码在1~2046之间,所以为规格化数,计算尾数时应加上隐含的1。阶码真值=(0111111 1111)b-(1023)10=0尾数真值=1+(0101 10000101 00011110 11000000 000000000000000000000000)b=1+0.34500002861022 94921875=1.3450000286102294921875按照同样的计算方法,得出Ib的值实际上为1.123 0000257492065,因此Ia+Ib的值实际上为2.468000 054359436,而事实上I_expected的值为2.467999935 1501465。

这就是第二节典型案例描述中的软件代码为什么执行flag=2的分支的原因。

3 解决措施

如何避免浮点数直接判等呢?可以将浮点变量与浮点数据的相等判断设法转化成区间判断的形式。比如浮点变量x与浮点数据0.0的相等判断,可以通过如下方式判断:fabs(x)≤err,其中err为允许误差。按照以上方法,修改第二节典型案例描述中的软件代码,经调试发现,程序执行了意想之中的flag=1的分支。

4 结语

一个十进制实数能否用二进制浮点数精确表示,关键在于小数部分。按照2.5小节介绍的乘以2取整数位的方法,计算(0.1)10到(0.9)10这9个十进制小数的浮点代码,发现一个有趣的现象,从而得出两条结论。首先计算(0.1)10的浮点代码,得到一个无限循环的二进制小数,(0.1)10=(0.000110011……)b,用有限位无法表示无限循环小数,因此(0.1)10无法用IEEE754浮点数精确表示。同样,也可以计算得到:(0.2)10=(0.00110011……)b,(0.4)10=(0.011001100……)b(0.8)10=(0.11001100……)b,(0.6)10=(0.100110011……)b由于(0.3×2)10=(0.6)10=(0.100110011……)b,可以得出:(0.3)10=(0.0100110011……)b

同理,由于(0.7×2)10=(1.4)10=(1.011001100……)b,(0.9×2)10=(1.8)10=(1.11001100……)b可以得出:(0.7)10=(0.1011001100……)b, (0.9)10=(0.111001100……)b根据以上计算结果,可以看出这8个数都无法用IEEE754浮点数精确表示。由此得出以下两个有趣的结论:结论1:十进制小数0.1~0.9这9个小数中,只有0.5可以用IEEE754浮点数精确表示:(0.5)10=(0.1)b。结论2:任何以下形式的十进制实数都无法用IEEE754浮点数精确表示:

(nini-1…n1n0.1)10、(nini-1…n1n0.2)10、(nini-1…n1n0.3)10、(nini-1…n1n0.4)10(nini-1…n1n0.6)10、(nini-1…n1n0.7)10、(nini-1…n1n0.8)10、(nini-1…n1n0.9)10

综上所述,C/C++语言编程时不能直接用等号或不等号判浮点变量是否与浮点类型数据相等,工程实践中,软件设计人员应设法将其转化成区间判断的形式进行判断。

参考文献:

[1] 王成.计算机组成原理[M].北京:清华大学出版社,2004.

[2] 胡越民.计算机组成与系统结构[M].北京:电子工业出版社,2002.

[3] 朱亚超.基于IEEE754的浮点数存储格式分析研究[J].计算机与信息技术,2006(9).

[4] DAVID A PATTERSON,JOHN L puter organization & design:the hardware/software interface [M].第2版.北京:机械工业出版社,1999:275-321.

[5] IEEE standard for binary floating-point arithmetic[J].ANSI/IEEE Standard 754-1985.Institute of Electrical and Electronics Engineers.August 1985.

上一篇:云计算对高等教育信化建设的影响 下一篇:基于边界处理改进的局部信息粒子群优化算法