Abstract: 本文介绍实数在计算机内的浮点数表示方法
Keywords: 浮点表示,IEEE754
实数的浮点表示
好久没更新数值分析了,上一篇我们研究了十进制和二进制之间的转换,根本目的就是想让我们从我们熟悉的十进制计算体系转换到二进制的计算机的计算体系来。我们称之为浮点数,浮点数有很多版本,当然众多版本中有很多都是用在特定设备上的,IEEE有明确的关于浮点数的标准,我们今天介绍的是在IEEE754标准下的浮点数,如果你能做CPU你也可以做你自己的浮点数标准,然后让一群人按照你的标准开发程序,那都是有可能的,但是如果我们使用通用的CPU,我们还是按照标准来比较合理。
我们一直以来学习计算做大量的计算题都是基于有限位精确度的,并且我们很少研究当我们做所谓的四舍五入的时候,对数字本身有什么影响,比如我们最初学的无限循环小数,$\frac{10}{3}=0.333\dots$ 我们在学习这个数字的近似表示的时候,我们会说如果想保留两位小数,那么我们舍去第三位后面的循环,直接得到 $0.33$ 这时候,$0.33$ 和 $0.333\dots$ 不是一个数字,而是两个完全不同的实数,中间存在差距 $0.000 333\dots$ 这就是我们在10进制中计算过程中产生的误差,这种舍入是无法避免的,毕竟在实数中无限位的数字非常之多。
如果你说这个误差非常小可以忽略,小学的时候是这么讲的,但是当我们用舍入后的结果和一个很大的数字相乘的时候,或者在高斯消元,微分方程的求解这些简单的过程中,这个极小误差就会被放大。这些问题同样存在于二进制中,因为我们用无法用有限的位来表示无限的小数,我们必须时刻注意小心计算机的小误差使得计算结果不可靠的危险,因为这可以帮你避免在一个不错的模型下而得不到好结果时是误差再作祟。
浮点格式
还记得科学计数法么?十进制科学计数法有一定的规则,把数字写成规定的一个大于1小于10的实数和10的整数幂的乘法形式:
$$
\pm 1.aaaaa\dots\times 10^m
$$
同样在二进制浮点数中也是一样规定:
$$
\pm 1.aaaaa\dots\times 2^p
$$
进制改变我们把10变成了2,但是如果想把上面这种表示写入内存,就需要提前规定,比如aaaa这些a要占多少位,p要占多少位, $\pm$ 怎么表示,于是IEEE就给出了下面这个标准:
精度 | 符号 | 指数 | 尾数 |
---|---|---|---|
单精度 | 1 | 8 | 23 |
双精度 | 1 | 11 | 52 |
长双精度 | 1 | 15 | 64 |
上面就是在内存中不同浮点数类型所占的内存位数,注意是位,而不是字,字节等其他单位。
符号位占一位这个好解释,指数是指p,单精度指数最多有8位,尾数是指 $aaaa\dots$ ,其在单精度中最多有23位空间,注意每一位上只能是0或者1,因为这是二进制下的表示。
符号位1表示负号,0表示正号
举个例子:
$$
+1.01\times 2^{101}
$$
以单精度浮点数保存在内存中是这样的:
0 | 00000101 | 01000000000000000000000 |
---|---|---|
符号位 | 指数 | 尾数 |
这是完整的在单精度浮点数中内存的样子
接着我们来看一个定义
定义0.1 机器精度对应的数字,记做 $\varepsilon_{mach}$ 是1和比1大的最小的小浮点数之间的距离,对于IEEE双精度浮点数表示:
$$
\varepsilon_{mach}=2^{-52}
$$
这是关于机器精度的定义,这个和我们在十进制科学计数法中关于有效数字有关系,注意这里是1和比1大的最小数字之间的差距,这个1是不能变的,因为是二进制,如果你说2和比2大的最小数字,在浮点数中我们要退一位,从机器内存储的角度看,还是和1比较,同样方法用于任何数字,精度和数字大小无关,也就是和指数没有关系,只和尾数有关系,或者说和尾数所占位数的长度有关,这一点是可以理解的,最大精度由最末尾的位来决定。
那么问题又来了,十进制数我们在小数无限的时候取近似有四舍五入的法则,那么二进制在无限小数取近似的时候是用什么法则呢?
取近似就是截断无限长的小数尾巴,我们有多种方式:
- 截断,直接丢弃一定长度后面的数字而不管后面的数字是什么
- 舍入,十进制中四舍五入就是舍入
截断不可取是因为会造成系统性误差,截断后总是朝着更小的方向迈进,我们希望整个系统是无偏的,虽然有误差,我们希望不要有系统误差,这个在数理统计里面也会介绍。
相比之下舍入就好的多,因为他有可能向上也有可能向下,也就是近似结果可能大于精确值,也可能小于精确值,当我们保证两种概率相同的时候,那么这就是个无偏的系统。
二进制舍入规则,对于双精度浮点数,52位尾数:
- 53位是0的时候,舍去52后面的数字
- 53位是1的时候,且52位之后不是 $1000\dots$ ,52位加1,然后舍去52位后面的数字
- 当52位后面的数字是 $1000\dots$ 的时候,根据52位的数字选择是进位还是直接舍去,如果52位是1,则52位加1后截断否则直接截断
第3条看着有点怪,但是却是非常重要的一条,因为第53开始是 $1000\dots$ 的数字刚好是进位和不进位的中间,如果规定这个数字向任何一方都会导致一方比另一方的概率变大,这样就会出现偏差,虽然很小,但是为了避免这个误差出现,我们观察52位,我们假定52位在大量样本下是均匀分布的,那么我们以此作为进位与否的标志,就能得到一个无偏的方法。
当存在系统误差的时候,计算结果会出现漂移现象,就是朝着偏大或者偏小一直移动。
不得不说,第3条准则是非常重要,其保证不会出现漂移。
定义0.2 将IEEE双精度浮点数字记做 $x$ 利用舍入最近法则记做 $\text{fl}(x)$
接下来我们就要研究当把一个十进制数转换到二级制浮点数的时候舍入所造成的误差。
例子:
十进制数9.4表示成二进制数是 $1.0010\overline{1100}$ 其在第53位开始往后是 $\overline{1100}$ 因为53位是1,且54位也是1,所以我们要在52位上加1,然后舍去53位及之后的所有尾数,那么按照这个过程,我们得到的实际的数字是:
- 计算截断部分的十进制数
$$
0.\overline{1100}\times2^3\times2^{-52}=0.4\times 2^{-48}
$$ - 计算包含误差后的值
$$
fl(9.4)=9.4+2^{-52}\times 2^{3}-0.4\times 2^{-48}\\
=9.4+0.2\times 2^{-49}
$$
我们存入计算机的十进制数和实际在计算机中的这个数并不一致,而且多数情况下是不一致的,通过舍入保存的数字和实际的十进制数之间的差,我们称为舍入误差。
上面例子中我们存入计算基的是9.4,但是由于计算机的本身的局限性,只能得到 $9.4+0.2\times 2^{-49}$ 这个近似值。
这是绝度的误差,当指数变大的的时候这个数字会变得非常大,所以舍入误差具体值对我们来说没什么参考意义,但是相对误差就很有意义了,因为相对误差抛弃了指数,而考虑比值。
定义0.3 令 $x_c$ 是计算机版本的 $x$ 精确度量,那么:
- 绝对误差: $|x-x_c|$
- 相对误差:$\frac{|x-x_c|}{x}$
其中 $x\neq 0$
一个结论,在IEEE计算机中相对误差不会超过机器精度的一半:
$$
\frac{|x-x_c|}{x}\leq \frac{1}{2}\varepsilon_{mach}
$$
这个证明比较简单,因为我们的尾数最后一位是进位的时候,我们原始数字和近似后的数字相差在第53位而且肯定小于其一半,所以就有了 $\frac{1}{2}$
也可以通过计算验证,这里就不详细讲了,带个数字进去就能验证这个结论。
机器表示
到目前为止,我们学了怎么得到一个要在内存中存储的数字,接着我们就要看看如何在计算机中实现。
双精度浮点在内存中占64位,也就是八字节:
$$
se_1e_2\cdots e_11b_1b_2\cdots b_52
$$
- 第一位s是符号位的个没什么说的
- 第二位开始的11位表示指数,为正数;考虑到指数也有正负,这里没有设置符号位,而是通过让指数去叠加 $2^{10}-1=1023$ 来得到,这样指数范围就是 $[-1022,1023]$ 之间。由于特殊目的,没有使用 $[0,2047]$ 这个我们后面会说
- 后面的 $b$ 就是尾数了
指数中的1023称为双精度格式的指数偏差,起作用就是把负指数转化成正数保存,单精度指数偏差是127,长双精度是16383
通过MATLAB的format hex使用16个连续的16进制数表示64位的双精度浮点数在计算机中的样子,比如数字1,在计算机中的样子如下:
我们看一下1.0的表示:
对应的二进制:
| 0 | 01111111111 | 0000 $\cdots$ 0 |
|:——:|:——————:|:———————–:|
| 符号位 | 指数(加了1023) | 尾数52位 |
9.4的表示:
9.4的二进制大家自己画画吧
我们接着要说三个特殊的数字,指数有11位,那么可以表示0~2047共2048个数,但是我们这里只有-1022~1023是指数可取值,共有2046个数,剩下两个数字-1023和1024做了什么;按照指数要加1023的规则,我们说的-1023和1024在内存中是以0,和2047存在的,那么我们规定下面的特殊情况:
机器数 | 例子 | 十六进制数 |
---|---|---|
+Inf | 1/0 | 7FF0 0000 0000 0000 |
-Inf | -1/0 | FFF0 0000 0000 0000 |
Nan | 0/0 | FFFx xxxx xxxx xxxx |
其中x表示0或者1的任意组合且不全为0
当指数是在内存中是2047的时候,如果尾数都是0,那么表示 $\infty$ Inf,否则表示 NaN(Not a Number)不是一个数字,$-\infty$ 对应的就是把Inf的符号位设置为1,也就是从7FF变成了FFF
指数都是1的研究完了,还有一个0,当指数是0的时候,数字解释为非标准的浮点数字:
$$
\pm0.b_1b_2\dots b_52\times 2^{-1022}
$$
小数点前面被设置为0,这是非标准的,这种与规定不符的数字,我们叫做异常(subnormal)浮点数.
根据这个异常我们的指数最小时 -1023,此时小数点前面没数字,那么当我们的尾数是 $0000\cdots 0001$ 的时候,那么此时是双精度浮点数能表示的最小的数字 $2^{-52}\times 2^{-1022}=2^{-1074}$ 不能更小了。
另一个重要的特征是我们有两种0的表示,+0和-0相差一个符号位:
浮点数加法
浮点数大概的规则和表达我们说完了,接着就看看浮点数怎么计算了,最基本的从加法开始,加法的过程如下:
- 将两个数字的对齐,所谓对齐就是对齐小数点。
- 在特定的寄存器存储完成加法,寄存器能存储多于52的精度
- 计算完成后进行舍入,变回52位尾数。
来个例子,我们用 $2^{-53}+1$ 来描述描述一下过程:
$$
\begin{aligned}
&1.\boxed{00\cdots 0} \times 2^0+1.\boxed{00\cdots 0}\times 2^{-53}\\
=&1.\boxed{0000000000000000000000000000000000000000000000000000}\times 2^0\\
+&0.\boxed{0000000000000000000000000000000000000000000000000000}1\times 2^0\\
&\hline\\
=&1.\boxed{0000000000000000000000000000000000000000000000000000}1\times 2^0\\
=&1.\boxed{0000000000000000000000000000000000000000000000000000}\times 2^0
\end{aligned}
$$
上面的过程的实验结果如下:
两个数字相加后没变化,有这个性质的最大浮点数是: $2^{-53}$ 其他更大的数字加到1上都会比1大。
注意区别相对大小和绝对大小, $\varepsilon_{math}$ 是相对误差的精度,不是绝对误差,也就是说比 $\varepsilon_{math}$ 的数字并不能直接省略,而是要看这两个数的相对大小,这也就是大数吃小数可以而小数吃小数却不行的原因。
舍入不差常常带来意想不到的结果,比如 $9.4-9.0-0.4$ 的结果并不是 $0$
具体原因是每一步都存在舍入误差,所以,最后的结果产生了误差累积,且到了影响最后舍入结果的程度,所以会造成以上结果。
这些特殊的结果的原因就是舍入带来的,这也是计算机计算和精确计算之间的差别。
本文中所有描述的过程在实际计算中都是可以预测的,给出的舍入原则是一个通用的原则,也可以在不同的环境下使用不同原则的舍入原则。
不同舍入原则可以一起用来判断计算的稳定性。
舍入误差是非常凶险的 ,一个小小的舍入误差可能使得整个计算功亏一篑,这看起来不可思议,但却影响深远,后面我们会研究如何处理这种情况
总结
本文主要研究在计算中最常使用的浮点数的性质和在计算机中的各种相关规则,舍入误差是重点。