前些天参加深信服面试,面试官问了这样一个问题:浮点数的大小比较为什么不能用等号?那时就没回答好,因为自己一直把“浮点数大小比较不能够用等号”当做默认事实而没有去深究其背后原理。本文将试着解释这个事实。
小数在计算机中的表示
计算机中是如何存储和表达数字的?对于整数,情况比较简单,直接按照数学中的进制转换方法处理即可,即连续除以2取余。这并不是难点,真正的难点在于小数是如何转换为二进制码(即浮点数)的(注意区别小数和浮点数)。
当然,从数学的角度来讲,十进制的小数可以转换为二进制小数(整数部分连续除2,小数部分连续乘2),例如125.125D=1111101.001B,但问题在于计算机根本就不认识小数点“.”,更不可能认识1111101.001B。那么计算机是如何处理小数的呢?
历史上计算机科学家们曾提出过多种解决方案,最终获得广泛应用的是IEEE 754标准中的方案,目前最新版的标准是IEEE std 754-2008。该标准提出数字系统中的浮点数是对数学中的实数(小数)的近似,同时该标准规定表达浮点数的0、1序列被分为三部分(三个域):
以32位单精度浮点数为例,其具体的转换规则是:首先把二进制小数(补码)用二进制科学计数法表示,比如上面给出的例子1111101.001=1.111101001*2^6。符号位sign表示数的正负(0为正,1为负),故此处填0。exponent表示科学计数法的指数部分,请务必注意的是,这里所填的指数并不是前面算出来的实际指数,而是等于实际指数加上一个数(指数偏移),偏移量为2^(e-1)-1,其中e是exponent的宽度(位数)。对于32位单精度浮点数,exponent宽度为8,因此偏移量为127,所以exponent的值为133,即10000101。之后的fraction表示尾数,即科学计数法中的小数部分11110100100000000000000(共23位)。因此32位浮点数125.125D在计算机中就被表示为01000010111110100100000000000000。
对于32位单精度浮点数,sign是1位,exponent是8位(指数偏移量是127),fraction是23位。对于64位双精度浮点数,sign是1为,exponent是11位(指数偏移量是1023),fraction是52位。
用程序可以看出小数在计算机中是如何表示的(即浮点数):
1 #include <iostream> 2 #include <bitset> 3 using namespace std; 4 5 int main() 6 { 7 float input; 8 cin >> input; 9 unsigned long long nMem = *(unsigned long long *)&input; 10 bitset<32> myBit(nMem); 11 cout << myBit << endl; 12 13 return 0; 14 }
程序运行结果:
浮点数的大小比较为什么不能用等号?
下文将从几个方面来探索这个问题:
浮点数精度不同
请见程序:
1 #include <iostream> 2 #include <cmath> 3 using namespace std; 4 5 int main() 6 { 7 float a = (float)0.1; 8 float b = (float)0.1; 9 if(a == b) 10 cout << "a == b" << endl; 11 else 12 cout << "a == b" << endl; 13 14 float c = (float)0.1; 15 double d = (double)0.1; 16 if(c == d) 17 cout << "c == d" << endl; 18 else 19 cout << "c != d" << endl; 20 21 float e = (float)0.1; 22 float f = (double)0.1; 23 if(abs(e - f) < 0.0001) 24 cout << "e == f" << endl; 25 else 26 cout << "e != f" << endl; 27 28 return 0; 29 }
程序运行结果如下:
从该例子可以看出,对于同一个小数,当用不同精度表示时,结果是不一样的,不能直接用等号比较大小。
寄存器与内存表示浮点数精度不同
按照博文计算机中基本类型float值表示和大小比较问题的说法:
即使在精度相同的情况下,比较也可能会出问题。因为在运算过程中会将内存(或高速缓存)中的值加载到CPU浮点寄存器(80 bit扩展精度)中,然后再进入CPU浮点计算单元进行计算,计算结果写回浮点寄存器,然后写回内存(或高速缓存)。从内存到浮点寄存器,浮点数的精度会扩展,从浮点寄存器到内存,浮点数的精度会降低(精度扩展通常没问题,但如果精度降低了,很可能值会发生变化,出现截断),而浮点运算的结果由于下面还要使用所以暂时保存在浮点寄存器中留待下次使用(没有及时写回内存,这是一种优化策略),从而导致数据并不是内存中和内存中的数据比较而是浮点寄存器中的值和内存中的值进行比较,而无论内存中是float类型还是double类型,其精度和浮点寄存器精度都不相同,从而导致比较结果是不相等。
详细的可参考维基百科条目Extended precision。