@[toc]
一、引子
这一小节中,我们来学习定点数的移位运算怎么实现。
运算又可以进一步的划分为算数移位、逻辑移位还有循环移位,我们会按从上至下的顺序依次讲解。
二、原码的算数移位
(1)十进制
首先来认识一下什么叫做算数移位
。
这里从大家熟悉的十进制数出发,假设这儿有这样的一个十进制数 985. 211。
我们从小经常做的一个事情是让小数点后移1位,或者后移 2 位。
小数点每后移1位,相当于我们对整个数值乘以了一个 10 的1次方,也就是奇数的1次方;而小数点后移两位相当于乘以 10 的2次方。
小数点往前移也是类似的一个效果,只不过往前移相当于是除以 10 的1次方和除以 10 的2次方。
这是大家在小学的时候就学会的东西,如果结合我们之前对 r 进制数它的实际数值的定义来看,其实我们移动了小数点之后,相当于改变了每一个数码位的位权。
因为每一个位的位权为多少,其实是以小数点的位置作为参考的。
所以所谓的算数移位的意思就是我们通过改变各个数码位和小数点的相对位置,从而改变各个数码位的位权。
我们可以用这种算数移位的方式来等价地实现乘法和除法。
(2)二进制
1.算术右移
刚才是大家熟悉的十进制,对于我们之前的小节中学习的二进制数,其实也是一样的。
先来看算术右移
。
对于定点数来说,我们没办法改变小数点的位置,但是山不转可以让水转,所以我们如果能够移动数值部分,只要能改变每一个数值位和小数点的相对位置关系,我们同样可以实现算数移位的运算。
比如这儿我们已经有了-20 的原码表示。
进行了算数右移1位之后,得到的值应该是 2 的 1 次方,再加上 2 的 3 次方等于10。再考虑上符号位就应该是-10。
所以和我们之前十进制推出的结论是类似的。
当我们对二进制的这种定点数右移1位之后,相当于我们实现了除以奇数的1次方这样的操作。
结合这个图并不难理解,本来以前这两个1(图中画圈的位置),它们的权重分别是 2 的 4 次方和 2 的2次方。右移一位之后,它们的权重都分别除以了2,一个变成了 2 的 3 次方,一个变成了 2 的1次方,整体都是缩小了一半。
所以这就是算数右移的效果,相当于除以2。
我们再右移一位。
刚才最低位就会移动到小数点的后面位置。由于我们的机器字长有限,所以移出去的这一位我们就只能舍弃不用。
同样的,新的高位我们会用 0 来补充。
这次右移的结果,同样也是相当于再除以一个2。
继续在这个基础上再右移一位。这个时候我们会把末位的 1 给移出这 8 个比特的范围,同样高位补0。
不过这个时候得到的值它应该是-2。
这样的一个值已经不是- 5/ 2 的精确表示了,因为- 5/ 2 应该是负的 2. 5,这样才是精确的除以 2 的值。
这是因为我们移出去的这一位,它的值不是0,而是1,所以其实相当于我们舍弃了 2 的-1 次方这样的精度。
因此我们得到这样的结论,当我们进行算数右移的时候,首先高位会用 0 来补充,低位直接舍弃。如果我们舍弃的这一位是0,就相当于严谨的除以 2 的结果。而如果我们舍弃的这一位不等于0,在这种情况下,我们会丢失一定的精度。
这是算数右移。
2.算术左移
接下来看算数左移
。
左移的方式也是一样的,我们只让数值部分进行移动,符号位是保持不变的。
进行左移之后,原本数值位的最高位会被我们舍弃,而最低位出现的空位,我们会用 0 来代替。
进行这样的一次右移之后,得到的结果应该是负的40,相当于在原有的基础上乘以2。
如果再左移一位,也是类似的效果。
丢弃最高位的0,相当于在原有的基础上再乘以一个2。
我们再左移一位。由于这次我们丢掉的最高位是1,所以最终我们得到的结果这儿等于负的32。
这次左移操作就不是乘以 2 这么简单了。
这一点其实也很好理解,因为我们原本左移2位的时候,数值就已经到了-80 这样的一个值。
负的80,如果让它再乘以2,应该是负的160。而这儿我们原码的尾数只有 7 位, 7 个比特位只能表示 0~127
这样一个绝对值范围,所以 160 肯定是已经超出 7 个比特位能够表示的范围。
这种情况下,我们丢弃最高位的1,权值最高的 1 被我们丢弃了,当然就会出现这种严重的误差。
这是对原码的算数左移需要注意的地方。
三、定点小数
目前为止我们探讨的算数左移和算数右移是基于用原码表示的定点整数
来探讨的。
如果不是定点整数,而是定点小数
,其实也是一样的道理,一样的效果。
当我们进行算数左移
的时候,同样相当于乘以 2 的效果。算数右移
相当于除以 2 的效果。
因为所有的这些1,它们的位权在左移和右移的时候,分别会乘以 2 和除以2。(如下图)
所以定点小数我们就不再单独的探讨。
四、反码的算数移位
接下来我们再来看基于反码的算数移位。
<1> 先来看正数
。由于正数的反码表示和原码表示是一模一样的,所以对于正数的算数移位,不管是左移还是右移,处理的方法都是和原码一样。
这是反码表示的正数,需要采取的一个策略,和原码是一样的。
<2> 再来看负数
,也就是符号位为 1 的数,这种数的反码尾数部分和原码是完全相反的, 1 变成 0、0 会变成1。反码的 1 相当于原码的0,反码的 0 相当于原码的1。
所以对负数的算数移位,进行补位的时候,需要注意,我们补的都是1,这是对于反码的算数移位。
正数和负数我们需要补充的这个位是不一样的。
五、补码的算数移位
接下来我们再来看补码的算数移位。
<1>先来看正数
。由于正数的补码表示和原码也是一样的,所以对于正数补码的移位运算,我们同样和原码保持一样的策略就可以,都是需要用 0 来补充移动之后出现了空位。
<2> 再来看负数
。负数补码的移位会相对来说复杂一些,补码是从反码的基础上末位加 1 得到的。反码的末位加 1 ,会导致反码当中更靠后的这些 1 都会变成0,并且都会发生进位,直到进到第一个 0 为止。
所以反码转补码有这样的一个规律,从反码的最右边这一位开始,从右往左依次取反,把 1 都变成0,直到碰到第一个 0 为止。
把反码的第一个 0 变成 1 之后,再往前的这些部分就不用再改变了。如下图,红线左侧不用再改变了。
所以负数的补码呈现出了这样的一个规律,在这个补码最右边的一个1,还有 1 再往右的这些部分,这些部分是和原码保持一致的。如下图:
而最右边的1左边的这些部分又是和反码保持一致的。如下图:
所以当我们对补码的这些尾数进行算数右移
的时候,往右移会导致高位出现一个空位,补空位的方法是和反码补空位规则保持一致的,也就是补1。
而当我们对补码进行算数左移
的时候,最低位会出现一个需要补的空位。由于补码的后半部分和原码是相同的,所以我们在补空位的时候应该补0。
因此我们得到结论,对于负数补码的算数移位,当右移的时候,我们应该补1,低位舍弃。当进行算数左移的时候,应该低位补0,高位舍弃。
用这样的方式,我们就可以保证对补码的算数右移同样是相当于除以 2 的效果;算数左移相当于乘以 2 的效果。
六、算数移位总结
我们再对算数移位进行一个小小的总结。
<1> 对于正数
来说,由于原码、补码、反码,它们的正数表示都是一样的,所以我们需要补位的时候都是用 0 去补。
<2> 而对于负数
来说,补码的负数左移的时候需要填0,右移的时候是要填1。而反码的负数,不管左移还是右移,我们都是要填1。
只要遵从这儿给出的规定,无论我们使用的是什么码,只要我们进行的是算数左移就相当于乘2,只要是右移就相当于除以2。
只不过由于我们机器字长位数有限,所以有的时候我们没办法用算数移位精确地来等效乘除法。这一点我们在讲原码的左移和右移的时候特别强调过。
七、案例
(1)算数移位
接下来用一个简单的例子让大家体会一下算数移位
的具体应用。
我们之前已经探讨了-20 原码,它左移1位和左移2位所得到的结果。
如果现在我们要算的是-20 乘以7,要让计算机完成这样的乘法运算。 7 这个数,我们可以把它拆分为 2 的 0 次方加 2 的1次方加 2 的2次方。
所以- 20 乘以7,我们可以把它拆解为这样的三个乘法。 硬件在执行乘以 7 这个过程的时候,其实就相当于把- 20 原有的数的基础上不移位,还有左移1位,左移2位这样的三个数进行一个相加操作,就可以等效地完成- 20 乘以 7 的操作。
所以计算机硬件实现乘法其实是基于算数移位还有加法来进行的。
而实现算数移位的硬件电路设计起来并不复杂。
具体乘法怎么实现,我们还会用之后的小结进行更进一步地探讨。
(2)逻辑移位
接下来我们再来看第二种移位,叫做逻辑移位
。
逻辑移位的规则很简单,当我们右移的时候,高位补0,低位移出的这一位直接舍弃就可以。
左移的时候,我们在低位补0,移出的这一位我们直接舍弃就可以。
逻辑移位的这种规则,我们可以把它看作是对无符号数的算数移位。
我们来看一下逻辑移位有什么作用。
比如我们在计算机里面表示一种颜色的时候,经常会使用到这样的一种表示方式,叫做RGB。 r 指的是red,也就是红色;g 表示的是green,也就是绿色;而 b 表示的是blue,也就是蓝色。
因为我们知道自然界里边所有的颜色都是由红、绿、蓝这三种三原色来按照一定配比来组成的。
比如对于最后颜色(如下图),它的RGB值分别是 102、139 和139。有的时候我们存储一个像素点它的RGB值的时候,需要把RGB这三个值把连成 3 个字节的一个整体。
第一个字节存放r,第二个字节存放g,第三个字节存放 b 的值。
而现在我们只是分开指明了这三个部分的值分别是多少。
来看一下怎么把它们拼成三个字节的一个整体。
首先我们申请用三个字节来存储无符号数102,也就是 r 的值。现在我们对无符号数进行逻辑左移 16 位,这就会导致低8位移动到高8位的位置。左移产生的这些空位我们都是用 0 来补充,这是逻辑左移的规定。
接下来我们再定义一个三个字节的无符号数,这个数的值是139。我们在对这个无符号数进行逻辑左移8位的操作,就会导致原本的第8位被放到了中间的 8 个位的位置。
然后我们再用三个字节来存储无符号数,也就是 b 的值139。
最后我们再把刚才得到的 123 这三个部分进行一个加法操作,就可以得到三个字节表示的RGB值。
最高的一个字节表示的是 r 值,中间表示的是g,也就是绿色的值。最后表示的是b,blue 的值。
这是逻辑移位的一个应用小例子。
八、循环移位
接下我们再来看。最后一种移位运算叫做循环移位
。
顾名思义,所谓循环,就是指当我们进行比如循环左移的时候,移出来的这一位会被放到我们需要填补的空位。
在移位的时候,整个二进制串是进行循环补位的。
应该很好理解,当我们进行循环右移的时候,也是类似的,从右边移出来的这一位又会跑到应该补充的位置去。
还有一种比较特殊的循环移位,就是带进位位的这种情况。
先来解释一下什么叫进位位,也就我们标注的CF这一位。
进位的概念大家都知道了,比如我们对两个8位的二进制数进行加法操作,当我们运算到最高位的时候, 1 加 1 等于 0 往高位进1,由于机器字长有限,寄存器里只能保存 8 个二进制位,而最高位又确实产生了 1 这样的进位,因此为了实现超过 8 比特的这种数据的加法,计算机硬件里边会包含这样的所谓的进位的位,来记录下之前这些低位的运算有没有产生进位。
把进位保留下来之后,我们在进行之后的更高字节的运算的时候,就可以得到正确的结果 0 加 1再加上刚才保留的进位1,用这样的方式我们就可以不断地往高位进行计算。
所以这就是所谓的进位位的作用。
总之它里面要么存了一个1,要么存了一个0。
现在当我们考虑上进位之后,再进行循环左移,产生的效果就是这样的。
会把原本数值位的最高位把它放到进位位这个地方,而原本的进位位会来补充出现了空位这个样子。
这就是带进位位的循环左移。
带进位位的循环右移也是类似的。无非就是把末尾的低位放到进位的位置,而原本的进位把它放到最高位出现空缺的位置。
这就是所谓的循环移位。
循环移位的操作很适合用于把一个数据的高字节和低字节进行调换。
比如之前我们讲过一个例子,比如“啊”这个汉字,它需要占两个字节的位置,但是我们可以有大端存储和小端存储这样的两种方式。
大端存储就是先存放高字节,再存放低字节,而小端存储是先存放低字节,再存放高字节。
如果要在大端存储和小端存储之间进行转换,使用循环移位是不是很方便?我们可以循环右移8位,或者循环左移8位,就可以实现高低字节的调换。
这是循环移位的作用。
九、总结回顾
这个小结中,我们学习了定点数的移位运算,其中最常考的是算数移位。
当我们进行算数左移移位的时候,相当于乘了一个基数;当我们算数右移位的时候,相当于除以基数的效果。
原码、反码、补码进行移位之后,补位的策略是不太一样的。
特别是补码,大家比较容易忘,需要基于理解来记忆。
逻辑移位的实现很简单,无论是左移还是右移都补 0 就可以。
最后我们又学习了循环移位,就是用移出去的位来补上空缺的位。
而对于带进位位的循环移位,就是移出的位,我们会把它放到进位位的位置,而原来的进位位会补上空缺的位置。
再次强调,由于原码、补码、反码,它们的位数有限,也可以表示的数值范围是有限的。所以在某些情况下,移位操作并不能精确地等效乘法和除法的效果,可能会散失精度,甚至是产生比较大的误差。
这就是移位操作相关的所有内容。