数据的表示与运算-浮点数
前言:
- 计算机中,数字分为定点数和浮点数。相对于浮点数,定点数比较好理解,原码补码反码移码。而浮点数十分繁杂。
- 关于浮点数的繁杂,我觉得最好的解释就是,\(William\ M. Kahan\)因其在浮点数运算标准的制定上的突出贡献而获得图灵奖。\(Kahan\)也是浮点数\(IEEE754\)标准的主要设计师。
初识浮点数:
- 假如说我们现在想要表示光速这样一个数值,我们可以怎么做?
- \(1:\)采用整数方式把他写出来,那么就是\(300...00m/s\)。这样数字十分的长,与计算机不好保存。
- \(2:\)采用科学计数法,那么就是\(3*10^{8}m/s\),那么如果我现在想保存这个数字,那么我只需要记录三个信息,第一个是\(3\),第二个是\(10\),第三个是\(8\)。
- 两种方法比较:
- 很明显第一种方法需要我们用更多的存储空间来保存它,而对于科学计数法,我们并不需要记录那么多的数却能表示同样的数值。
- 对于计算机而言,只能认识\(0/1\)符号,这在硬件实现上更为方便简单。所以我们这时候可以把\(10\)这个底数给取消掉,计算机默认他是\(2\),这样我们就可以只用保存两个数字,来表示这样一个大的数字。
- 对于表示电子的质量,太阳系的直径,这样非常极端的数字时,科学计数法的优势显得更为明显。它更方便。
- 但我们也可以发现,如果说我要表示的数字不是\(300...000\),而是\(29935...13\)这样的数字,而我科学计数法采用\(2.9*10^{8}\),那么我势必丢掉一些精度。
- 在我的理解看来,浮点数的表示是用精度来换取表示范围。
- 所以这里我们似乎也能理解为什么在\(c\)语言中,\(double\)比\(float\)能表示更高的精度,因为\(double\)位数更高,他能表示更多的小数。
- 也能多少的理解浮点数为什么叫浮点数,因为随着我数字的指数不同,小数点的位置也在随之改变。
浮点数的表示:
通常,浮点数可以表示为:\(N=r^E*M\)。
其中\(r\)是阶码的底,通常为\(2\),且与尾数的基数相同。
\(E\)是阶码,\(M\)是尾数。
如下所示:
阶符 阶码的数值部分 数符 尾数的数值部分 \(J_f\) \(J_1J_2,...,J_m\) \(S_f\) \(S_1S_2,...,S_n\)
阶码是整数,阶符和阶码共同表示浮点数的表示范围以及小数点的实际位置;
数符表示正负,尾数的数值表示浮点数的精度。
浮点数规格化:
- 先看门见山讲一下什么叫规格化。
规格化规定尾数的最高数位必须是一个有效值。
- 通过以上的阅读,我们可以发现,要想让精度最大化,那么我们就需要让尾数部分尽可能的保存有效的数字。
- 比如说对于这两个数(二进制)
- \(2^{10}*0.01\)和\(2^{01}*0.1\).
- 这两个数是相等的,但是第二个数明显可以在尾数上少保存一位\(0\),所以这时候我们可以对浮点数进行规格化,让他能表示更高的精度。
- 所谓规格化,是指通过一定的操作改变浮点数的尾数和阶码的大小,让浮点数(非0)的尾数在最高位保证是一个有效值。
- 有如下两种方法:
- 左规:当浮点数运算结果为非规格化时需要进行规格化处理,将尾数算术左移一位,并将阶码减一(二进制)。(左规可能需要进行多次)
- 右规:当浮点数运算尾数出现溢出时,也就是双符号为出现了\(01/10\),需要将尾数算术右移一位并将阶码加一。右规只进行一次。
- 那么规格化的浮点数的尾数的范围就是\(\frac{1}{2}\leq |M|\leq 1\)。
- 分析:
- 假设用原码来表示尾数:
- 正数:
- 数的最大值是\(0.11...111\),此时真值为\(1-2^{-n}\)。
- 数的最小值时\(0.10...000\),此时真值为\(\frac{1}{2}\)。
- 绝对值的范围为\([\frac{1}{2},1-2^{-n}]\)。
- 负数:
- 数的最大值是\(1.10...00\),此时真值为\(-\frac{1}{2}\)。
- 数的最小值是\(1.11...11\),此时真值为\(-(1-2^{-n})\)。
- 绝对值的范围是\([\frac{1}{2},1-2^{-n}]\)。
- 正数:
- 假设用补码来表示尾数:
- 正数:
- 正数的补码与原码相同,不做分析。
- 负数:
- 负数的最大值为\(1.011...1\),最小值为\(1.00...0\)。
- 绝对值得范围为\([\frac{1}{2}+2^{-n},1]\)。
- 这里需要注意。尾数的最大值不是\(1.10...00\)这样的形式,因为\(1.10...000\)不是一个规格化数。
- 这里我查了一些资料,有两种说法我比较认可,第一个是对于\(1.10000\),我可以接着对他规格化到\(1.00000\);第二个是方便机器的设计,对于原码,我可以判断他的尾数最高位是不是\(1\)来判断他是否规格化,对于补码,我可以判断他的数符和尾数最高位是否相同来判断他是否规格化。
- 正数:
- 假设用原码来表示尾数:
IEEE754标准:
按照\(IEEE754\)标准,浮点数表示格式如下:
数符 阶码(用移码表示) 尾数(用原码表示,隐藏最高位\(1\)) \(m_s\) \(E\) \(M\) 为了最大幅度的增大浮点数表示精度,我们尾数最高位如果为\(1\)我们将其隐藏。举个例子,假如说尾数是\(1011\),那么我们存储\(011\)。
\(float\)和\(double\)都是满足\(IEEE754\)标准的浮点数。
阶码以移码形式存在。对于短浮点数\(float\),偏置值为\(127\),对于长浮点数\(double\),偏置值为\(1023\)。
那么可以这么求:我先将\(E\)的看成补码形式求出其值,然后减去\(127/1023\)就是他的移码代表的值。
- \((-1)^s*1.M*2^{E-127}\)(短浮点数)。
- \((-1)^s*1.M*2^{E-1023}\)(长浮点数)。
浮点数的加减运算:
浮点数运算需要将阶码运算和尾数运算分隔开。且分成以下几步:
- 对阶
- 尾数加减
- 规格化
- 舍入
- 判断溢出:阶码溢出
接下来一一分析。
对阶:
- 对阶的目的是让两个操作数阶码相等。原则是小阶向大阶看齐的方法。将阶码小的数尾数右移,阶码加一知道阶码相等。当然因为右移需要舍弃数据,所以精度会受影响。
尾数加减:
- 对阶后进行定点数加减运算。
规格化:
- 按照上文所述的规格化进行左规或者右规。
舍入:
- 在对阶和右规的过程中,可能会尾数低位丢失,引起误差,常见的舍入方法有:
- \(1:\)\(0\)舍\(1\)入法:尾数右移的时候如果是\(0\),就舍去;如果是\(1\),就在尾数末尾\(+1\),这样有可能让尾数溢出,需要进行一次右规。(假设那个尾数全是1,加上就会一直进位进位到溢出)。
- \(2:\)恒置\(1\)法:尾数丢掉后不管是\(1\)还是\(0\),补上\(1\),这样可能会使尾数变大或者变小。
- 在对阶和右规的过程中,可能会尾数低位丢失,引起误差,常见的舍入方法有:
溢出判断:
- 最后一步需要判断溢出。
- 在浮点数规格化部分已经知道尾数双符号位出现\(01,10\),并不表示溢出,将此数右规即可。
- 浮点数的溢出是由阶码决定的。双符号阶码出现\(01/10\),这时候就溢出了。
- \(10:\)阶码小于最小阶码,按机器零处理。
- \(01:\)阶码大于最大阶码,进入中断处理。