206-MIPS 指令简介
# 206-MIPS 指令简介
MIPS 秉承着指令数量少,指令功能简单的设计理念,那这样的设计理念是如何实现的呢?在这一节,我们就将来分析 MIPS 指令的特点。
相比于 X86 指令所提供的动辄上千页的指令说明,MIPS 指令 只用这两页纸就可以说清楚了。
# 基本格式
MIPS 指令的基本格式就分为这三种,R 型,I 型和 J 型。R 型指的是寄存器型, I 型指的是立即数型,J 型指的是转移型。
我们用这张表对 MIPS 的指令进行不同纬度的分类,横轴是按照指令的格式分为 R 型、I 型和 J 型, 纵轴则是根据指令的功能类型分为运算指令、 访存指令和分支指令。
# R 型指令
首先,我们来看指令格式为 R 型的运算指令。 R 型指令总共包含六个域,其中最高位的 opcode 域 是六个比特,最低位的 funct 域也是六个比特, 中间的四个域,均为五个比特, 我们分别来看各个域的用途。
opcode 域,用于指定指令的类型,对于所有的 R 型指令,这个域的值,均为零, 但这并不是说明 R 型指令只有一种,它还需要用 funct 域来更为精确的指定指令的类型。所以说, 对于 R 型指令,实际上一共有 12 个比特操作码, 那大家可以思考一下,为什么不将 opcode 域和 funct 域合并成一个 12 比特的域呢? 那样岂不是更直观明了吗?
我们再来看这些 5 比特的域。
rs 域,这个域通常用来指定第一个源操作数所在的寄存器编号,
rt 域通常用来指定第二个源操作数所在的寄存器的编号,
rd 域通常用来指定目的操作数的寄存器编号, 也就是保存运算结果的地方。 5 个比特的域可以表示 0-31 的数, 正好对应 Mix 的体系结构中的 32 个通用寄存器,
还剩下最后一个域,它指示的是移位操作的位数。因为对于 32 比特的数,5 比特的域正好可以表示 0-31 的移位位数。那这个域只是对于移位指令有用,对于非移位指令,这个域被设为 0,
我们来看一个例子,这是将 9 号寄存器和 10 号寄存器中的数相加,把运算结果保存在 8 号寄存器中, 那我们通过这条汇编指令的描述,如何得到 mix 指令的二进制编码呢? 这其实很容易。首先,我们查询 MIPS 指令编码表,就可以得到 加法指令的 opcode 域应该是 0,funct 域应该是 32, 因为它不是移位指令,所以移位的域被设为 0,然后我们根据这条指令的操作数 可以得到目的操作数,也就说 rd 这个域等于 8,第一个源操作数应该是 9,第二个源操作数应该是 10, 这样我们把各个域的数值转换成二进制数,填写到对应的位置, 就可以得到这条指令的二进制编码了。
MIPS 指令系统简洁明了的规则可以让我们非常容易的对指令进行这样的手工编码转换, 同样也说明了 CPU 对这样的指令进行硬件的译码也会非常的方便。
# I 型指令
如果指令中需要用到立即数,那么就要用到 I 型指令, 因为 R 型指令当中只有一个 5 比特的域,也就说移位这个域 可以用来表示立即数,但能表示的数的范围为 0-31, 在程序中常用的立即数远大于这个范围,所以 R 型指令并不适用,我们需要新的指令格式。
这就是 I 型指令,I 型指令的大部分域与 R 型指令是相同的,
I 型指令的第一个域,也是 opcode 域,用于指定指令的类型,但它没有 funct 域,所以不同的 I 型指令,其 opcode 域是不一样的。
第二个域 rs,指定了第一个源操作数所在的寄存器编号,
第三个数 rt 用于指定目的操作数,I 型指令与 R 型指令不同,它只有两个寄存器数域, 剩下的 16 位被整合成了一个完整的域,可以存放 16 位的立即数, 可以表示 2 的十六次方个不同的数值。
对一般的访存指令,我们需要用一个寄存器,加上一个立即数来指示一个内存单元,那么这个立即数就是访存地址的偏移量,16 位的立即数,可以访问正负 32K 的空间,对于一般的访存指令来说,就可以满足了。
而对于运算指令,虽然无法满足全部的需求,但是大多数情况下,16 位也可以使用了。在这一点上,就可以体现出 X86 这样的 CISC 指令系统的优势,对于 X86 指令来说,如果它想使用更大宽度的立即数,它可以很容易的扩展,因为它的指令本来就没有限制长度。但是 MIPS 指令就不行,它的指令总长度就是 32 位的,再加上各个寄存器位域的使用,所以 I 型指令最多只能使用 16位的立即数 。
我们来看一个例子,对于加法,如果我们想让其中的 源操作数是一个立即数的话,就可以用 addi 这个指令, 注意它和 add 指令是不一样的。add 指令的操作数必须都是寄存器。 我们再来练习一下手工转换指令的编码。我们通过查指令编码表, 可以发现 addi 指令的 opcode 域是 8,从这一点我们也可以看出, addi 和 add 虽然只有一个字母的差别,但是他们指令格式是完全不一样的。 剩下的域我们通过分析这条指令的操作数就可以得到, rs 域,等于 22,rt 域=21,立即数域 等于-50,我们将这些数转换成二进制,就可以得到这条指令的编码了。
# J 型指令
然后我们来看所有的分支指令,分支指令是用于改变控制流的指令,其实就相当于 X86 当中的转移指令。 在 MIPS 中,分支指令也分为条件分支,和非条件分支两种。对于条件分支有两条指令,beq 和 bne, 对于非条件分支,只有一条指令,j
我们先来看条件分支指令,条件分支指令实际上是 i 型指令。 这就是两条条件分支指令,他们的 opcode 域分别是 4 和 5, 我们以 beq 指令为例,它共有三个操作数,前两个是寄存器操作数, 第三个操作数是存储器地址,也就是一个立即数,CPU 会判断第一个寄存器当中的数和第二个寄存器当中的数是否相等。如果相等就跳转到 L1 所指向的寄存器单元取出下一条指令,否则, 顺序执行 deq 之后的那条指令。
我们需要注意,这里和 X86 的条件转移指令有很大的不同。MIPS 没有标志寄存器,它就在一条指令当中即进行了比较,又完成了转移。我们还记得 MIPS 的全称,就是为了减少指令流水线的互锁,也就说要尽量避免不同指令之间相互的影响。而标志位这件事,很明显就是前一条指令运行的结果,可能会对后面的某一条指令产生影响,这是 MIPS 指令设计时要尽量避免的。所以BEQ 指令也很好的体现了 MIPS 的这一设计理念。
我们来看一个例子。这段 C 语言代码是我们经常会写的。 如果把它转换为 MIPS 指令,是这样的,第一条 BEQ 指令, 如果 S3 寄存器和 S4 寄存器内容相同,则转移到 True 所对应的这行指令。那么 S3 和 S4 中保存了 I 和 J 这两个变量, 如果他们内容相同,会转移到这里,执行加法指令, 也就对应于 F=G+H
如果他们不等, 则会顺序的执行下一条指令,也就一条减法指令对应于 F=G-H 执行完之后,会跳过这条加法指令,然后进入后面的代码
从条件分支指令的格式可以看出,目标地址只能使用十六位的位移量, 这是一个很大的局限,但是我们还得考虑如何充分发挥这十六位的作用。 如果以当前的 PC 寄存器为基准,在 MIPS 中,指向下一条指令地址的寄存器称为 PC, 类似于 X86 中的 IP 寄存器。这个寄存器,是指向 32 位寄存地址的。 如果以它为基准(peterjxl 注,应该是指段寄存器),十六位位移量可以表示出当前指令前后 2 的 15 次方字节这么一个范围。
但是我们要注意一点,MIPS 的指令长度固定位 32 个比特,因此每条指令的位置, 一定会在四个字节对齐的地方,这样的地址,最低两位肯定为 0。 所以我们实际上可以用十六位的位移量去指示每四个字节为一个单位的地址。 这样就可以把目标地址的范围扩大四倍,可以达到前后 128kB。(这里看的不是很懂)
在这样的条件下,目标地址应该这么计算, 当分支条件不成立时,下一条指令的地址就等于当前的 pc+4。
如果分支条件成立,那下一条指令的地址就等于已经加了 4 的 pc, 再加上这个立即数乘以四。
然后我们来看非条件分支指令,相比于条件分支指令,有两个寄存器域用于比较条件,那如果我们不需要判断条件,我们就可以想办法扩大目标地址的范围。 当然理想情况下是直接使用 32 位的地址, 但还是因为 MIPS 的指令长度固定为 32 位,而每条指令至少需要有 opcode 域,指示它的指令类型。 这就占用了六个 bit。那我们把剩下的 26 个 bit 全都用于目标地址, 这就是 J 型指令。
在考虑到 MIPS 指令是四字节对齐的这个情况, 对于 J 型指令,下一条指令的地址的计算方法可以是将当前的 pc 加四之后, 取最高的四位,再加上 J 型指令编码中的 26 位, 然后在末尾填上两个零,虽然目标地址的范围还不能达到整个 4G 的空间,但比之前的条件分支指令已经扩大了很多。
我们用一个例子来进行进一步的说明。 假设我们在高级语言中用的若干变量与寄存器的对应关系是这样的, 那我们就可以用这样一种方式来实现这段 c 语言的代码, 第一条指令是判断 i 和 j 是否相等,如果不相等, 则转移到 else 这个标号所对应的位置, 也就是执行一条减法指令对应于 f=g-h,如果判断条件成立, 也就是 i=j 的时候,顺序地执行下一条加法指令, 也就对应于 f=g+h。然后用无条件分支指令跳到 else 条件之后继续执行后面的程序。
我们现在已经知道这个 J 型指令的目标地址可以是当前指令前后 256MB 的范围,那如果我们还想跳转到更远的地址, 应该怎么办呢?有一个很简单的方法 就是两次调用 J 指令,第一条 J 指令尽可能跳到最远的地方, 然后在那个目标地址再放一条 J 指令,像接力一样再跳一次。 这个方法很简单,但是用起来不算太方便,那么还可以用什么方法呢? 大家还记得我们曾说过间接转移指令吗?MIPS 中也可以用同样的方法, 这就是 jr 指令。jr 指令有一个寄存器操作数, 可以把要转移的目标地址放到寄存器当中,这样就可以使用 32 位的目标地址了, 但是这样的指令显然无法用 J 型指令来实现, 那么需要新增一种指令类行吗?其实也不需要,我们就用原来的 r 型指令就可以很好的实现。 只用占用其中的一个寄存器位域,然后新增一种 function 的编码就可以了。 这就是 MIPS 指令系统的核心内容,我们只用熟悉这两页的内容就可以轻松的掌握 MIPS 的指令了。
我们已经介绍完了 MIPS 制定系统体系结构, 它不愧为精简指令系统的经典设计,指令简洁,而且精巧。
# 扩展
arm,mips架构,为什么采用字节对齐有利于性能提升?? - 知乎 (opens new window)
假设我的 CPU 是 32 位数据线,那么一次取数据必须是 4 字节,不可能只取一字节。如果我非要取一字节的话,也只能从内存里一次取 4 字节,然后把其中的一字节拿来使用。
回到开始的 32 位假设,我若是要取地址为 0~3 中的任意一个字节,很容易实现,CPU 会把取到的 4 个字节中拿一个给你用。具体算法是当地址部件发出逻辑地址的时候,CPU 会把地址信号中的高 30 位当成 32 数据线的地址偏移,低 2 位会当成 32 位数据线的线内偏移。
如果你取 0~3 字节的内容,高 30 位是 0,数据线只需要传输第 0 个 32 位的数据过来给你选择就行了,如果取 4~7 字节的内容,高 30 位就是 1,只需要传输第 1 个 32 的数据过来,然后 4 号地址对应的偏移是 0,7 号地址对应的偏移是 3,以此类推。这是取单字节的情况。也就是说 CPU 传输数据的时候始终是 4 字节对齐的,从 0 开始,从 4 开始。假如一次取 2 个字节的话,假设数据线对准了高 30 位对准了 0,那么你可以从 0、1、2 取两个字节,但是不能从 3 取,因为从 3 取的话还需要取第四字节的内容,这时候需要把地址线的高 30 位调整成 0000000……0001 来。如果一次取 3 字节的话,只能从 0、1 开始,对于一次取 4 字节的话只能从 0 开始了。
通俗来说就是“如果一次取一个数据块出来,必须使这个块在数据线对准的范围之内,否则只能移动数据线对准的地方,也就是多次才能取出来”。这就是地址对齐的原因了,而且大部分 CPU 不能一次取 3 字节。对于一次取一个字节,无需对齐,因为数据线始终能在第一次瞄准的时候就把这个字节取出来。其他数据块的读取就需要考虑 CPU 的数据宽度了
发布于 2015-01-28 13:02
作者:花藤麻
链接:https://www.zhihu.com/question/26856453/answer/38277147
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如果你有两张银行卡都有 1000 块,你要取 1000 块当零花,你是直接到 ATM 从一张里取 1000 方便呢还是跑两个银行每张各取 500 方便?
处理器可以在 1 个时钟周期内从对齐的地址中取出相应字节数到寄存器中,而如果地址不对齐,必须在两个对齐的相邻地址中各取一部分,拼在一起,一般来说至少是 2 个时钟周期。
作者:Felix Qiu 链接:https://www.zhihu.com/question/26856453/answer/34298078 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。