为什么不能直接使用if来判断浮点数的大小

发布时间 2023-06-06 15:03:41作者: 小景生生

浮点数为什么不能判等?

浮点数存储方式

  由于数据在计算机中的存储都是以二进制的方式存储,那么十进制中的小数当然也是以二进制的方式存储。

C语言中,对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit, double数据占用64bit。无论是单精度还是双精度在存储中都分为三个部分:

  • 符号位(Sign) : 0代表正,1代表为负
  • 指数位(Exponent): 用于存储科学计数法中的指数数据,并且采用移位存储
  • 尾数部分(Mantissa):尾数部分

首先看这段代码:

 

再看下面这段代码,请问输出结果是什么:

func main() {
    a, b := 1.5, 1.3
    fmt.Println(a-b == 0.2)
    fmt.Println(a-b > 0.2)
}
  • 第1行输出,很多同窗应该能知道,浮点数不能直接比较,结果会是false。
  • 第2行输出,结果会是true。

若是上面的示例没惊奇到你,那么咱们再看这个示例:

func main() {
    a := float32(16777216)
    fmt.Println(a == a+1)
    a = math.MaxFloat32
    fmt.Println(a == a-float32(math.MaxUint32))
}

 

很神奇,上面这段代码的输出结果是"true true",即咱们的代码认为16777216 = 16777216+1,并且最大的float32数减去最大的32位整形(42亿多)结果竟然仍是等于原值。编程语言

上述“违反常理”问题的缘由与浮点数的计算机表示方式有关。

先来一段通用的,可以较浮点数的方法:

/*
    f1/f2为待比较的参数,degree为数据的精度
    好比:cmpFloat32(1.5, 1.3, 0.000001)返回结果为1
    注意:精度degree须要根据实际场景自行调整
*/
func cmpFloat32(f1, f2, degree float32) int {
    if f1 + degree > f2 && f1 - degree < f2 {
        return 0    // 相等
    } else if f1 < f2 {
        return -1   // f1比f2小
    } else {
        return 1    // f1比f2大
    }
}

 

小数的二进制表示

一个小数能够分为3部分:整数部分、小数点、小数部分。

以10.75为例,十进制的转换规则是:10.75 = 1*10^1 + 0*10^0 + 7*10^-1 + 5*10^-2。注意,小数部分取的是模数的负的指数,即模数的指数的倒数。

(对于二进制,转换思路是同样的:10.75 = 1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 + 1*2^-1 + 1*2^-2,因而10.75的二进制就是1010.11 )
对于一个复杂的小数,上述转换公式很难直接写出,因此下面介绍一种方便计算的思路:

  • 整数部分,你们很容易想到的编程思路:不断除以2并对2取余获得的0或1便是对应位的二进制值,当整数部分为0时中止。
  • 小数部分,则正好与整数相反,不断乘以2,溢出部分会是0或1,这正是小数的二进制值,当小数部分为0时中止。

以10.125为例,整数部分咱们直接给出是1010:

0.125 * 2 = 0.25,整数部分溢出为0,则表示1010.0
0.25 * 2 = 0.5,溢出仍是0,1010.00
0.5 * 2 = 1.0,溢出是1,1010.001
剩余小数部分为0,计算中止,最终结果10.125的二进制表示是1010.001

 

浮点数精度和精度丢失,为何浮点数是近似表示?

关于浮点数的精度问题,咱们能够经过分析开篇的1.5-1.3 != 0.2案例来解释。
如今咱们将1.5, 1.3, 1.5-1.3, 0.2用前面的打印代码打印出二进制:

浮点数[1.5000]的二进制为[0-01111111-10000000000000000000000]
浮点数[1.3000]的二进制为[0-01111111-01001100110011001100110]
浮点数[0.2000]的二进制为[0-01111100-10011001100110011010000] // 这段是1.5-1.3
浮点数[0.2000]的二进制为[0-01111100-10011001100110011001101] // 这段是0.2

 

首先,咱们关注下第2行,十进制1.3转换成二进制后是1.01001100110011001100110...,注意后面是循环的,实际上这会是个无限循环小数。一样的,0.2转换成二进制,也是无限循环小数。
当出现无限循环时,须要在没法存储的位上截断掉,此时相似于十进制的四舍五入,二进制下采用0舍1入。咱们观察1.3,紧随后面的截断位应该是0,因此舍去。但0.2的截断处前面1位应该是0,后面1位是1,因而进1,前面的0变成了1。
这就是为何浮点数是近似表示,由于十进制转成二进制后算不尽,有可能出现无限循环小数,此时计算机会将数字截断并做0舍1入取近似值。
相似0.1/0.2/0.3/0.4/0.6/0.7/0.8/0.9这几个数字,都是无限循环的,有兴趣的同窗能够本身用前文的方法计算一遍。

接下来咱们看看浮点数的精度问题。

浮点数[0]的二进制为[0-00000000-00000000000000000000000]
浮点数[0.000000000000000000000000000000000000000000001]的二进制为[0-00000000-00000000000000000000001]
浮点数[16777216]的二进制为[0-10010111-00000000000000000000000]
浮点数[16777217]的二进制为[0-10010111-00000000000000000000000]

 

上面第2行是float32能表示的最接近0的小数了,再小的话表示不了。此时精度很是高。
但随着数字离0愈来愈远,即除去符号位,数字愈来愈大,精度会慢慢丢失,缘由是指数位能表示的小数点偏移量最大12

 

为何浮点数不能直接比较?

这个问题跟精度问题是相似的,也是截断引发的。
仍是以1.5-1.3为例:

浮点数[1.5000]的二进制为[0-01111111-10000000000000000000000]
浮点数[1.3000]的二进制为[0-01111111-01001100110011001100110]
浮点数[0.2000]的二进制为[0-01111100-10011001100110011010000] // 这段是1.5-1.3
浮点数[0.2000]的二进制为[0-01111100-10011001100110011001101] // 这段是0.2

 

咱们将上述浮点的二进制表示转换为二进制小数:

1.5:    1.10000000000000000000000 // 固定在数据域前面添加'1.',下同
1.3:    1.01001100110011001100110 // 无限循环,后面截断了
1.5-1.3:0.00110011001100110011010000 // 注意指数域,小数点左移3位
0.2:    0.00110011001100110011001101

 

不难算出,第3行+第2行,正好等于第1行(注意遇2则向高位进1位)。
因为1.5和1.3的精度不足,相减后精度没有0.2的精度高,因此上面能够明显看出1.5-1.2和0.2相比,末尾的精度丢失了。
这就是浮点数不能直接比较的缘由。

 

再看看这部分浮点格式转换

 

回顾

介绍完上面的理论知识,再回过头来看第一个案例描述中的代码:

double la = 1.345;
double lb = 1.123;
double l_expected = 2.468;
double l_sum = la + lb;

 

调试这段程序,在内存中查看到la的IEEE754浮点代码是0x3ff5 851e c000 0000,将这个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

按照同样的计算方法,得出lb的值实际上为1.123 0000257492065,

因此la+lb的值实际上为2.468000 054359436,而事实上 l_expected的值为2.4679999351501465。

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

解决措施

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