前言
0.1+0.2=0.3,这几乎是常识,但在计算机世界里却不一定成立,打开python,输入0.1+0.2,最终它输出的是:0.30000000000000004。这个结果使我们隐约觉得浮点数并不简单。
确实不大简单,现实世界的数是无限的,而计算机的存储空间是有限的,所以只能以有限的空间表示数的子集。比如整型,即使64位的内存,也只能表示2^64个数。浮点数还包括小数,小数也是无限的组合,比起整型的存储更加麻烦。
浮点数的存储格式
现在几乎所有语言都支持 IEEE 754 的二进制浮点格式。在说明这个格式之前,先看看科学计数法,有一个这样的数:327.849,表示成科学计数法是:+ 3.27849 * 10^2,这个表示法有三个部分:
+是符号3.27849是有效数- 10的2次方上的
2是指数
二进制也有一样的科学计数法,如:
1001.0101 可以表示成 + 1.0010101 * 2^3
0.001011 可以表示成 + 1.011 * 2^-3
方法是将小数点移动到最左边的1之后,向左移多少位,就是2的几次方;向右移多少位,就是2的负几次方。这种表示方法也叫正规化,这种叫正规数(等一下还有一种叫非正规数)。
- +为符号
- 1.0010101为有效数
- 3 为指数
IEEE 754的浮点数格式遵循科学计数法的表现方式,以32位浮点数为例(下面默认都是32位浮点数):

- 第31位表示符号,简写为S,0为正,1为负
- 接下来的8位与指数有关,简写为E,因为是8位,所以E的范围是[0, 255],其中0和255代表特殊值,剩下的值减去127得到指数:E - 127,即指数的范围是[-126, 127]
- 剩下的23位表示有效数的小数部分,也叫做尾数,我们简写为M,真正的有效数是 1.M
用下面公式计算出真正的浮点数值:
(-1)^S * 1.M * 2^(E-127)
以上面图中二进制为例,一步步计算最终的值:
// E等于十进制的124
E = 01111100 = 124
// 代入公式
(-1)^0 * 1.01000000000000000000000 * 2^(124 - 127)
// 继续
1 * 1.01 * 2^-3
// 继续:下面就是二进制的最终值
0.00101
// 继续
0 + 0/2 + 0/4 + 1/8 + 0/16 + 1/32
// 继续
0 + 0 + 0 + 0.125 + 0 + 0.03125
// 得到结果
0.15625
另一个视角的浮点数
上面我们学会了如何将二进制格式的浮点数,转换成我们能看懂的实数。那么我们如何将0.2, 0.1转化成二进制格式呢?还用上面的方法逆推好像有点麻烦,幸好我通过科学上网,找到了另一个方法,这种方法也让我们用另一个视角看待浮点数的规律。
将指数部分划成一个个区域,尾数就是区域里的偏移,像下面这样:

区域的范围越来越小,但区域里的浮点数数量相同,都是2^23个。区域范围如下表示:
[0.0625, 0.125), [0.125, 0.25), [0.25, 5), [0.5, 1), [1, 2), [2, 4), [4, 8), [8, 16)
写成通用的形式就是:
[2^n, 2^(n+1)),n就是指数,取值范围为[-126, 127)
现在试试看怎么把0.2转换成二进制格式:
- 由于是正数,所以S=0
- 0.2是在[0.125, 0.25)这个范围内,即[2-3, 2-2)。
- 取-3为作指数,通过上面关系得到:E = 指数 + 127 = -3 + 127 = 124
- (0.2 - 0.125) / (0.25 - 0.125) = 0.6,这个意思是0.2大概在区域的60%处。
- 2^23 * 0.6 = 8388608 * 0.6 = 5033164.8 四舍五入得到5033165,这就是尾数M。
现在我们得到S=0, E=124, M=5033165,转化成二进制就是:
0 01111100 10011001100110011001101
怎么看这个二进制确切的大小呢?有一个很棒的网站:float.exposed,可以很直观的看到浮点数的数值:

结果是:0.20000000298023223877,把Significand那一栏调成5033164,会得到:0.199999988079071044922,这让我们明白一件事:0.2根本没法准确表示,只能得到近似值,这是IEEE754这种格式的问题。
- 0.1的单精度最接近的值为:
0.100000001490116119385。 - 0.3的单精度最接近的值为:
0.300000011920928955078。 - 0.3的双精度最接近的值为:
0.300000000000000044409,这就是为什么最前面用python计算会得到这个值。
浮点数的分布
把浮点数等额放到多个区域里,这种做法似乎更符合人的思维逻辑,它也展现了一个规律:浮点数不是均匀分布的,而是数值越小,浮点数越多。虽然每个区域的浮点数数量相等,但每个区域的范围是不同的,比如
[0.5, 1), [0.25, 0.5), [0.125, 0.25) ....
越接近于0,区域的范围越小
很容易得出:S=0, E=[0, 126]时浮点数为[0, 1);S=1,E=[0, 126]时浮点数为(-1, 0]:
- (-1, 1)这个范围占了单精度浮点数近一半的数量
- 剩下的一半延实数轴两边伸展,越远数量越稀疏。
- 到E=150的时候,浮点数间的间隔已经达到1。
- 到E=151的时候,浮点数间的间隔已经达到2.
- 到E=152的时候,浮点数间的间隔已经达到4. 以此类推。。
所以,浮点数值越大的时候,其在连续整数之间的分布越稀疏,浮点能够连续表示的整数范围为:
- 32位浮点数能够表示[-224, 224]的整数,即[-1.6777216×107, 1.6777216×107];所以其精度范围是小数点7位。
- 64位浮点数能够表示[-253, 253]的整数,即[-9.007199254740992×1015, 9.007199254740992×1015];所以其精度范围是小数点15位。
另一个图是浮点数值和指数的关系:

横轴是指数,竖轴是浮点数值,随着指数越大,数值也跟着越大,同时越离散。
特殊的浮点数值
有一些存储格式被预定义为特殊值,它们是:
0值
0:当E=0,M=0时,浮点数表示0,因为S的存在,浮点数会出现两个0的存储格式(以单精度为例):
+0 = 0 00000000 00000000000000000000000
-0 = 1 00000000 00000000000000000000000
无穷(Infinity)
当E=255,M=0时,表示无穷,同样有正无穷和负无穷:
+INFINITY = 0 11111111 00000000000000000000000
-INFINITY = 1 11111111 00000000000000000000000
无穷数有这样一些性质:
- 任何有穷数加上无穷,还是等于无穷,如:
var f = 1000.0 + math.Inf(1)
fmt.Printf("%f\n", f) // => +Inf
- 任何正有穷数乘以无穷,还是等于无穷;任何负有穷数乘以无穷,等于符号相反的无穷。
- 任何有穷数除以无穷,等于0。
总之,无穷数就是大到无法正常计算结果的数。
非数(NaN)
当E=255,M != 0时,称为非数:
0 11111111 00000000000000000000000 ~ 0 11111111 11111111111111111111111
1 11111111 00000000000000000000000 ~ 1 11111111 11111111111111111111111
非数也有很多性质,不过最值得提出来的是 NaN != NaN :)
Subnormals
前面说到浮点数的有效数是1.M,这种形式叫正规数。
当E=0,M!=0时,浮点数处于Subnormal的范围,它的表示方法是0.M,即前面的数是0,这种数也叫非正规数(denormal)。
定义非正规数的目的是:使最小正规数和0之间更平滑,换句话说能存储更多很小的数,比如下面的数:
0.00110001101001 * 2^(−126)
如果用正规的表示方式是:1.10001101001 * 2^(-129),指数部分已经超过E能表示的范围,所以按正规方式是没有办法存储的。但按非正规方式就可以:
- E = 0
- M = 00110001101001
- 公式写成
(-1)^S * 0.M * 2^(E-126)
这样就能表达很小的数了,不过这些数的精度要比正规数低,相当于牺牲精度为代价,消除最小正规数和0之间的差距。
作为程序员,特别是服务端程序员,我们只要明白浮点数不是均匀分布的,越大的浮点数精度越低,大多数实数在计算机中只能以近似值表示就差不多了。剩下的让浮点运算单元帮我们处理吧:)
最后淘了张很不错的图作为结束:

评论区