对浮点数精度丢失的一点研究

什么叫精度丢失

计算不准确

我们看看如下代码:

1
2
3
4
5
float f;
for(int i=0;i<100;i++){
f+=0.1f;
}
printf("%f\n",f);

上述代码中f的结果本应该是10.000000,但是却输出10.000002,如下图所示。这就是浮点数计算不准确的现象,即精度丢失。

我们打印小数点后更多位数,能看得更清楚。printf("%.20f\n",f);,得到的结果如下:

我们通过gdb调试看看存储在内存中的数据是什么样子的:

我们将其转化为二进制是0100 0001 0010 0000 0000 0000 0000 0010

将这个IEEE745标准的二进制浮点数转化为十进制为:10.000001907348633。

为什么会出现这种情况呢?这就要涉及到我们下一个知识点,保存不准确。

保存不准确

我们看看如下代码:

1
2
float f;
printf("%.18f\n",f);

上述代码中打印结果如下所示:

我们来看看内存中如何表示这个数据的:

将这个二进制数转化为十进制数为:

可以看到某些浮点数在写入内存的过程中就存在误差,当然这是无法避免的。这个跟十进制中的无穷小数是一个道理。

浮点数判断大小或者排序

浮点数相等

当判断两个浮点数是否相等时,我们应该使用如下语句:

1
2
3
if (fabs(a - b) < EPSILON) {
//执行当两个浮点数 a 和 b 相等时的操作
}

其中EPSILON为精度,例如0.000001。

float可以作为map中的key吗(Go语言)

从语法上看,Go语言中只要是可比较的类型都可以作为key。除开slice,map,functions这几种类型,其他类型都是OK的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 ==!= 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。当然,任何类型都可以作为value,包括map类型。

我们来看如下例子:

1
2
3
4
5
6
7
8
9
10
func main() {
m := make(map[float64]int)
m[1.4] = 1
m[2.4] = 2
for k, v := range m {
fmt.Printf("[%v, %d] ", k, v)
}
fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001])
fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])
}

程序输出如下:

1
2
3
[2.4, 2] [1.4, 1] 
k: 2.400000000001, v: 0
k: 2.4, v: 2

我们发现2.4000000000000000000000001存在value。当用float作为key的时候,先要将其转成uint64类型,再插入key中。通过以下函数实现:

1
2
3
// Float64frombits returns the floating point number corresponding
// the IEEE 754 binary representation b.
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }

也就是我们上述所说的IEEE 754规定的格式。

我们再来输出点东西:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
"math"
)
func main() {
m := make(map[float64]int)
m[2.4] = 2
fmt.Println(math.Float64bits(2.4))
fmt.Println(math.Float64bits(2.400000000001))
fmt.Println(math.Float64bits(2.4000000000000000000000001))
}

输出结果如下:

1
2
3
4612586738352862003
4612586738352864255
4612586738352862003

转成十六进制如下:

1
2
3
0x4003333333333333
0x4003333333333BFF
0x4003333333333333

由此可见,由于精度受限,2.4和2.4000000000000000000000001经过 math.Float64bits() 函数转换后的结果是一样的。自然,二者在 map 看来,就是同一个 key 了。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!