2 指令系统原理与实例
- 指令集系统结构的分类:指令集系统的不同结构
- 存储器寻址:大小端模式及地址对齐
- MIPS系统结构:MIPS指令集结构
2.1 简介
第一章提到的应用领域:
桌面计算:注重程序的定点和浮点运算性能,不注重程序的大小以及处理器的功耗。
服务器及集群:主要应用于数据库、文件服务器、Web应用等。注重定点和字符串方面的性能,有浮点指令不注重。
嵌入式应用和个人移动设备:注重成本和功耗,代码量大小很重要。一些指令(如浮点指令)作为可定制的选项。
指令系统在这三个应用领域的情况很相似。
本章的MIPS系统结构在桌面、服务器及嵌入式应用中均有广泛应用。
2.2 指令集系统结构的分类
★指令集系统结构
指令集系统结构(Instruction Set Architecture, ISA)最根本的区别:在于处理器内部数据的存储类型不同。
存储类型
存储类型:堆栈、累加器或一组寄存器。操作数可以显式指定或者隐含指定。
(1)堆栈系统结构中操作数隐含地位于栈顶
(2)累加器系统结构中的一个隐含操作数就是累加器。
(3)通用寄存器结构系统中只能明确地指定操作数,不是寄存器就是存储器地址。
不同的指令集系统结构的示意图
箭头指示操作数是输入还是ALU运算的结果,或者既是输入也是结果。灰度较轻表示输入,较重表示结果。
(a)中,栈顶寄存器(TOS)指向堆栈顶部的输入操作数,并与下面的操作数结合在一起。第一个操作数被从堆栈中删除,运算结果存放在第二个操作数的位置,同时栈顶寄存器指向运算结果。所有的操作数都是隐含的。
(b)中,累加器既是隐含的输入操作数也运算结果。
(c)中,其中一个操作数在寄存器中,另一个在存储器中,运算结果存放在寄存器中。
(d)中,所有的操作数都是寄存器,与堆栈结构类似,也只能通过一些单独的指令传输到存储器中:(a)中是push或pop, (d)中是load或store。
例子
下表说明代码C=A+B在这三类系统结构中分别是如何表示:(设A,B和C都在存储器中且A和B的值不破坏)
| 堆栈 | 累加器 | 寄存器(Reg-mem) | 寄存器(load-store) |
|---|---|---|---|
| Push A | Load A | Load R1,A | Load R1,A |
| Push B | Add B | Add R1,B | Load R2,B |
| Add | Store C | Store R1,C | Add R3,R1,R2 |
| Pop C | Store R3,C |
说明:在堆栈结构和累加器结构中加法指令的操作数是隐含的,而在寄存器结构中操作数必须明确指定。
通用寄存器系统结构
按照通用寄存器访问方式划分,有两种通用寄存器系统结构的计算机:
(1) register-memory系统结构,一般指令都可以访问存储器。
(2) register-register或load-store系统结构,只能通过load和store指令来访问内存
教材提到的纯 memory-memory系统结构,现实中不存在的结构,把所有的数据都保存在存储器中。
此外,有的ISA在累加器外扩展了其他寄存器,称为扩展累加器计算机。
通用寄存器(GPR)出现的原因
- 寄存器比存储器快
- 编译器使用寄存器很方便,比使用其他存储形式效率更高。
例如:(A*B) – (C*D) –(E*F) 在寄存器系统结构的计算机上,可以按任意顺序来执行三个乘法,但是在堆栈计算机上则只有一种计算顺序,因为操作数隐含在堆栈中,且必须多次载入。
寄存器用来存放变量,减少了数据流量,加速程序运行(寄存器比存储器快);改善代码密度(寄存器地址比存储器地址的位数少)。
通用寄存器ISA的两个特性
- ALU指令中包括两个还是三个操作数。在三个操作数格式中,指令包含一个结果(目的操作数)和两个源操作数。在两个操作数的格式中,有一个既是结果操作数也是源操作数。
- ALU指令中包括多少个存储器操作数。典型的ALU指令中所支持的存储器操作数的数量可能是从0~3个不等。
常见通用寄存器
| 存储器地址个数 | 最多操作数个数 | 系统结构类型 | 举例 |
|---|---|---|---|
| 0 | 3 | Load-store (Reg-Reg) | Alpha, ARM, MIPS, PowerPC, SPARC, superH, TM32 |
| 1 | 2 | Reg-Mem | IBM360/370, Inter80x86, Motorola 6800, T1 TMS320C54x |
| 2 | 2 | Mem-Mem | VAX (也有3个操作数的格式) |
| 3 | 3 | Mem-Mem | VAX (也有2个操作数的格式) |
三种常见通用寄存器计算机的优缺点
| 类型 | 优点 | 缺点 |
|---|---|---|
| Reg-Reg(0, 3) | 简单、定长的指令编码; 简单的代码生成模式; 每条指令运行的时钟周期数相近 |
指令数比可以直接访问存储器的系统结构多; 指令多和指令密度低使程序变得很大 |
| Reg-Mem(1, 2) | 数据不需要专门的载入指令就可以直接访问; 指令格式更加易于编码,代码密度高 |
由于源操作数在二元操作中被破坏了,所以操作数不是等价的; 在一条指令中同时对存储器地址和寄存器号码进行编码会限制寄存器的数量; 操作数位置不同使得每条指令执行所需的时钟周期不同 |
| Mem-Mem (2, 2)或(3, 3) | 最紧凑。 不浪费寄存器来做临时交换空间 |
指令长短不相同,特别是三操作数指令; 同样,每条指令的操作各不相同; 存储器访问带来了存储器瓶颈 |
2.3 存储器寻址
存储器地址表示
我们讨论的所有指令系统都是字节寻址的,都提供了字节(8位)、半字(16位)和字(32位)寻址,大多数的计算机还提供了双字(64位)寻址。
小端模式&大端模式
小端模式把地址为“X…X000”的字节放在整个字的最低有效位置上(低地址存低字节)
大端模式把地址为“X…X000”的字节放在整个字的最高有效位置上(低地址存高字节)(和阿拉伯数字相同,最高位在最开始)
| 地址(从低到高) | 00 | 01 | 10 | 11 |
|---|---|---|---|---|
| 小端模式 | 个 | 十 | 百 | 千 |
| 大端模式 | 千 | 百 | 十 | 个 |
对齐
假设一个 s 字节数据的地址是 A ,如果 A mod s = 0 ,访问该地址就是对齐的。
为什么要有对齐限制?
- 字或双字整数倍对齐访问存储器:简化硬件实现的复杂性。
- 一次不对齐的存储器访问:导致多次对齐存储器访问。因此,即使是在没有对齐限制的计算机里面,对齐访问的程序也会运行得比较快。
寻址方式
寻址方式:指令中如何指定所要访问操作数的地址。寻址方式要指定常量、寄存器和存储器操作数的位置。
常见寻址方式
后图列出了近期计算机中使用的所有数据寻址方式。
- 立即数通常也被认为是一种存储器寻址方式(尽管它们要访问的数值在指令流里)。
- 寄存器不属于存储器寻址。
- 把依赖于程序计数器的PC相对寻址(后面详细讨论)也分离出来。
| 寻址方式 | 指令举例 | 含义 | 何时使用 |
|---|---|---|---|
| 寄存器寻址 | Add R4,R3 | Regs[R4]←Regs[R4]+Regs[R3] | 数值在寄存器中 |
| 立即数寻址 | Add R4,#3 | Regs[R4]←Regs[R4]+3 | 数值是常量 |
| 位移量寻址 | Add R4,100(R1) | Regs[R4]←Regs[R4] +Mem[100+Regs[R1]] | 存取局部变量(+模拟寄存器间接、直接寻址) |
| 寄存器间接寻址 | Add R4,(R1) | Regs[R4]←Regs[R4]+Mem[Regs[R1]] | 使用指针或者计算出的地址进行寻址 |
| 间接寻址 | Add R3,[R1+R2] | Regs[R3]←Regs[R3] +Mem[Regs[R1]+[Regs[R2]] | 有时用在数组寻址中 |
| 索引寻址 | Add R1,(1001) | Regs[R1]←Regs[R1] +Mem[1001] | R1是数组的基址,R2是索引值用来存取静态数据;地址常量可能需要很大 |
| 存储器间接寻址 | Add R1,@(R3) | Regs[R1]←Regs[R1] +Mem[Mem[Regs[R3]]] | 如果R3是指针p的地址,那么就得到*p |
| 自动递增寻址 | Add R1,(R2)+ | Regs[R1]←Regs[R1]+Mem[Regs[R2]] Regs[R2]←Regs[R2]+d | 用在循环中递增变量,R2是数组的起始地址,每次增加d |
| 自动递减寻址 | Add R1,-(R2) | Regs[R2]←Regs[R2]-d Regs[R1]←Regs[R1]+Mem[Regs[R2]] | 和自动递增类似,自动递增/递减用来实现类似堆栈的push/pop功能 |
| 比例寻址 | Add R1,100(R2)[R3] | Regs[R1]←Regs[R1] +Mem[100+Regs[R2]+Regs[R3]*d] | 用来对数组寻址。一些计算机可以用任意的索引(间接)寻址方式 |
寻址方式的使用频率
后图给出了VAX系统结构的计算机上用三个程序所测试出的存储器寻址方式(寄存器寻址不在其中)的结果。
采用VAX系统结构的计算机来进行测试,是因为它有丰富的寻址方式,且对存储器寻址限制很少。
如图所示,立即数寻址和位移量寻址是用得最多的寻址方式。
位移量寻址方式
主要问题是位移的范围,即多长的位移量。
选择位移量的长度:直接影响到指令的长度。
后图列出了Alpha(16位位移量)上运行SPEC CPU2000的CINT2000和CFP2000测试程序数据访问测试结果的平均值。
如上图所示,位移量值分布范围很广。由于变量的存储位置和存取变量方式的不同以及编译器使用的整个寻址方式的原因,位移量分布范围很广。
结论:8位~16位位移量是有必要的。如MIPS采用16位位移量。
立即数寻址方式
立即数的取值范围:与位移量相同,立即数的数值大小会影响指令的长度。
如后图所示的立即数分布中,小立即数是最常用的。有时也使用大立即数,特别是在寻址计算中。
后图列出了Alpha(最大16位立即数)上运行SPEC CPU2000的CINT2000和CFP2000测试程序结果的平均值。
X轴代表要表示一个立即数所需要数值的位数(0表示立即数的数值为0)。大多数立即数的值是正的,在CINT2000中20%的立即数是负的,CFP2000中为30%。
在一台支持32位立即数的VAX计算机上进行相同的测试,结果显示有20%-25%的立即数大于16位。这样,16位及小于16位的大约占80%,8位及小于8位的大约占50%。
结论:8~16位立即数是有必要的。如MIPS和ALPHA采用16位立即数。
立即数寻址常用于算术运算指令、载入常数到寄存器指令、比较指令(主要是条件转移指令)。
立即数寻址:是支持所有操作还是只支持一部分操作对设计指令系统很重要。下面的统计图表列出了一个指令系统中常见定点操作使用立即数的频率。
大约1/4的数据传输和定点ALU操作有一个立即数操作数。
图中最下面的横条表明在定点程序中有约1/5的指令用到立即数,在浮点程序中这个比例约为1/6。
2.4 操作数的大小和类别
操作数的类型如何指定
第一种,操作数类型可以通过操作码的编码来指定,是最常用的方法;
第二种,操作数中用硬件解释的字段表示数据类型。
常见的操作数类型
基准测试程序中数据访问的大小分布
64位处理器中基准测试程序中数据访问的大小分布:
由于系统为64位地址,双字数据类型可用于浮点程序中的双精度和存储器地址。在32位地址的计算机中,64位地址将被32位地址取代,定点程序中几乎所有的双字访问都转换成单字访问。
在一些系统结构中,寄存器的数据可能以字节或者半字来访问(这种情况很少发生)。在VAX计算机中,统计数据表明这种情况不超过所有寄存器的12%,大约占这些程序中所有操作数访问的6%。
2.5 指令系统的操作
大多数指令集系统结构支持的操作:
| 操作类型 | 举例 |
|---|---|
| 算术和逻辑运算 | 定点算术和逻辑操作:加、减、与、或、乘、除 |
| 数据传输 | Load-store指令(在REG-MEN结构计算机上是传送指令) |
| 控制 | 条件转移、跳转、过程调用和返回、陷阱 |
| 系统 | 操作系统调用、虚拟存储器管理指令 |
| 浮点 | 浮点操作:加、乘、除、比较 |
| 十进制 | 十进制加、十进制乘、十进制到字符的转换 |
| 字符串 | 字符串传送、字符串比较、字符串匹配 |
| 图像 | 像素、顶点操作、压缩/解压缩操作 |
指令系统有一条共同的规律:
使用最多的是一些简单指令。一般所有的计算机都提供前三类指令。指令集对后四类指令的支持数量可能为0,也可能包含大量特殊指令。
80X86中执行最多的前10类指令:
排名 80x86指令 定点平均值(占百分比) 1 载入 22% 2 条件转移 20% 3 比较 16% 4 存储 12% 5 加 8% 6 与 6% 7 减 5% 8 Reg-Reg传输 4% 9 调用 1% 10 返回 1% 总计 96% 这80x86上运行的定点程序中,10类占96%。因此,它们是最常用的指令,执行起来应该尽量快。
2.6 控制流指令
控制流指令分类
- 条件转移
- 跳转(无条件转移)
- 过程调用
- 过程返回
控制流指令不同类型出现的相对频率
控制流指令的寻址方式
一般要指明转移的目标地址。过程返回是例外,因为编译时不知道返回地址。
PC相对寻址
即使用基于程序计数器(PC)的位移量来指定目标地址。
PC相对寻址指令的优点:目标与当前指令离得不远;使用相对偏移地址可以缩减指令长度。使用相对寻址的程序可以载入到主存任何位置,称为位置无关,对在执行时才链接的程序可以减少工作量。
PC相对寻址转移距离:转移指令与目标之间的指令数
定点相对寻址转移位移量:至少8位
寄存器间接跳转
编译时不知道目标位置,为了实现返回和间接跳转,需要使用寄存器间接跳转:给出包含目标地址的寄存器名称。
寄存器间接跳转还支持:
- 分支选择语句case或者switch
- 面向对象语言中的虚拟函数或者方法
- 高阶函数或者函数指针
- 动态共享库
以上四种情况,编译时都不知道目标地址,因此通常在寄存器间接跳转之前,才把地址从存储器载入到寄存器中。
控制流指令的可选方案
条件转移的可选方案
| 名称 | 举例 | 如何测试条件 | 优点 | 缺点 |
|---|---|---|---|---|
| 条件码(CC) | 80x86,ARM,PowerPC,SPARC,SuperH | 由ALU操作设定的某些特定位,可能是由程序控制的 | 有时条件可自由设置 | CC是附加状态。条件码改变了指令顺序 |
| 条件寄存器 | Alpha | 用比较结果测试任意寄存器 | 简单 | 占用一个寄存器 |
| 比较并转移 | MIPS,PA-RISC,VAX | 比较是转移的一部分,通常比较只限于子集内部 | 一个转移是一条而不是两条指令 | 对流水线执行来说,一条指令要做的事情可能太多了 |
不同类型条件转移中的频率
过程调用的可选方案
有两种基本、传统的方法用来保存寄存器:调用者保存和被调用者保存。
- 调用者保存:调用者调用其他过程时,必须保存在调用过程后还要使用的寄存器,被调用者则无须维护这些寄存器。
- 被调用者保存:被调用的过程必须保存它要使用的寄存器,调用者则不受这种限制。
有时候,如果两个不同的过程都要访问相同的全局变量,则必须使用调用者保存方法。大多数实际使用的编译器会结合这两种方法。
2.7 指令系统的编码
前面讨论的问题都会直接影响指令如何编码。
指令编码
指令编码包括:操作、寄存器地址、寻址方式编码。
指令编码影响编译后程序大小和处理器实现。
对指令编码,需要在以下因素之间找到一个最佳平衡点:
1、尽可能多的寄存器和寻址方式。
2、寄存器字段、寻址方式字段尽量少,以缩短指令长度。
3、指令长度易于流水线处理。
三种常见编码方式
变长编码
变长编码(如VAX和Intel 80x86):允许所有的操作使用所有的寻址方式,适合寻址方式和操作比较多的情形。最少的位数表示程序,译码复杂,不适合流水线。
定长编码
定长编码(如Alpha,ARM,MIPS,PowerPC,SPARK,SuperH):把操作和寻址方式组合在操作码里,通常所有的指令长度都相同。这种方式适合于寻址方式和操作比较少的情况。译码简单,适合流水线,代码量大。
混合编码
混合编码(如IBM360/370,MIPS16,Thumb, TI TMS320C54x):混合方法,减少过多的指令,以减轻多种结构的指令带来的工作负担,但仍提供多种指令长度以减少代码长度。
选择编码方式
关注代码量大小,选择变长编码。
关注程序的执行性能,选择定长编码。
折中可以选择混合编码。
2.8 相关问题:编译器的角色
编译器功能
将高级、抽象表示方式逐步转换成低级表示形式,最终到达机器目标指令代码。
机器运行的大多数指令代码是编译器的输出。
- 早期:编译器是在机器实现后再开发的。
- 现在:机器性能不仅是其原始速度,还包括编译器如何利用系统结构特征。(编译器在处理器设计阶段开发,编译后的代码在模拟器上运行,代码用来评估系统结构特征)
编译器开发的二个主要目标:
- 正确性
- 编译后的代码的执行速度。重要的是针对当前系统结构的优化技术。
与编译相关的主要系统结构特征
(1)并行性
指令级并行:编译器通过重新调度指令顺序,配合硬件使指令高效执行。
处理器并行:程序员编写多线程,编译器自动生成并行代码。
(2)存储层次结构:寄存器—Cache—主存—外存
高效使用寄存器是优化处理的重要问题。
对于数组计算,改变数据布局或数据访问代码顺序提高访存效率;改变Cache代码布局提高访问命中率。
指令系统的设计方案的选择影响
编译器产生指令代码的质量,如代码大小、代码复杂度、代码效率。
构建一个高效编译器的复杂度。
近来编译器的结构
近代编译器一般包括2~4遍扫描,而性能更高的编译器包括更多次的扫描。
编译目标(较高编译速度、较低代码质量)可接受时,优化扫描可以被跳过,一遍扫描即可。优化扫描被设计成一个可选项。
由于优化扫描是独立的,因此同一种ISA上不同的语言可以使用相同的优化与代码生成步骤。对于一个新的语言,只需要一个新的前端程序就可以了。
编译器优化方式的分类
-
高层优化,一般在源码上进行,同时把输出传递给以后的优化扫描步骤。
-
局部优化,仅在一系列代码片段之内将代码优化。
-
全局优化,将局部优化扩展为跨越分支,并且引入一组针对优化循环的转换。
-
寄存器分配,将寄存器与变量联系起来。
-
处理器相关的优化,充分利用特定的系统结构。
2.9 MIPS系统结构
MIPS64系统结构
一种简单64位 load-store 系统结构。
固定长度指令编码,译码简单,有利于实现高效率流水线,使编译器更容易产生高效的目标代码。
MIPS64的寄存器
32个64位通用寄存器(GPR),名称为R0,R1,…,R31,也称为寄存器。
32个浮点寄存器(FPR),名称为F0,F1, …,F31,即可以作为32个32位单精度寄存器来使用,也可以作为32个64位双精度寄存器来使用。
R0的值永远是0。
MIPS64的数据类型
定点数据类型有8位字节、16位半字、32位字和64位双字。
浮点数有32位单精度和64位双精度浮点数。
字节、半字或者字在调入64位寄存器中时,用零或者符号来填充64位寄存器的剩余部分。
MIPS64数据传输的寻址方式
- 16位立即数
- 16位位移量方式(基址寻址),即操作数地址是一个存放在寄存器中的基地址与相对该基址的一个16位移量相加获得。
例如,载入字指令LW的具体使用形式如下:LW R2, 128 (R3)
含义:( (R3) + 128 ) → R2
特殊情况:
- 位移量为0:寄存器间接寻址
- R0作为基址寄存器:16位绝对寻址
- MIPS64存储器是用64位地址字节寻址的。
- MIPS32存储器是用32位地址字节寻址的。
MIPS64和MIPS32的指令位数都是32。
MIPS的指令格式
由于MIPS64只有两种寻址方式,所以它们编码到操作码中。为了使机器更容易进行流水线操作和译码,所有指令都是32位的,其中6位是基本操作码。
指令的格式如下图。这些指令格式很简单,同时还为位移量寻址、立即数或PC相对分支地址提供了16位字段。
MIPS操作
大致可以分为四类:载入和存储、ALU操作、分支与跳转、浮点操作。
所有通用寄存器与浮点数寄存器都可以被载入或存储,唯一的例外是载入R0无效。
载入指令
下图给出了载入和存储指令的例子。都只使用一种寻址方式,并且要求存储器的值必须对齐。当然,载入和存储指令对所有的数据类型都是有效的。
- 当被传送的数据长度不确切时,在符号←上附加一个下标,表示传送n位。
- 下标用于标识字段中特定的位。位从以0开始的最高位开始标注。
- 变量Mem用来表示主存储器(内存),按字节编址,可以传输任意字节的数据。
- 上标用来表示对字段进行复制。
- 符号##用来链接两个字段并且可以出现在数据传送的任何一边。
| 指令举例 | 指令名称 | 含义 |
|---|---|---|
| LD R1,30(R2) | 载入双字 | Regs[R1] ←64\xleftarrow{64} Mem[30+Regs[R2]] |
| LD R1,1000(R0) | 载入双字 | Regs[R1] ←64\xleftarrow{64} Mem[1000+0] |
| LW R1,60(R2) | 载入字 | Regs[R1] ←64\xleftarrow{64} (Mem[60+Regs[R2]]0)32##Mem[60+Regs[R2]] |
| LB R1,40(R3) | 载入字节 | Regs[R1] ←64\xleftarrow{64} (Mem[40+Regs[R3]]0)56##Mem[40+Regs[R3]] |
| LBU R1,40(R3) | 载入无符号字节 | Regs[R1] ←64\xleftarrow{64} 056##Mem[40+Regs[R3]] |
| LH R1,40(R3) | 载入半字 | Regs[R1] ←64\xleftarrow{64} (Mem[40+Regs[R3]]0)48##Mem[40+Regs[R3]]##Mem[41+Regs[R3]] |
| L.S F0,50(R3) | 载入单精度浮点数 | Regs[F0] ←64\xleftarrow{64} Mem[50+Regs[R3]]##032 |
| L.D F0,50(R2) | 载入双精度浮点数 | Regs[F0] ←64\xleftarrow{64} Mem[50+Regs[R2]] |
存储指令
| 指令举例 | 指令名称 | 含义 |
|---|---|---|
| SD R3,500(R4) | 存储双字 | Mem[500+Regs[R4]]←64\xleftarrow{64}Regs[R3] |
| SW R3,500(R4) | 存储字 | Mem[500+Regs[R3]]←32\xleftarrow{32}Regs[R3]32..63 |
| S.S F0,40(R3) | 存储单精度浮点数 | Mem[40+Regs[R3]]←32\xleftarrow{32}Regs[F0]0..31 |
| S.D F0,40(R2) | 存储双精度浮点数 | Mem[40+Regs[R3]]←64\xleftarrow{64}Regs[F0] |
| SH R3,502(R2) | 存储半字 | Mem[502+Regs[R2]]←16\xleftarrow{16}Regs[R31]48..63 |
| SB R2,41(R3) | 存储字节 | Mem[41+Regs[R3]]←32\xleftarrow{32}Regs[F0]56..63 |
ALU指令
所有的ALU指令都是寄存器-寄存器指令,包括算术和逻辑操作:加、减、与、或、异或和移位。下图给出了一些算术和逻辑指令的例子。所有这些指令都支持立即寻址方式,它带一个16位的符号扩展立即数。
| 指令举例 | 指令名称 | 含义 |
|---|---|---|
| DADDU R1,R2,R3 | 无符号加 | Regs[R1]←Regs[R2]+Regs[R3] |
| DADDIU R1,R2,#3 | 加无符号立即数 | Regs[R1]←Regs[R2]+3 |
| LUI R1,#42 | 载入立即数到高位 | Regs[R1]←032##42##016 |
| DSLL R1,R2,#5 | 逻辑左移立即数 | Regs[R1]←Regs[R2]<<5 |
| DSLT R1,R2,R3 | 置小于 | If(Regs[R2]<Regs[R3]) Regs[R1]←1 else Regs[R1]←0 |
MIPS控制流指令
比较指令,比较两个寄存器的值。如果第一个寄存器的值小于第二个的值,则比较指令将置目标寄存器为1(代表真),否则将置为0(代表假)。由于这些操作都设置寄存器,因此它们称为置等于、置不等于和置小于,等等。同时这些比较指令也具有立即数的形式。
控制由一组跳转与一组分支来处理。下图给出了几个典型的分支和跳转指令。
| 指令举例 | 指令名称 | 含义 |
|---|---|---|
| J name | 跳转 | PC36.63←name |
| JAL name | 跳转并链接 | Regs[R31]←PC+4; PC36.63←name; ((PC+4)-227)≤name<((PC+4)+227) |
| JALR R2 | 寄存器跳转并链接 | Regs[R31]←PC+4;PC←Regs[R2] |
| JR R3 | 寄存器跳转 | PC←Regs[R3]; |
| BEQZ R1,name | 等于零时分支 | If(Regs[R1]==0) PC←name; ((PC+4)-217)≤name<((PC+4)+217) |
| BNE R3,R4,name | 不等于零时分支 | If(Regs[R3]!=(Regs[R4]) PC←name; ((PC+4)-217)≤name<((PC+4)+217) |
| MOVZ R1,R2,R3 | 等于零时转换 | If(Regs[R3]==0) Regs[R1]←Regs[R2] |
分支的目标地址=16位带符号位移量左移2位 +(PC+4)
MIPS的浮点操作
浮点指令对浮点数寄存器进行操作,并指出将被使用的操作数是单精度(SP)还是双精度(DP)。
-
MOV.S 和 MOV.D分别把一个单/双精度浮点数寄存器的值复制到另一个同类型的寄存器中。
-
MFC1,MTC1,DMFC1和DMTC1在一个单精度或双精度浮点数寄存器和一个定点寄存器之间传送数据。
浮点操作:加、减、乘、除。后缀S表示单精度浮点数,而后缀D表示双精度浮点数。
为提高图形处理性能,MIPS64提供了在一个64位浮点数寄存器两半部分中分别进行两个32位浮点数的操作。
MIPS指令系统的使用情况
为了说明哪些指令更常用,后图给出了5个SPECint2000程序的指令类别和指令调用频率。
下图给出了5个SPECfp2000程序的指令类别和指令调用频率。
2.10 谬误和易犯的错误
易犯的错误:设计一种“高级”指令系统特性就意味着要支持一种高级语言结构。
为了支持高级语言的特征,设计者们会设计出一些灵活性很强、强有力的指令。但是,这些指令经常会做些需求以外的工作,或者不能准确地符合一些语言的要求。曾经有很多努力花在20世纪70年代 “语义缺口”问题上。虽然是想补充指令系统使得硬件的功能达到语言的水平,但是,这些增加的功能却又导致了被Wulf[1984]称为语义冲突问题:
由于给指令赋予了过多内容,因此机器的设计者仅能够在有限的环境中使用指令。
对于经常发生的情况来说,这种指令的功能通常过于强大,这就导致了许多不必要工作以及指令速度降低。
谬误:存在一种典型的程序。
很多人都倾向于相信存在一个典型的程序,可以用它来设计一个理想的指令系统。我们可以参考综合基准测试程序,其中的数据清楚地表明应用程序在使用指令系统方面存在显著的差别。
例如,如下图所示的在四个SPEC2000的程序中数据传输大小的情况:很难说这四个程序中哪一个是典型的。对于专门支持一类应用的指令系统,这种差异可能会更大,例如十进制指令在其他应用中就不会被使用。
谬误:可以不考虑编译器而改进指令系统以缩减代码大小。
下表显示的MIPS指令系统下四个编译器产生的EEMBC的通信程序相对代码大小。尽管设计师努力使代码减少了30%~40%,不同的编译器策略却更大程度地影响着代码的大小。就像性能优化技术一样,在试图改进硬件以节约空间之前首先应该考虑编译器如何产生较少的代码。
谬误:有缺陷的系统结构不可能是一种成功的系统结构。
80x86提供了一个很明显的例子:它的结构只有它的设计者才喜欢。Intel的工程师们试图更正80x86设计中不受欢迎的设计。例如, 80x86支持段式存储,而所有其他的机器都选择了页式存储; 80x86对整型数据使用扩展的累加器,而其他的机器使用通用寄存器; 80x86使用一个堆栈来保存浮点数据,而所有其他的机器在很久以前就废弃了执行堆栈。
尽管存在这些缺陷,80x86系统结构还是获得了巨大的成功。主要有三个方面原因:第一,它被选为IBM PC的微处理器,使得在二进制上与80x86兼容变得非常重要;第二,摩尔定律保证了有足够的资源可使80x86在内部转化为RISC指令系统,然后执行类RISC指令。这两点使得它与大量PC软件二进制兼容,在性能上和RISC处理器不相上下;第三,大量的处理器出货使Intel有能力支付这种高代价的硬件转化。此外,大量的处理器出货使生产厂家可以跟得上学习曲线,这也在一定程度上降低了成本。
谬误:可以设计一个没有缺陷的系统结构。
所有系统结构的设计都涉及一系列硬件和软件技术之间的折中。这些技术可能会随着时间的发展而发生变化,那些在设计者当初看来好像是正确的决定事后可能证明是错误的。
例如,在1975年,VAX的设计者们过分强调了代码大小的重要性,而没有估计到5年后译码的简化和流水线是那么重要。RISC系统中一个典型的例子就是延迟分支(见附录J)。对于5级流水线的处理器来说这容易解决,但如果处理器的流水线更长,在一个时钟周期内同时执行数条指令其困难就比较大。此外,几乎所有的系统结构最终都会产生地址空间不足的问题。
20世纪60年代,堆栈系统结构流行。它们被认为能够最好地配合高级语言——就当时的编译技术而言,这或许是对的。
20世纪70年代,设计的主要焦点是如何降低软件的费用,具体主要是用硬件来取代软件,或是提供能够简化软件设计者工作的高级结构。结果是:既产生了高级语言计算机系统结构,又出现了诸如VAX一类的强大系统结构。VAX具有大量的寻址方式、多种数据类型以及一个高度正交化的系统结构。
20世纪80年代,更加成熟的编译技术以及重新强调机器效率的观点使得更加简单的系统又成为了主流,它主要基于load-store系统结构的机器。
20世纪90年代指令集系统结构发生了一些变化:
- 地址空间翻倍:32位地址指令系统扩展到64位,寄存器的位数扩充到64位。附录C给出了3种从32位扩展到64位的系统结构的例子。
- 通过条件执行实现条件分支转移的优化:条件分支严重地限制了性能的提升。因此用条件操作完成来代替条件分支有很大的好处,例如条件传送(参见附录G)已经被大多数指令系统采用。
- 通过预取优化Cache性能:Cache的一次不命中所花掉的时间与早期机器中一次缺页错误所花掉的时间一样多,因此,增加了预取指令来减少Cache不命中的代价。
- 对多媒体的支持:大多数桌面和嵌入式指令系统都增加了对多媒体和DSP应用程序的支持。
- 更快的浮点操作:附录I给出了一些提高浮点性能的新操作。如执行一个乘法和一个加法的操作,执行配对单精度的操作。