新博客pppppkun
写在前面
在PA_2019fall中有一项任务是完成CPU中的浮点数运算,这也是我第一次认真的思考了一下真实的计算机中CPU是如何进行的浮点数运算
在写PA的过程中一头雾水,从迷茫,到困惑,到弄懂,到完成,中间经历了各种坎坷,又无奈于手上的资料仅仅只有一个Guide和i386,剩下不会的地方全靠百度
于是就诞生了这一篇博客,把其中的过程给大家讲的明明白白的!
预备知识
什么是浮点数?浮点数表示的是一个数字,其小数点所在的位置是不确定的,也就是浮动的,因此称之为浮点数
在IEEE 754标准中,单精度的浮点数由3部分组成
- 符号位,在首位,用1表示负数,0表示正数
- 阶码 部分,一共八位,采用移码形式,偏置常数为127
- 尾数部分,一共23位。
规格化于非规格化
规格化的数表示阶码部分不是全都为0,其23位的尾数实际上表示了24位,最高缺省位为1。而非规格化的数没有缺省位。
尾数加上最高位可能存在的缺省的1后构成的有效数字称为
实现之前的思考
排除特殊情况
根据表格我们可以先把边界条件排除掉,比如
加减
如何让两个浮点数相加?根据上面的知识我们知道两个浮点数的形式基本可以这样给出 ,我们很容易就能想到两个浮点数是不能直接相加的,应该让他们同阶后再将尾数相加,这个操作我们叫对阶
对阶
对阶有两种方式,小阶往大阶对齐和大阶往小阶对齐,该如何选择呢?
不妨考虑一下这样的事情,在IEEE754表示的浮点数中,对于相同的阶数来说,尾数中越靠后面的1对最后结果的影响越小。
在对阶中,如果大阶向小阶看齐,那么实际上是将尾数左移,小阶向大阶看齐的话就是尾数右移。
如果我们选择大阶向小阶看齐,那么我们很容易就将尾数中靠前的1给左移没了,因为尾数一共只有23位,这样对计算造成的误差相当大,所以我们选择小阶向大阶看齐
注:考虑如何减小误差是浮点数运算中非常重要的一环
对阶中的特殊情况
在处理非规格化浮点数的时候,它们的阶码全都是0,但是尾数并非是0,其表示的真实值为 ,不能将阶码理解成-127,在后面会讲为什么会有这样的结果。
shift
的计算
在此,我们已经可以给出右移的位数了,在这里我们假设
shift = (fb.exponent==0 ? fb.exponent+1:fb.exponent) - (fa.exponent==0? fa.exponent+1:fa.exponent)
最后我们得到的临时的阶码就是
流程图
先让我们把目前的东西总结一下,后面结合实际栗子来讲也会更加清晰
在这个图中,我们可以看到在对阶之前先判断了的值是否为0,如果不是那就开始对阶
在对阶的过程中,小阶慢慢变大,这会导致尾数右移(前面已经提到过),在尾数右移的过程中,我们有可能把尾数移为23个0,如果是这样,我们简单的判断为是阶数小的数实在是太小了,就像10亿+1,那个1加不加我们可能都没有直观的体验
再接下来,我们对尾数进行相加,这个过程中要考虑符号位,如果是做减法,那就对减数的尾数位采用广义上的 取反加一
相加完后,如果尾数变成0,那结果就是0,如果不是0,那就考虑一下尾数有没有溢出(尾数溢出指相加后最高位缺省位达到了2)。如果溢出了,那我们就右移一次,然后判断一下阶数的情况即可
要是没有溢出,那当然万事大吉,但是我们接下来还需要将我们的浮点数规格化一下,意思是规格化的浮点数为,但是我们计算出来的浮点数可能是,那么我们需要小数点移位,降低阶数,将最高缺省设置成1
当然,规格化的过程中阶数也有可能等于0,这里需要一次特判。
小小的总结
到这里浮点数的加减基本上已经介绍完了,对于乘除法就很简单了,只需要将指数相加,尾数相乘就行了,当然,其中涉及到的一些溢出情况都需要单独判断,在这里不多做讨论
附加位与舍位
一个贴近生活的栗子
加入你是亿万富翁,你的银行卡中有1000亿元,你每天稳定收入1000元,但是由于银行的计算机用的是上面的实现方法,这1000块钱在放进你的卡中的过程中需要进行对阶,对着对着尾数变成了0,相当于你存进去0块
那么该怎么办呢?当然你可以设置一些if
语句,让1000加多点,加到足够影响1000亿的时候再一起加上去,这是目前计算机的一个研究方向之一。
附加位
根据IEEE754的规定,所有浮点数运算的中间结果右边都必须至少保留两位的附加位,依次是保护位(guard,G)和舍入位(round,R),为了进一步提高精度,舍入位的右侧还有一个粘位(sticky,S),只要舍入位右边有任何非0的数字,粘位就是1,否则为0。
我们简称这三位为,下面用一个实例来展示一下它的作用
保护位
x = 1.000..00 * 2^1 y = 1.11...11 * 2^0
// without guard bits
x = 1.000...00 * 2^1
- y = 0.111...11 * 2^1
z = 0.000...01 * 2^1
= 1.000...00 * 2^-22
// with guard bits
x = 1.000...00 0000 * 2^1
- y = 0.111...11 1000 * 2^1
z = 1.000...00 0000 * 2^-23
可以很明显的观察到,在多了保护位后,计算结果明显更精确了
但是解决了一个问题后又出现了第二个问题,那就是我们的尾数只有23位,就算我们使用了保护位,也只能保护计算过程中的误差,那么对于结果来说,依然有可能不精确
所以接下来,我们引入舍位
舍位
为了保证运算过程中的最大精确度,我们会把23位+3位附加位的浮点数扩展到64位,相加完后再进行规格化,规格化完后我们要对这三位进行舍入操作。舍入的方法采用就近舍入,中间值4按照奇偶来舍入,确定舍入后,完成尾数的后三位右移操作。
缺陷
我们不妨考虑这个栗子
\\省略对阶
1 010001100 1 010001100 000000
-0 000000111 0 000000111 011110
1 010000101 1 010000100 100010
645.0 644.0
就算有了舍位,在单精度下我们依然很难给出两个浮点数加减的准确答案。
关于浮点数的表示
如果都是规格化的浮点数,那么我们很容易知道我们是没法表示0的,因为最小就是,所以为了表示0,我们将和的和用来表示0,那该怎么办呢?
为了填上0到这段上的实数,我们设置了非规格化的浮点数,并且将到的数扩展到了整个0到上,这之间的每个数间隔都是一样的
最大的数为
其中每个数的间隔都是
和最大数之间的距离是
Summary
-
Floating-point representation
-
Operations
-
Addition
-
-
Precision Consideration