casyup.me@outlook.com

0%

other/float

前言

在看到<深入理解计算机系统>的浮点数时, 第一想法是:

  • 无法精确保存大多数浮点数
  • 精度上的缺失

零值的比较

很多面试题都会考一道浮点数零值比较的题(一般是单精度, 双精度太长了)

我觉得答案应该是:

1
f > -0.000001f && f < 0.000001f

这个题的核心在于 float 什么时候缺失精度

这里我没有使用等于, 因为我认为 0.000001f 和 -0.000001f 并不算缺失了精度

(百度上的答案是有等于的, 我很怀疑这个答案, 甚至有人还用的是 0.00001 (눈_눈) )

(而google上我好像没有找到类似的答案, 再根据编译器给我的结果, 我只能如此推断)

下面是我推断的依据:

1
2
3
printf("%f\n", 0.000001f);
printf("%f\n", 0.0000006f);
printf("%f\n", 0.0000005f);

你觉得上面会打印什么呢? 输出结果是:

1
2
3
0.000001
0.000001
0.000000

这也就是我认为 0.000001 它并未损失精度的原因, 既然未损失, 那么就不能当做 0 值来对待

(再次看不起百度上的解答(눈_눈), 不过… 万一是cas错了呢?)

(损失精度还有更精确的 0.00000055f, 这个数字也被认为是 0.000001)

数字的精度取决于有多少位表示

上面看到了6为精度的情况, 他准确表示了0.1 (虽然它把 0.0000006f 当做了0.1…)

我们来看看其他的结果, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
printf("%f\n", 255.1f);
printf("%d\n", 254.2f == 254.200001f);
printf("%d\n", 254.2f == 254.200002f);
printf("%d\n", 254.2f == 254.200003f);
printf("%d\n", 254.2f == 254.200004f);
printf("%d\n", 254.2f == 254.200005f);
printf("%d\n", 254.2f == 254.200006f);
printf("%d\n", 254.2f == 254.200007f);
printf("%d\n", 254.2f == 254.200008f);
printf("%d\n", 254.2f == 254.200009f);
printf("%d\n", 254.2f == 254.199999f);
printf("%d\n", 254.2f == 254.199998f);
printf("%d\n", 254.2f == 254.199997f);
printf("%d\n", 254.2f == 254.199996f);
printf("%d\n", 254.2f == 254.199995f);
printf("%d\n", 254.2f == 254.199994f);
printf("%d\n", 254.2f == 254.199993f);
printf("%d\n", 254.2f == 254.199992f);
printf("%d\n", 254.2f == 254.199991f);
printf("%d\n", 254.2f == 254.199990f);

你觉得这次又会输出什么呢?

1
255.100006 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1

(输出结果我做了缩减, 不然太长了)

也就是说, 除了

1
2
3
4
5
254.200005f
254.200006f
254.200007f
254.200008f
254.200009f

之外, 编译器认为它们都是相等的, 为什么呢?

精度再次缺失(我只能如此猜测), 因为整数的数字过大, 剩下的留给小数的位数不足以达到6位精度

所以这次的精度缩减到了5位, 而因为四舍五入(我只能再次如此猜测 (눈_눈))的关系

(其实说四舍五入有点不对, 应该是: 数字的二进制表示刚好进入了有效的区间)

一些能达到 254.20001 的数字被判段为不等, 而一些 254.19999 的数字又可四舍五入的关系被判断为相等

所以, 整数数字的大小会影响小数的精度 (我感觉我在说废话 (눈_눈)), 而当整数过大时, 比如 0x7fffffffff

所有的小数精度全都会缺失(unsigned float 可能是例外, 不过不影响结论)

下面我又做了一次比较, 我将254换成了126, 输出结果是

0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0

emmm… 其实这次的有效精度还是接近5位

不过能够在5位之外, 能判断更多的数字了

(这个数字并未完全达到6位, 也许 0.000005 能判断到, 0.000004 却不能, 就像上面那样)

底层到底对我们的代码做了什么

又到了喜闻乐见的看汇编环节 ┑( ̄Д  ̄)┍

1
2
3
4
5
6
float f = 0.1f;
printf("%f\n", f);
printf("%f\n", f * 2);
float f2 = 0x7fff + 0.1f;
printf("%f\n", f2);
printf("%f\n", f2 * 2);

它在汇编中的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp // 依旧是开辟了16个直接的栈帧, 为什么不是8(我有两个float)?
movl .LC0(%rip), %eax // .long 1036831949
movl %eax, -4(%rbp) // 将变量放到了栈中
movss -4(%rbp), %xmm0 // 放到了浮点数寄存器中
cvtps2pd %xmm0, %xmm0 // emmm... PS2PD Single-Precision Double-Precision
// 它将两个单精度浮点数转化成了双精度, 这可不在我的预料之中 ∑( ̄□ ̄;)
movl $.LC1, %edi
movl $1, %eax
call printf
movss -4(%rbp), %xmm0
addss %xmm0, %xmm0
unpcklps %xmm0, %xmm0
cvtps2pd %xmm0, %xmm0
movl $.LC1, %edi
movl $1, %eax
call printf
movl .LC2(%rip), %eax // .long 1191181875
movl %eax, -8(%rbp)
movss -8(%rbp), %xmm0
cvtps2pd %xmm0, %xmm0
movl $.LC1, %edi
movl $1, %eax
call printf
movss -8(%rbp), %xmm0
addss %xmm0, %xmm0
unpcklps %xmm0, %xmm0
cvtps2pd %xmm0, %xmm0
movl $.LC1, %edi
movl $1, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

关键点在于 .LC1 和 .LC2, 他们的数字, 不过一点数字看不出什么, 需要多一些数据

1
2
3
1036831949 = 0.1f	= 3dcccccd
1045220557 = 0.2f = 3e4ccccd
1038174126 = 0.11f = 3de147ae

其中 0.1f 和 0.2f 相差 800000

0.1f 和 0.11f 相差 147ae1

emmmm… 想不出来, 或许我该再看看书

嗯 好的, 看完了 ( ̄ˇ ̄)

大概是这样的, 根据不同的位数安排, 计算的结果也有相应的不同

一个浮点数, 1位符号位S, 8位阶码E, 23位小数位M

其中又分为4种情况: 规格, 非规格, NaN(not a number?), 无穷大

(具体的细节请参考书中的介绍)

总之, 我们用书中的算法来检验一下这几个数字

首先 0.1f, 它的数字是 3dcccccd, 它是一个规格化数字

E = 123 - 127 = -4 , M = 5033165 / 8388735 +1

2的E次方 x M = 0.0999994337644472

emmm… 没错, 这是一个非常接近 0.1 的数字

(书中说到了小数的舍入, 简单来说是四舍五入, 同时向偶数舍入, 比如 1.245 它会向 1.24 舍入)

(再次很好奇一些需要极其精确的小数运算是如何做到的 (ー_ー?))

(像存储金额这样的小数精度, 特别是银行, 损失一个精度都很严重啊)

规格化用于表示一些比较大的数字, 而非规格化用于表示一些相对较小的数字

这里顺便再看一下失去精度的结果, 看看他是怎么计算的

1
printf("%f\n", 255.1f);

奇怪的是, 它有两个数字

1
2
3
111 .LC0:
112 .long 1073741824
113 .long 1081074483

可惜计算不出来, 这种格式是无穷大(不太明白 (눈_눈))

summary

简单来说, 其实也没有做笔记的必要 ┑( ̄Д  ̄)┍, 书上已经给了你答案

不过还好, 沉浸在思考的海洋中挺不错的(其实都快被淹死了 (눈_눈))

最后, 若无必要, 或者非常确信浮点数的范围, 否则不要使用单精度浮点数

如你所见, 单精度浮点数的范围很小, 一不小心还要失去精度

(这可能也是默认小数是双精度的原因)