摘要:看了看资料,确认过眼神,还是我驾驭不了的浮点数。浮点数的内容似乎十分庞大,我一时还无法完全理解,因此本篇只记录IEEE标准的 float 的二进制表示方式,能够学会手动将浮点数的二进制与十进制相互转换即可。另外会学习一下其精度如何计算。
本篇大部分参考小土刀的:【读薄 CSAPP】壹 数据表示 一文。自己仅作摘录、整理和略微的总结。
IEEE 浮点数标准
IEEE标准中,浮点数以如下公式表示:
(
−
1
)
s
M
2
E
(-1)^sM2^E
(−1)sM2E
其中,s 为符号位,代表正负;M 通常是一个值在[1.0, 2.0)的小数,称为有效数字;E 为次方数。
这其实就是一个二进制的科学计数法。
但是,float 的二进制虽然分为三段,也确实对应 s、E、M,但却不一定相等。因为位数限制表达能力有限。
在计算机中,一个浮点数的二进制位划分如下,其中 s
对应符号位,exp
对应着 E(但不一定等于 E,因为位数限制表达能力有限),frac
对应着 M(但不一定等于 M,因为位数限制表达能力有限)。不同的位数就代表了不同的表示能力,也就是单精度,双精度,扩展精度的来源。如图所示:
现在对照着上图,就可以手动转换一个浮点数的二进制表示与十进制值了。
再列出我们计算浮点数的公式:
v
a
l
=
(
−
1
)
s
M
2
E
val=(−1)^sM2^E
val=(−1)sM2E
其中的E是一个偏移值: E = E x p − B i a s E = Exp - Bias E=Exp−Bias,即 E x p = E + B i a s Exp = E + Bias Exp=E+Bias:
- Exp 为二进制表示中的 exp 部分,为无符号数值;
- Bias 为偏移量,为了保证 exp 编码只需要以无符号数来处理:
- 单精度:127(Exp: 1…254, E: -126…127);
- 双精度:1023(Exp: 1…2046, E: -1022…1023);
公式中的 M,一定以1开头,也就是 M = 1. x x x M = 1.xxx M=1.xxx,其中的 xxx 就是图中的 frac 编码部分。当 f r a c = 000.00 frac=000.00 frac=000.00 的时候值最小( M = 1.0 M=1.0 M=1.0),当 f r a c = 111...1 frac=111...1 frac=111...1 的时候值最大( M = 2.0 − ϵ M=2.0−ϵ M=2.0−ϵ),也就是说开头的 1 是『免费附送的』,并不需要实际的编码位。
有了以上内容,就可以完成绝大多数浮点数的二进制表示与十进制值的相互转换了(注意不是全部,具体原因下下节说明)。
下面举个例子。
浮点数的二进制表示与十进制值的相互转换:
float F = 15213.0; // 32位单精度浮点数
1521 3 10 = 11101101101101 2 = 1.1101101101101 2 ∗ 2 13 15213_{10} = 11101101101101_2 = 1.1101101101101_2*2^{13} 1521310=111011011011012=1.11011011011012∗213
下面来看这个浮点数的二进制表示的三部分分别是多少:
- s : 0,表示这是一个正数;
- exp : 10001100,即: 13 + 127 = 140 = 1000110 0 2 13 + 127 = 140 = 10001100_2 13+127=140=100011002;
- frac : 11011011011010000000000,即: 1.1101101101101 2 ∗ 2 13 1.1101101101101_2*2^{13} 1.11011011011012∗213 中的 1.1101101101101 1.1101101101101 1.1101101101101的小数部分,后边记得用0补齐23位;
所以 15213 编码出来的浮点数为:
0 10001100 11011011011010000000000
s exp frac
反过来,对于一个浮点数,比如 01000110010001110101100000000000 01000110010001110101100000000000 01000110010001110101100000000000,
拆分为三部分:
0 10001100 10001110101100000000000
s exp frac
解析三部分内容:
- s : 0,表示这是一个正数;
- exp : 10001100,有: 1000110 0 2 = 140 = 13 + 127 10001100_2 = 140 = 13 + 127 100011002=140=13+127;
- frac : 10001110101100000000000;即: 1.1101101101101 2 ∗ 2 13 1.1101101101101_2*2^{13} 1.11011011011012∗213 中的 1.1101101101101 1.1101101101101 1.1101101101101的小数部分,后边记得用0补齐23位;
所以三部分加起来就是:
01000110010001110101100000000000
=
1.100011101011
2
∗
2
13
=
1100011101011
0
2
=
12758
01000110010001110101100000000000 = 1.100011101011_2*2^{13} = 11000111010110_2 = 12758
01000110010001110101100000000000=1.1000111010112∗213=110001110101102=12758
本节内容并未涵盖所有浮点数,下面就来解释为什么是这样。
规范化值、非规范化值以及特殊值
前一节说明的都是规范化值的情况,也就是前边提到的绝大部分情况。那么为什么不是所有情况?规范化值,以及与之对应的非规范化值,还有特殊值分别是什么?
在 e x p ≠ 000 … 0 exp≠000…0 exp=000…0 和 e x p ≠ 111 … 1 exp≠111…1 exp=111…1 时,表示的其实都是规范化的值,为什么说是规范化呢?这里只需要大概知道因为实数轴上原来连续的值会被规范到有限的定值上并且这些定值之间的间距是不一样的。
当 e x p = 000 … 0 exp=000…0 exp=000…0 的时候,值是非规范化的,意思是,实数轴上原来连续的值会被规范到有限的定值上,这定值之间的间距也是一样的。
再次列出我们计算浮点数的公式:
v
a
l
=
(
−
1
)
s
M
2
E
val=(−1)^sM2^E
val=(−1)sM2E
和前面不同的是:
E
=
1
−
B
i
a
s
E=1−Bias
E=1−Bias
而且
M
=
0.
x
x
x
…
x
M=0.xxx…x
M=0.xxx…x,不是以 1 开头了。
当 e x p = 000 … 0 exp=000…0 exp=000…0 且 f r a c = 000 … 0 frac = 000…0 frac=000…0 时,表示 0,而且因为符号位的缘故,实际上是有 +0 和 -0 两种的。而在 e x p = 000..0 exp=000..0 exp=000..0 且 f r a c ≠ 000 … 0 frac≠000…0 frac=000…0 时,数值是接近 0 的,并且间距是一致的,间距为 2 − 23 2^{-23} 2−23(单精度)或 2 − 52 2^{-52} 2−52(双精度)或 2 − 63 2^{-63} 2−63(扩展精度)。
至于特殊值,是当 e x p = 111 … 1 exp=111…1 exp=111…1 的时候,用于表示一些特殊值。
当 e x p = 111 … 1 exp=111…1 exp=111…1 且 f r a c = 000 … 0 frac = 000…0 frac=000…0 时,表示 ∞ ∞ ∞,而且因为符号位的缘故,实际上是有 + ∞ +∞ +∞ 和 − ∞ −∞ −∞ 两种的。那些会溢出的操作就会用这个来表示,比如 1.0 / 0.0 = − 1.0 / 0.0 = + ∞ 1.0/0.0=−1.0/0.0=+∞ 1.0/0.0=−1.0/0.0=+∞, 1.0 / − 0.0 = − ∞ 1.0/−0.0=−∞ 1.0/−0.0=−∞;
而在 e x p = 111 … 1 exp=111…1 exp=111…1 且 f r a c ≠ 000 … 0 frac ≠ 000…0 frac=000…0 时,我们认为这不是一个数值(Not-a-Number,NaN),用来表示那些没办法确定的值,比如 s q r t ( − 1 ) sqrt(−1) sqrt(−1), ∞ − ∞ ∞−∞ ∞−∞, ∞ × 0 ∞×0 ∞×0。
上述各种情况对应到数轴中的情况,如下图所示:
原文中有举例说明,还蛮清晰的,有兴趣的可以去原文看一眼,一定会有收获的。
再贴一次原文链接:【读薄 CSAPP】壹 数据表示
聊完了表示,下面介绍浮点数的数值表示能力,也就是其范围与精度。
浮点数的范围与精度计算
先放结果:
-
float的范围基本由exp位决定,为 − 2 128 ∼ + 2 128 -2^{128} \sim +2^{128} −2128∼+2128,也即 − 3.40 ∗ 1 0 38 ∼ + 3.40 ∗ 1 0 38 -3.40*10^{38} \sim +3.40*10^{38} −3.40∗1038∼+3.40∗1038;
-
double的范围基本由exp位,为 − 2 1024 ∼ + 2 1024 -2^{1024} \sim +2^{1024} −21024∼+21024,也即 − 1.79 ∗ 1 0 308 ∼ + 1.79 ∗ 1 0 308 -1.79*10^{308} \sim +1.79*10^{308} −1.79∗10308∼+1.79∗10308;
-
float的精度由frac位决定,约为 1.19 × 1 0 − 7 1.19×10^{−7} 1.19×10−7
-
double的精度由frac位,约为 1.22 × 1 0 − 16 1.22×10^{−16} 1.22×10−16
浮点数的上界由指数位决定,既好理解,也容易计算。但是它的精度计算却很有趣,所以综合了一些别人写的东西之后,找出了我最喜欢的一种解释。主要内容来自:为什么单精度浮点数的精度是7位
首先,计算精度的基础是frac位。以单精度浮点数float
为例:
float
的frac位有23位,所以表示其大小的基本单位为
2
−
23
=
0.00000011920928955078125
2^{-23} = 0.00000011920928955078125
2−23=0.00000011920928955078125,刚开始我和博主的想法一样,这后面明显有这么多小数,为什么说精度只有7位?
我们可以这么理解,如果以整数做类比,那么float
里的
2
−
23
2^{-23}
2−23就相当于整数中的1,即所有float
中的数都是由若干倍的
2
−
23
2^{-23}
2−23组合而成的。如果忽略舍入算法,只取小数点后面7位的计算结果,可以看到相当有趣的现象:
0.00000011920928955078125 * 1 = 0.0000001
0.00000011920928955078125 * 2 = 0.0000002
0.00000011920928955078125 * 3 = 0.0000003
0.00000011920928955078125 * 4 = 0.0000004
0.00000011920928955078125 * 5 = 0.0000005
0.00000011920928955078125 * 6 = 0.0000007
0.00000011920928955078125 * 7 = 0.0000008
0.00000011920928955078125 * 8 = 0.0000009
0.00000011920928955078125 * 9 = 0.0000010
有趣的事情发生了,可以看到运算结果中是没有0.0000006的。
运算的原始结果如下:
0.00000011920928955078125 * 1 = 0.00000011920928955078125
0.00000011920928955078125 * 2 = 0.00000023841857910156250
0.00000011920928955078125 * 3 = 0.00000035762786865234375
0.00000011920928955078125 * 4 = 0.00000047683715820312500
0.00000011920928955078125 * 5 = 0.00000059604644775390625
0.00000011920928955078125 * 6 = 0.00000071525573730468750
0.00000011920928955078125 * 7 = 0.00000083446502685546875
0.00000011920928955078125 * 8 = 0.00000095367431640625000
0.00000011920928955078125 * 9 = 0.00000107288360595703125
如果按照四舍五入处理,则会发现没有 0.0000004 0.0000004 0.0000004。
总之,浮点数在小数点后的第七位是没办法表示所有数的,所以一般说float的精度在小数点后6~7位。
此外,还有一种理解也很有趣,并且好记:
23位表示的十进制的数最大为2^23=8388608,8388608是一个7位数字,但是并不能完全包括所有7位有效位,所以是6-7位有效位。
最后加一条,权当彩蛋:浮点数不满足结合律(因为舍入会造成精度损失)。
在拜读了小土刀的文章之后感觉获益匪浅,加上其他一些博客的辅助,算是对浮点数有了一个大概的认识,更进一步的内容可能就要留到以后了。