@[toc]
一、原码乘法运算
经过之前小节的学习,我们已经知道了定点数的加法、减法,还有移位运算如何实现。
这小节中我们要学习定点数的原码乘法如何实现。
这一小节中,我们会首先探讨乘法运算的实现思想,介绍原码的移位乘法如何实现。最后还会介绍补码的一位乘法如何实现。
(1)手算乘法
1.十进制乘法
首先来看一下大家比较熟悉的十进制的乘法
。
回忆一下小学时候如何做一个乘法运算的。比如 0. 985 乘以 0. 211,如果算上小数点之前的0,这两个数都是4位,都有 4 个数码位。
首先是 1 乘以985,把它写在下面。
接下来还有一个 1 乘以985,但是这一次的 985 要和上一个 985 进行一个错位,相对于上一个 985 来说,要往前移一位。
最后2乘以 985 应该是等于1970,同样的, 1970 的最后一位又要比上一个 985 再往前挪了一位。
每一个数码位进行乘法运算得到的结果,我们是把它们错位地排列在一起的。最后我们会把它进行一个相加,得到这样的结果。
最后小学老师会告诉我们如何处理小数点。小时候老师教我们的做法是从最后位置往前数 6 位就到了 2 的前边,所以小数点最终确定在 2 的前面。如下:
这就是 0. 985 乘以 0. 211 的一个手算方式。
这是我们小学时候学过的东西,现在的问题是这样的,不知道大家有没有思考过,为什么我们在写这些每一位乘得的结果的时候,都要错位得把它们写在一起。
其实这个原因可以结合我们之前介绍过的 r 进制的数值的定义来进行理解。
0.211 这个数我们可以把它看作是 2 乘以 10 的-1 次方,加上 1 乘以 10 的-2次方,再加上 1 乘以 10 的-3次方。 0. 985 我们不妨把它看作是 985 乘以 10 的-3 次方。如下:
所以这两个数相乘,我们可以把它拆分成这样的形式:
985 乘以 10 的-3 次方,再乘以 1 乘 10 的 -3 次方,也就是 985 乘以 1 乘 10 的-6 次方。
这一项再加上 985 乘 1 再乘以 10 的-5 次方。也就是和这儿的第二项的一个结合,最后这项也是一样的。
所以其实这个式子如果我们把中间加和的这些部分给它扩展一下,其实它是长这个样子。
985 乘以1,再乘以 10 的- 6 次方,就相当于 985 从这个位置小数点往前移 6 位。
985 乘以1,再乘以 10 的- 5 次方,就相当于从 985 数这儿把小数点往前挪 5 位。最后这个数也是一样的, 985 乘以 2 等于 1970。本来小数点在 0 的后面,乘了一个 10 的-4,就相当于往前挪4位。
所以我们最终要给这几个小数进行一个加和。
是不是得保证这个小数点是相互对齐的,而小数点相互对齐,就会导致我们后边这些数有这样的一个错位。
所以这就是为什么我们小时候学的乘法,需要把每一位的位积进行一个错位的原因,这才是它的本质。
2.二进制乘法
这是我们熟悉的十进制乘法,接下来我们把这种乘法的思想迁移到二进制的乘法
。
同样先来看手算的方式。
假如有这样的两个二进制数的相乘,比如两个定点的正小数进行相乘,并且每个定点数占 5 位,类似于十进制 0. 985 乘以 0. 211 的乘法。
首先我们应该用乘数的最低位(1)乘上被乘数,得到的结果应该是1101。
再用乘数的下一位(次第位)乘以被乘数,得到的结果同样是1101,不过我们要把它往前挪一位,进行一个错位。
接下来 0 乘以被乘数,得到结果应该是 0000。
最后是 1 乘以这个被乘数,得到结果是1101。把它们错位并且依次相加之后,可以得到这样的一个结果。 如下:
接下来确定小数点的位置应该是从最后位置往前移8位,也刚好到了 1 的前面。
原因和十进制是一样的。
十进制最终得到的结果。我们在确定小数点的时候,采用的最原始的方法是看一下被乘数(0.985)它小数点往前移了3位,乘数(0.211)是往前移3位。所以这两个数加起来总共需要从最后位置往前移 6 位。
用这样的方式来确定小数点的位置,二进制的确定方式也是一样的。
现在我们使用和之前类似的思路,把这个式子进行一个完善。
其实每一位和被乘数的积进行错位相加的原因是这样的。我们把乘数按位权进行展开,把被乘数与乘数各个项分别进行相乘相加,进行展开,就会得到这样的一个式子。如下:
第一项对应于 1101 乘以 2 的- 8 次方,也就是最低位的位积。第二个部分是对应了次低位的位积。第三部分乘了一个0,但是我们还是把它统一的写成 0. 000000 这样的形式,保持队形一致。最后这一项也是类似。
所以我们模仿十进制得到的上图左边这种乘法规则,其实是正确无误的。
我们把它展开就可以看到背后的逻辑。
所以二进制的原码乘法实现起来要比十进制还要方便。因为二进制的乘数每一位只有可能出现 0 或者 1 这样的两种情况。
如果出现1,那么这一次得到的位积刚好和被乘数是一样的;如果出现的是0,这一次得到的位积,我们直接取全 0 就可以。只有可能出现这两种情况。
另外,在这个式子里面(如下图),我们乘以的 2 的-8、2 的-7、2 的-5,这样的运算用计算机很方便实现。1101 乘以 2 的-5,其实就相当于把 1101 右移了 5 位。
左移等价于乘,右移等价于除
。
乘以 2 的- 5 次方,就是除以 2 的 5 次方,也就是向右移 5 位。
因此,每一位和被乘数相乘所得到的位积,我们可以很方便地用移位运算来实现。
3.一些问题
现在我们已经模拟了手算的思想,接下来我们尝试着用机器
来实现这些乘法。
刚才我们还有一些没有考虑到的问题。
第一个问题,我们这个例子当中是用两个正的小数进行相乘,但是实际的数字肯定有正负之分,所以符号位我们应该如何处理?
第二个问题,5 位的两个定点小数相乘之后的结果有可能到达 9 位。这么多几乎是翻了一倍。如果计算机的机器字长本来就只有 5 位,也就是每一个寄存器只能存放 5 位的数据,我们最终得到的乘积已经超出一个寄存器可以保存的容量。这个问题又要怎么处理?
第三个问题,刚才我们这个例子当中乘数的每一位和被乘数相乘所得到的位积都要保存下来,最后再来统一地相加。也就把这四个数(如下图)分别的保存在 4 个寄存器里,最后再来进行统一地相加。
显然这种方式是不靠谱的,因为我们这进行举例的两个数,它的长度还不是很长。
如果是两个 64 位的二进制数进行相乘,那就意味着会有 64 个位积。这就意味着我们需要有专门的 64 个寄存器来分别保存这 64 个位积。
这显然是比较浪费的一种行为。所以这个问题又要如何处理?
接下来我们带着这 3 个问题来寻找答案。
(2)机器实现
1.案例
我们来看这样的一个例子。
假设计算机的机器字长为 n +1 位,也就是 5 位,有一个符号位,数值位是 n 位。
现在有 x 和 y 这样的两个数,这儿分别给出了原码。 x 是一个负数, y 是一个正数。
要用原码的一位乘法来求得 x 乘以 y 的值。
之前我们已经探讨了两个原码正数相乘应该怎么计算,可以沿用之前的思路。
符号位
我们可以单独的处理,处理的方式很简单,就是把两个数的符号位进行一个异或运算,就可以确定它们的乘积到底正是负,用被乘数和乘数的绝对值来进行乘法运算。
这样就转换成了我们之前提到的正数乘以正数的乘法运算思想。
我们把 x 和 y 的绝对值的原码给它写出来(如上图所示)。
2.确定符号位
现在回头来解释一下这个符号位为什么是这么确定的。
还记得异或
的规则吗?异或的规则是,如果参与异或运算的两个二进制位相异,不一样,一个是0,一个是1,那么最终异或的结果就是1。而如果两个二进制数相同,比如 0 和0异或者 1 和 1 异或,最终得到的结果都是0。
由于正数的符号位是0,负数的符号位是1。正和负相乘得到一个负数,正和正相乘或者负和负相乘,都会得到一个正数。
因此,用异或的逻辑来确定乘积的符号位是没有问题的。所以符号位的确定很简单。
3.绝对值相乘的机器实现
接下来我们来看这两个绝对值的相乘如何用机器来实现。
现在我们来回忆一个很远古的知识。还记不记得在第一章里边,我们在聊到运算器的基本组成的时候,给过这样的一个表,如下:
我们运算器里边有ACC、MQ和X,也就是通用寄存器有这样的三个必不可少的寄存器,当我们进行乘法运算的时候, ACC寄存器里边存放的是乘积的高位,而MQ里边存放的是乘数,还有乘积的低位。 X通用寄存器里边存放的是被乘数。
现在我们来深挖一下背后的运算过程。
<1> 首先, X 通用寄存器里边要存被乘数,被乘数就是 X 的绝对值的一个原码,符号为为0,后边 是1101。
<2> MQ 里边存放的是乘数,这是最开始的一个状态,乘数是 y 的绝对值的原码,也就是01011。 最后, ACC里边存放的是乘积的高位,什么叫乘积的高位、低位,大家一会儿会有切实的体会。
在这儿大家只需要知道,在乘法运算进行之前,我们需要把ACC寄存器给清0。
<3>现在对比手算乘法,我们刚开始是计算乘数的最低位和被乘数的一个位积,而现在乘数存在 MQ 里边,我们在这儿把最低位的颜色涂成了更深的灰色,所以此时更深的灰色这一位就是当前要参与运算的位。如下:
规则是这样的,如果当前参与运算的位等于1,我们需要让ACC的值加上被乘数。
而如果当前的位为0,ACC要加上0,也就是什么也不加。
目前来看,现在要参与运算的位是1,所以我们让ACC里边的值和被乘数进行一个相加。这个过程是由ALU,也就是算数逻辑单元里边的加法电路来完成的。
这两个数相加之后的结果是01101,相加的结果会被放到ACC寄存器里边。如下:
这相当于我们把第一个位积给算出来了。
<4> 接下来我们要计算第二个位积,由于第二个位积和第一个位积进行相加的时候需要有一个错位,所以计算机的处理方式是让ACC和 MQ里边的这些数据统一逻辑右移1位,所有的这些位都向右移, ACC的最低位会移到MQ的最高位这个位置。
就是变成这个样子:
由于是逻辑右移,所以我们在高位补的是0。 如下:
这就相当于我们在接下来进行加法操作的时候,是让下一个位积和之前得到的位积进行了一个错位的相加。
另外,这个地方大家会发现 MQ 里边之前的最低位已经被我们移出了寄存器,其实就是直接丢弃了,因为乘数的最低位之前已经用过了,之后肯定用不到,所以我们可以直接把它丢弃。
<5> 接下来要计算的是次低位(1)和被乘数(0.1011)的位积。
由于之前的右移,乘数的次低位此时来到了 MQ 的最低位置,因此接下来同样的我们是用 MQ 的最后这一位进行位积的运算。
由于这个位也是1,因此接下来我们会让 ACC 的内容加上被乘数,也就是刚才我们画框的加法。
这个加法得到的结果应该是10011,大家可以自己具体算一下。这一次的加法,导致 ACC 的结果更新为这样的数。
<6> 下一个位积的处理方式也是类似的。
为了让下一个位积和之前得到的部分进行一个错位的相加,所以我们会让 ACC 和 MQ 统一的右移1位。再次强调是逻辑右移,所以我们高位要补0。如下:
这补充一个概念,就是我们红色的部分,我们可以把它称为部分积
。如下:
由于当前参与运算的位,它的值是0,所以这一次我们让ACC的值当前位加 0 就可以。
ACC 加 0 之后它的值是不会变的。
每一次加法之后,我们都会进行一次逻辑右移,所以接下来右移的结果是这样的。
<7> 接下来要参与运算的位,它的值是1,所以我们让ACC加上被乘数X,也就加上最后的这一项(01101),所以 ACC的结果更新为这个值。如下:
做完加法之后,再进行逻辑右移,得到这样的一个结果。
<8> 此时MQ的最后一位是0,但是这个 0 我们不需要让它参与位积的运算,因为这个 0 它其实是原本的乘数的符号位。
所以在数值位有 n 位的情况下,我们只需要重复 n 次加法和 移位,就可以得到最终正确的结果。
定点小数,它的小数点是隐含在符号位后面的这个位置的。
x 和 y 的绝对值相乘的结果就是 0. 10001111。
和我们手算得到的结果是一致的,结合之前手算的过程,相信大家能够理解为什么每次要进行一个加法,每一次要进行一个移位。
这个原理希望大家能够好好体会。只要能够理解这个原理,你肯定也能记住做题的方法。
经过之前这一堆骚操作,我们得到的其实是x 和 y 的绝对值的乘积。
别忘了我们还需要处理符号位。
所以最后我们还需要根据 x 和 y 的符号位异或的结果来代替修改符号位。由于 1 和0异或等于1,所以我们把符号位修改为1,这样我们就得到了 x 和 y 用原码的一位乘法得到的值,这个值同样是原码的表示。
4.补充说明
现在大家再来体会为什么说ACC 里边存储的是乘积的高位,而MQ 里边最终会存储乘积的低位。
现在应该能够理解这两个名词背后的含义了。
另外,为什么要叫原码的一位乘法
?因为我们每一次参与运算的都只有一个位,所以叫原码的一位乘法。
其实还有更快的一种乘法实现的方法,就是原码的二位乘法。每一次有两个位来参与运算,这个地方就不拓展了。大家重点要理解的是原码一位乘法。
(3)手算模拟原码一位乘法
刚才我们是用机器的方式来一步一步模拟。接下来我们来看一下,如果我们手动做题,手算,应该如何来描述这些乘法执行的规则。
要计算 x 和 y 的乘积,数值部分我们是通过绝对值相乘来进行运算的。
不过这个地方大家会发现,我们课本里边被乘数和乘数,它是用这种双符号位的形式来进行描述的。
但是事实上,我们使用单符号位也不会出错。
在唐朔飞版的教材里边,它就是采用单符号位的描述方式。反正大家做题的时候用双符号位,单符号位都可以。不过由于补码的乘法一定要使用双符号位,所以为了方便大家记忆,大家在做原码乘法的时候,也可以把被乘数写成这种双符号位的形式,保持和补码的一个统一。
通过之前的讲解,我们知道乘数只有数值位会参与运算,符号位是不参与运算的,所以在我们手算的时候,其实我们只需要写出乘数的这数值部分就可以了。
另外我们知道被乘数的值是存到了通用寄存器 X 当中,乘数刚开始是存到了 MQ 乘商寄存器里边。高位的部分机我们是用 ACC 来记录, 刚开始需要初始化为0。
这个地方我们画了小横线的这一位,就是当前要参与运算的这一位,也就是这所谓的C4。
当 C4 等于 1 的时候,需要让ACC里的值加上x,也就是被乘数的绝对值。
当 C4 等于 0 的时候,就要加上0。
每次加法运算得到一个结果之后,一定需要进行一次逻辑右移。由于是逻辑右移,所以高位一定都是补0。
另外这个右移会导致 MQ, 也就乘商寄存器里边之前的最低位被丢弃。
在数值位有 n 位的情况下,只要经过 n 轮的加法和移位,就可以得到绝对值相乘的结果。
最后我们再通过符号位的一个异或运算,可以确定符号位应该为多少。
这地方课本上是写了 x 乘以 y 的一个用二进制表示的真值。如果大家自己答题,最好还是在写出真值它所对应的原码机器数。
总之,对原码进行移位乘法,无非就是进行 n 轮的加法和移位。
每一次加法之后,一定都需要向右,并且是逻辑右移一位,只要重复 n 轮就可以得到最终的结果。
而至于每一步的加法需要加什么,需要看当前MQ的最低位,它如果为1,就是ACC加上 x 的绝对值的一个原码。如果 MQ 当前最低位为0,就是 ACC加上0,也就什么也不加,直接右移就OK。
以上就是原码乘法的实现方式。
这个小节当中,我们是使用小数的原码乘法作为例子。其实两个整数的乘法实现也是类似的,大家只需要把小数点改成逗号就可以。
两个小数相乘,最终得到的结果小数点是固定在符号位后边这个位置。如下:
而如果大家算的是两个整数的乘法,那么最终小数点应该是固定在这个位置才对。如下:
所以整数的运算和小数的运算很类似,这就不再赘述。
二、补码乘法运算
刚才我们学了原码的乘法运算,现在我们来看补码的乘法运算怎么实现。
我们需要掌握的是补码的一位乘法
。
(1)原码与补码
由于补码的计算方法和原码的计算方法非常类似,所以我们会和刚才的内容进行对比,学习和记忆。
<1> 第一点。在原码的一位乘法当中,我们说过需要进行 n 轮的加法,还有移位操作可以得到最终的结果。补码的一位乘法,我们同样需要进行 n 轮的加法和移位,但是最后会比原码的移位乘法再多来一次加法。
<2> 第二点。原码每一轮的加法,我们有可能会加这样的两种值,要么就是加权0,要么就是加 x 的绝对值的一个原码。
具体加什么,我们是根据 MQ 乘商寄存器的最低位来确定的。当最低位为 1 的时候,加上 x 的绝对值的原码,当最低位为 0 的时候,加上0。
再看补码的乘法。补码每一次有可能加的值有这样的三种情况,要么加0,要么加上 x 的补码,要么加上负 x 的补码。
具体要加什么,我们需要根据 MQ 当中的最低位。,还有一个所谓的辅助位来确定。
辅助位是什么,我们一会儿再来解释。
规则是这样的,不管是辅助位还是MQ的最低位,肯定要么就是0,要么就是1。所以辅助位-MQ最低位
有可能得到的值。只会有这样的三种情况,为 1 为 0 或者为-1。
当辅助位减MQ最低位等于 1 的时候,我们需要用ACC 加上 x 的补码。如果相减为0,我们需要让 ACC加上0,而如果相减得-1,我们需要让 ACC加上负 x 的补码。
其实还是很直观很好记的,都有这种一一对应的关系,这是第二个区别。
<3> 第三点。对于原码的乘法来说,我们每一次做完加法之后,都需要进行逻辑右移。逻辑右移就意味着高位一定是添0。
而对于补码来说,每一次做完加法之后,我们进行的移位操作是补码的算数右移。
还记不记得补码的算数右移怎么做?就是符号位保持不变,而高位到底添什么,你需要具体看符号位到底为多少,我们一会再带大家来复习。
总之,原码和补码乘法的右移规则是不太一样的。
<4> 最后,原码的符号位又叫乘数的符号位,是不参与运算的。
而补码的乘数,它的符号位必须参与运算。
两个原码的乘法,最终结果到底正是负,我们是单独的用一个异或运算来确定;而补码的乘法是正是负,我们是直接通过加法和移位的运算,最终会自然而然地确定。
(2)补码乘法运算的硬件构成
接下来看一下补码的乘法运算它的硬件构成和原码有什么不一样。
首先,我们这儿提到过一个很重要的东西,就是所谓的辅助位
。其实这个所谓的辅助位是把 MQ 寄存器它的容量多扩展了一位,是用新扩展的这一位来存储的。
辅助位初始是0,而之后我们每一轮会进行一个右移。
右移操作会导致原有的辅助位(0)被丢弃,而之前 MQ 最低的参与乘运算的这一位(1)会变成辅助位。
所谓的 MQ 寄存器里的最低位,指的是当前参与乘法运算的这一位。我们用这种描述方式,只是为了和原码乘法的描述方式进行一个统一,大家也比较方便记忆。
但事实上, MQ 真正的最低位其实是辅助位才对,所以大家不要混淆。
由于 MQ 里边新增加了一个位,所以它总共应该是有 n+2
这么多个位。
另外一点值得注意的是,CPU里边的这些寄存器,所有寄存器的长度一般都是统一的,所以由于MQ它多增加了一位,因此 ACC还有 X 这些寄存器也会多增加一位。多增加的这一位可以用来表示双符号位的这种补码。
另外需要注意的是,在原码的乘法当中,我们刚开始是往 X 还有MQ里边存入了被乘数和乘数的绝对值,而这个地方我们是直接把它的符号位也就是完整的补码给存进来了。
被乘数采用双符号位的补码,而乘数采用单符号位的补码。因为MQ的最后一位会用来存放这儿所谓的辅助位,所以乘数就只能是单符号位的补码。
由于之后每一轮的加法有可能是加上 x 的补码,也就是直接加上通用寄存器里存的值,也有可能是要加上负 x 的补码。
为了实现 x 的补码到负 x 补码的一个快速转换,一般来说会有一个专门的辅助电路来完成这个事情。
通过辅助电路的处理之后,送到ALU 里边的数就会直接变成负 x 的补码的形式。
(3)手算模拟
来看一下如何用手算
的方式来模拟补码的一位乘法。
同样的图也是从王道书上来的,每一步的加法当中,我们都会把两个二进制比特位画上一个小小的下划线。
右边这一位就是我们刚才所谓的辅助位
,而左边这一位就是我们所谓 MQ 的最低位
。如下:
<1> 第一轮
刚开始由于辅助位-MQ 的最低位
等于 0-1,也就是-1,所以第一次我们需要加的应该是负 x 的一个补码。
如何由补码求负 x 的补码,这个问题我们在之前探讨过,这儿就不再赘述。
经过第一次的加法之后,我们得到了一个加和的值,这个加和的值其实是存在ACC,也就是累加寄存器里边的。
加法运算完成之后, ACC和 MQ里的内容会统一的进行一次右移,并且这个右移是算数右移,也就是符号位保持不动数值位新出现的空位,我们用和符号位相同的数来进行填补。
像下边这一步负数的右移我们就需要补一,我们在之前也探讨过,如果已经忘了的同学可以回去再复习一下。
在这个地方:
到这一步为止,我们完成了第一轮的加法,还有算术右移。
<2> 第二轮
接下来第二轮的加法。
由于此时辅助位减掉最低位是 1-1,所以第二轮的加法应该加上的就是 0 。加0之后得到了一个结果,再进行一次算数右移。这样我们就完成了第二轮的加法和右移。
<3> 第三轮
接下来由于辅助位减掉MQ 的最低位是等于1,所以往后应该是加上 x 的补码。
这次加法得到的是一个负数,符号位为1,所以此时需要进行负数的算数右移,在高位补1。这样我们就完成了第三轮的加法和右移。
<4> 第四轮
第四轮的加法和右移也是一样的,总共有 4 个数值位,所以需要进行四轮的加法,还有算数右移。不过我们之前说过,补码在最后还需要再多进行一次加法,只有加法没有移位。
此时由于辅助位减掉MQ 的最低位是等于1,所以最后的这一次加法应该是加上 x 的补码。
需要注意的是,最后这一次到底加多少?这点其实和我们原来乘数的符号位是有关的,这儿的 0 就是原本乘数的符号位。
所以我们才说在补码的乘法当中,乘数的符号位也是会参与运算的。
这次的加法得到的数,在拼接上 MQ 的前 n 个位,就可以得到最终的一个乘积结果,并且我们天然的得到了乘积的正负性。
这两个数 x 和 y 相乘,得到了一个负的值。
这样我们就完成了 x 乘以 y 的补码运算。
课本上的 Y5 指的是辅助位, Y4 指的是我们刚才说的MQ的最低位。以下分别是Y1、Y2、Y3、Y4、Y5。最高的这符号位,课本上把它记作了 Ys。
三、回顾
通过之前的例子,再来回顾一下我们刚开始对原码和补码的一个对比,相信大家能有更深的认识。
比起原码的乘法来说,补码乘法除了在进行 n 轮的加法和移位之外,最后还要多来一次加法。最后的这一次加法会让乘数的符号位也参与到运算当中。
另外,在补码乘法中,每一次的加法到底加什么,这点需要通过辅助位和 MQ 的最低位相减来进行判断到底要加什么。这为什么 MQ 的最低位给他打了一个双引号(如下图),大家应该知道是什么原因。
因为事实上 MQ 真正的最低位就是我们所谓的辅助位,我们只是为了和原码乘法的 MQ 最低位进行一个类比,这样更方便记忆。所
以我们采用这样的方式来描述。
另外再次强调,原码的乘法当中,每次移位操作是逻辑移位,符号位也会参与移动,并且高位永远是补0;而补码的移位都是算数右移,符号位是固定不变的,空出来的位到底补多少,具体得看补码的正负性,正数要补0,负数要补1。
以上就是定点数的乘法的一个实现思想。