这篇文章是csapp第二章第四节的阅读笔记。
接下来看看浮点数的表示。
1. 二进制小数
首先看看有小数部分的二进制数:
$$
b_mb_{m-1}\cdots b_1b_0.b{-1}b{-2}\cdots b_{-n}
$$
和无符号整数类似,这个二进制小数的值可以这样计算:
$$
b=\sum_{i=-n}^m2^i\times b_i
$$
比如:$101.11_2=1\times 2^2+0\times 2^1+1\times 2^0+1\times 2^{-1}+1\times 2^{-2}=5\frac{3}{4}$。
2. IEEE浮点数表示
使用上面的方法无法表达很大的数,因此使用类似$x\times 2^y$的形式来表示。
IEEE浮点数就是使用这种形式:$V=(-1)^s\times M\times 2^E$。
- $s$:表示符号(sign),如果$s=0$代表这个数是正数;$s=1$表示负数;
- $M$:尾数(significant),是一个二进制小数,范围是1到$2-\epsilon$或0到$1-\epsilon$;
- $E$:阶码(exponent),将整个数的值乘以$2^E$。
这样一个浮点数的二进制表示就有三个部分。下图表示的是C语言中单精度浮点数float
和双精度浮点数double
的二进制形式:
即使用一个bit表示$s$,k个bit表示$E$和n个bit表示$M$。其中单精度下$k=8,n=23$,双精度下$k=11,n=52$。
使用上面的格式编码的浮点数,对应的值有三种情况。
2.1 规格化的值
这是最普通的情况,这时exp
既不都是0也不都是1,这时,阶码$E=e-Bias$,其中$e$就是exp
对应的十进制数,而$Bias$是$2^{k-1}-1$,单精度下是127,双精度下是1023。
同时,尾数$M=1+f$,其中$f$就是frac
部分编码的 。这样$M$的二进制形式就是$1.f{n-1}f{n-2}\cdots f_0$。
2.2 非规格化的值
当exp
全是0时,这个浮点数就是非规格化的值。这时。阶码$E=1-Bias$,而尾数$M=f$。
非规格化的值可以表示0。规格化的值不能表示0因为尾数$M=1+f>0$。当exp
和frac
都是0时就表示0。同时$s$的存在,还可以表示-0.0和+0.0。
非规格化的值还可以表示非常接近0的值。
2.3 特殊值
当exp
都是1,的时候,可以表示一些特殊值。比如frac
都是0的时候,可以表示无穷。同时根据$s$的值可以表示正无穷和负无穷。
当frac
不全是0的时候,表示NaN
,not a number。这个值可以表示$\sqrt{-1}$和$\infty-\infty$的值。
3. 示例数字
下面的表格就演示了IEEE浮点数的表示方法(其中k=4,n=3,Bias=7):
Description | Bits | $e$ | $E$ | $2^E$ | $f$ | $M$ | $2^E\times M$ | Decimal |
---|---|---|---|---|---|---|---|---|
Zero | 0 0000 000 | 0 | -6 | $\frac{1}{64}$ | $\frac{0}{8}$ | $\frac{0}{8}$ | $\frac{0}{512}$ | 0.0 |
Smallest positive | 0 0000 001 | 0 | -6 | $\frac{1}{64}$ | $\frac{1}{8}$ | $\frac{1}{8}$ | $\frac{1}{512}$ | 0.001953 |
0 0000 010 | 0 | -6 | $\frac{1}{64}$ | $\frac{2}{8}$ | $\frac{2}{8}$ | $\frac{2}{512}$ | 0.003906 | |
0 0000 011 | $\frac{1}{64}$ | $\frac{3}{8}$ | $\frac{3}{8}$ | $\frac{3}{512}$ | 0.005859 | |||
$\vdots$ | ||||||||
Largest denormalized | 0 0000 111 | 0 | -6 | $\frac{1}{64}$ | $\frac{7}{8}$ | $\frac{7}{8}$ | $\frac{7}{512}$ | 0.013672 |
Smallest normalized | 0 0001 000 | 1 | -6 | $\frac{1}{64}$ | $\frac{0}{8}$ | $\frac{8}{8}$ | $\frac{8}{512}$ | 0.015625 |
0 0001 001 | 1 | -6 | $\frac{1}{64}$ | $\frac{1}{8}$ | $\frac{9}{8}$ | $\frac{9}{512}$ | 0.017578 | |
$\vdots$ | ||||||||
0 0110 110 | 6 | -1 | $\frac{1}{2}$ | $\frac{6}{8}$ | $\frac{14}{8}$ | $\frac{14}{16}$ | 0.875 | |
0 0110 111 | 6 | -1 | $\frac{1}{2}$ | $\frac{7}{8}$ | $\frac{15}{8}$ | $\frac{15}{16}$ | 0.9375 | |
One | 0 0111 000 | 7 | 0 | 1 | $\frac{0}{8}$ | $\frac{8}{8}$ | $\frac{8}{8}$ | 1.0 |
0 0111 001 | 7 | 0 | 1 | $\frac{1}{8}$ | $\frac{9}{8}$ | $\frac{9}{8}$ | 1.125 | |
0 0111 010 | 7 | 0 | 1 | $\frac{2}{8}$ | $\frac{10}{8}$ | $\frac{10}{8}$ | 1.25 | |
$\vdots$ | ||||||||
0 1110 110 | 14 | 7 | 128 | $\frac{6}{8}$ | $\frac{14}{8}$ | $\frac{1792}{8}$ | 224.0 | |
Largest normalized | 0 1110 111 | 14 | 7 | 128 | $\frac{7}{8}$ | $\frac{15}{8}$ | $\frac{1920}{8}$ | 240.0 |
Infinity | 0 1111 000 | — | — | — | — | — | — | — |
使用这种方式,可以保证随着二进制编码的递增,对应的浮点数的值也是递增的(负数反过来)。
这样对于浮点数的排序就可以直接对二进制编码进行排序了。
同时,对于非规格化我们使用$1-Bias$作为$E$,过渡到规格化时使用$e-Bias$,可以看到一个渐进递增的关系。
作为一个例子,下面演示把一个整数编码转换成对应的浮点数编码。
整数3510593的十六进制编码是
0x00359141
,把它转换成对应的单精度编码。首先二进制编码是:
0000,0000,0011,0101,1001,0001,0100,0001
;可以表示成$1.1010,1100,1000,1010,0000,1_2\times 2^{21}$;
那么位数M就是
1.1010,1100,1000,1010,0000,1
,需要右移21位,所以$E=21$;这是一个规格化的数,所以f就是
0.1010,1100,1000,1010,0000,1
;单精度下
frac
一共23位,所以f需要补0:0.1010,1100,1000,1010,0000,100
,这样就得到了frac
的编码;规格化下$E=e-Bias$,其中$Bias=127$,所以$e=21+127=148=1001,0100_2$,所以
exp
的编码就是1001,0100
;最后这是一个正数,所以$s=0$,得到对应浮点数的编码:
0100,1010,0101,0110,0100,0101,0000,0100
,十六进制就是0x4A564504
。
4. 舍入(Rounding)
因为精度和位数的限制,浮点数只能近似地表示实数运算,所以浮点数运算涉及到一个舍入的问题。下表展示了四种舍入的方式:
Mode | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
Round-to-even | 1 | 2 | 2 | 2 | -2 |
Round-toward-zero | 1 | 1 | 1 | 2 | -1 |
Round-down | 1 | 1 | 1 | 2 | -2 |
Round-up | 2 | 2 | 2 | 3 | -1 |
其中后三种都好理解,不好理解的是第一种:Round-to-even。
为什么要向偶数舍入呢?考虑这个场景。我们需要计算一组数的平均数,如果采用下面的三种舍入方法,那么对于同样符号(正负)的数来说,舍入的方向是一样的,这样在计算平均值的时候带来的偏差会比较大。相反,如果使用第一种,即向偶数舍入,那么一组数大概率50%的数向上舍入50%的数向下舍入,平衡了舍入带来的偏差。不过这种舍入方式计算的平均值也比真实值略高一些。
5. 浮点数运算
IEEE浮点数的运算是这样的,将两个浮点数当做看作两个实数$x,y$,对于加法就计算$Round(x+y)$。也就是先计算然后舍入。
不过由于有限的精度,可以不需要完全计算出结果,只要精度够了即可。
像有符号和无符号整数的加法一样,浮点数的加法也构成一个阿贝尔群,只不过由于舍入的存在,令$x+^fy$定义为$Round(x+y)$,需要考虑如下的特点:
由于溢出,$x+^fy$可能得到无穷;
可交换:$x+^fy=y+^fx$;
不可结合:$(x+^fy)+^fz$和$x+^f(y+^fz)$不一定相等。比如
(3.14+1e10)-1e10
的结果是0.0
,而3.14+(1e10-1e10)
的结果是3.14
;由于不可结合,因此编译器对于浮点数就不能做一些优化。比如:
x = a + b + c; y = b + c + d;
就不能优化成:
t = b + c; x = a + t; y = t + d;
浮点数加法满足单调性:如果$a\geq b$,那么对于不是
NaN
的x
来说,有$x+^fa\geq x+^fb$;
浮点数的乘法也具有和浮点数的加法一致的上述特征。同时,对于浮点数乘法来说,$a\ast ^fa\geq0$,这对于无符号和有符号整数的乘法来说不满足。
6. C语言中的浮点数
C语言中有两种浮点数:float
和double
,两者在和int
做类型转换时,原则如下:
int
转float
:不会溢出,但是会舍入;int
或float
转double
:没有精度损失;double
转float
:值可能会变成正无穷或负无穷,因为可表示范围小了,同时也会有舍入,因为精度也小了;float
或double
转int
:值会向0舍入。比如1.999
变成1
,-1.999
变成-1
。也有可能溢出,不过C语言没有规定溢出后的int
值,与Intel兼容的处理器会将结果指定为$TMin_w$。转换浮点数到整数的时候,如果找不到一个合理的值,就使用这个值。因此(int) +1e10
的结果是-2147483648
。
通过下面的几个例子做一下练习(其中x
,f
和d
分别是int
,float
和double
类型的值):
Expression | True or False | Explanation |
---|---|---|
x==(int)(double)x |
✔ | double 的精度大于int |
x==(int)(float)x |
✘ | 当x=TMax 时错误 |
d==(double)(float)d |
✘ | 当d=1e40 时,结果是正无穷 |
f==(float)(double)f |
✔ | double 的精度大于float |
f==-(-f) |
✔ | 浮点数使用s 表示正负 |
1.0/2==1/2.0 |
✔ | 计算的时候两个数都会转换成浮点数 |
d*d>=0.0 |
✔ | 正确,即使会溢出 |
(f+d)-f==d |
✘ | 这个就是不符合结合律的例子了 |
注意:将大的浮点数转换成整数是一个常见的错误。