依据指令长度的不同,指令系统可分为复杂指令系统(Complex Instruction Set Computer,简称CISC)、精简指令系统(Reduced Instruction Set Computer,简称RISC)和超长指令字(Very Long Instruction Word,简称VLIW)指令集三种。
CISC中的指令长度可变;RISC中的指令长度比较固定;VLIW本质上来讲是多条同时执行的指令的组合,其“同时执行”的特征由编译器指定,无须硬件进行判断。
早期的CPU都采用CISC结构,如IBM的System360、Intel的8080和8086系列、Motorola的68000系列等。这种设计简化了软件和编译器的设计,但也显著提高了硬件的复杂性。当硬件复杂度逐渐提高时,CISC结构出现了一系列问题。大量复杂指令在实际中很少用到,典型程序所使用的80%的指令只占指令集总指令数的20%,消耗大量精力的复杂设计只有很少的回报。同时,复杂的微代码翻译也会增加流水线设计难度,并降低频繁使用的简单指令的执行效率。
RISC指令系统的最本质特征是通过load/store结构简化了指令间关系,即所有运算指令都是对寄存器运算,所有访存都通过专用的访存指令(load/store)进行。这样,CPU只要通过寄存器号的比较就能判断运算指令之间以及运算指令和访存指令之间有没有数据相关性,而较复杂的访存指令相关判断(需要对访存的物理地址进行比较)则只在执行load/store指令的访存部件上进行,从而大大简化了指令间相关性判断的复杂度,有利于CPU采用指令流水线、多发射、乱序执行等提高性能。
VLIW结构的最初思想是最大限度利用指令级并行(Instruction Level Parallelism,简称ILP),VLIW的一个超长指令字由多个互相不存在相关性(控制相关、数据相关等)的指令组成,可并行进行处理。VLIW可显著简化硬件实现,但增加了编译器的设计难度。
处理器可访问的地址空间包括寄存器空间和系统内存空间。寄存器空间包括通用寄存器、专用寄存器和控制寄存器。寄存器空间通过编码于指令中的寄存器号寻址,系统内存空间通过访存指令中的访存地址寻址。
根据指令使用数据的方式,指令系统可分为堆栈型、累加器型和寄存器型。寄存器型又可以进一步分为寄存器-寄存器型(Register-Register)和寄存器-存储器型(Register-Memory)。下面分别介绍各类型的特点:
下图给出了四种类型的指令系统中执行C=A+B
的指令序列,其中A、B、C为不同的内存地址,R1、R2等为通用寄存器。
寄存器-寄存器型指令系统中的运算指令的操作数只能来自寄存器,不能来自存储器,所有的访存都必须显式通过load和store指令来完成,所以寄存器-寄存器型又被称为load-store型。
计算机中常见的数据类型包括整数、实数、字符,数据长度包括1字节、2字节、4字节和8字节。下图是不同指令集整数类型的名称和数据长度:
在执行访存指令时,必须考虑的问题是访存地址是否对齐和指令系统是否支持不对齐访问。所谓对齐访问是指对该数据的访问起始地址是其数据长度的整数倍,例如访问一个4字节数,其访存地址的低两位都应为0。对齐访问的硬件实现较为简单,若支持不对齐访问,硬件需要完成数据的拆分和拼合。但若只支持对齐访问,又会使指令系统丧失一些灵活性,例如串操作经常需要进行不对齐访问,只支持对齐访问会让串操作的软件实现变得较为复杂。以X86为代表的CISC指令集通常支持不对齐访问,RISC类指令集在早期只支持对齐访问,现在也开始支持不对齐访问。
不同的机器可能使用大尾端或小尾端,最高有效字节的地址较小的是大尾端,最低有效字节的地址较小的是小尾端。Motorola的68000系列和IBM的System系列指令系统采用大尾端,X86、VAX和LoongArch等指令系统采用小尾端,ARM、SPARC和MIPS等指令系统同时支持大小尾端。
寻址方式指如何在指令中表示要访问的内存地址。下表列出了计算机中常用的寻址方式,其中数组mem
表示存储器,数组regs
表示寄存器,mem[regs[Rn]]
表示由寄存器Rn的值作为存储器地址所访问的存储器值:
现代指令系统中,指令的功能由指令的操作码决定,可分为四大类:
转移指令包括条件转移、无条件转移、过程调用和过程返回等类型。转移条件和转移目标地址是转移指令的两个要素,两者的组合构成了不同的转移指令:条件转移要判断条件再决定是否转移,无条件转移则无须判断条件;相对转移是程序计数器(PC)加上一个偏移量作为转移目标地址,绝对转移则直接给出转移目标地址;直接转移的转移目标地址可直接由指令得到,间接转移的转移目标地址则需要由寄存器的内容得到。程序中的switch语句、函数指针、虚函数调用和过程返回都属于间接转移。由于取指译码时不知道目标地址,因此硬件结构设计时处理间接跳转比较麻烦。
转移指令有几个特点:第一,条件转移在转移指令中最常用;第二,条件转移通常只在转移指令附近进行跳转,偏移量一般不超过16位;第三,转移条件判定比较简单,通常只是两个数的比较。条件转移指令的条件判断通常有两种实现方式:采用专用标志位和直接比较寄存器。采用专用标志位方式的,通过比较指令或其他运算指令将条件判断结果写入专用标志寄存器中,条件转移指令仅根据专用标志寄存器中的判断结果决定是否跳转。采用直接比较寄存器方式的,条件转移指令直接对来自寄存器的数值进行比较,并根据比较结果决定是否进行跳转。X86和ARM等指令集采用专用标志位方式,RISC-V指令集则采用直接比较寄存器方式,MIPS和LoongArch指令集中的整数条件转移指令采用直接比较寄存器方式,而浮点条件转移指令则采用专用标志位方式。
下图是五种RISC指令集的指令编码格式。在寄存器类指令中,操作码都由操作码(OP)和辅助操作码(OPX)组成,操作数都包括两个源操作数(RS)和一个目标操作数(RD);立即数类指令都由操作码、源操作数、目标操作数和立即数(Const)组成,立即数的位数各有不同;
MIPS、SPARC和LoongArch只支持四种常用的寻址方式,PowerPC和PA-RISC支持的寻址方式较多。
load-store指令。load指令将内存中的数据取入通用寄存器,store指令将通用寄存器中的数据存至内存中。当从内存中取回的数据位宽小于通用寄存器位宽时,后缀没有U的指令进行有符号扩展,即用取回数据的最高位(符号位)填充目标寄存器的高位,否则进行无符号扩展,即用数0填充目标寄存器的高位。
ALU指令。ALU指令都是寄存器型的,常见的ALU指令包括加、减、乘、除、与、或、异或、移位和比较等。
控制流指令。控制流指令分为绝对转移指令和相对转移指令。相对转移的目标地址是当前的PC值加上指令中的偏移量立即数;绝对转移的目标地址由寄存器或指令中的立即数给出。在条件转移指令中,转移条件的确定有两种方式:判断条件码和比较寄存器的值。SPARC采用条件码的方式,整数运算指令置条件码,条件转移指令使用条件码进行判断。MIPS和LoongArch的定点转移指令使用寄存器比较的方式进行条件判断,而浮点转移指令使用条件码。
算数运算类指令:加减乘除、取余、位运算(与、或、异或)、比较、及特殊运算(alsl、pcaddi等)。
移位运算累指令:逻辑左移、逻辑右移、算数右移、循环右移。
位操作指令:截取某个范围的数据,如ext.b rd rj
将rj的[7:0]符号扩展后写入rd。计量连续比特1或0的个数,如clo.w rd rj
对rj的[31:0]数据,从第31位方向开始向第0位统计连续比特1的个数,clz.w是连续比特0。
转移指令
普通访存指令:{ld/sd}.{b/h/w/d}[u] rd, rj, si12
等
边界检查访存指令:{ld/sd}{gt/le}.{b/h/w/d} rd rj rk
。从内存中取回数据放入寄存器。访存地址直接从rj中取,且要自然对齐,否则触发非对齐例外。还要比较寄存器rj和rk的值的大小,如果指令预设条件,则进行访存,不满足条件则会终止访存并触发边界检查例外。
原子访存指令:am{swap/add/and/or/xor/max/min}[_db].{w/d} rd rj rk
等
内存屏障指令:dbar
、ibar
在大部分场景下,我们不用特意关注内存屏障的,特别是在单处理器系统里,虽然CPU内部支持乱序执行以及预测式的执行,但是总体来说,CPU会保证最终执行结果符合程序员的要求。在多核并发编程的场景下,程序员需要考虑是不是应该用内存屏障指令。下面是一些需要考虑使用内存屏障指令的典型场景。
总之,我们使用内存屏障指令的目的是想让CPU按照程序代码逻辑来执行,而不是被CPU乱序执行和预测执行打乱了代码的执行次序。LoongArch有两个内存屏障指令:
dbar 0
是一个完全功能的同步屏障。只有等到之前所有load/store
访存操作彻底执行完毕后,dbar 0
指令才能开始执行;且只有dbar 0
执行完毕后,其后所有load/store
访存操作才能开始执行。ibar 0
使用完成单个处理器核内部store
操作与取值操作之间的同步。ibar 0
能保证其之后的取值一定能观察到其之前所有store
操作的执行结果。浮点运算类指令:浮点加减乘除运算。
浮点比较指令:大于、等于、小于以及无法比较,当有两个操作数中至少有一个NaN时,这两个数就无法比较。比较结果会写入指定的条件标志寄存器。
浮点转换指令:单精度与双精度之间转换、定点与浮点之间的转换(舍入模式)。
浮点搬运指令:浮点寄存器之间搬运(直接搬运:fmov.s fd, fj
;条件搬运:fsel fd, fj, fk, ca
,若ca为0,fd = fj,否则fd = fk)、浮点与通用寄存器之间搬运、浮点与条件标志寄存器 之间搬运、通用与条件标志寄存器 之间搬运、通用与浮点控制状态寄存器 之间搬运。
浮点分支指令:只有条件分支,根据条件标志寄存器 的值决定是否跳转目标地址。
浮点普通访存指令:从内存中取回数据放入浮点寄存器,或将浮点寄存器中的值放入内存。
浮点边界检查访存指令:检查有效地址是否越界,从内存中取回数据放入浮点寄存器,或将浮点寄存器中的值放入内存。
所谓特权指令是指有特权权限的指令,由于这类指令的权限最大,如果使用不当,将导致整个系统崩溃。常见的特权指令有:
为了保证系统安全,这类指令只能用于操作系统或其他系统软件,不直接提供给用户使用。为了防止用户程序中使用特权指令,用户态下只能使用非特权指令,核心态下可以使用全部指令。当在用户态下使用特权指令时,将产生中断以阻止用户使用特权指令。所以把用户程序放在用户态下运行,而操作系统中必须使用特权指令的那部分程序在核心态下运行,保证了计算机系统的安全可靠。从用户态转换为核心态的唯一途径是中断或异常。
【参考书籍】
《LoongArch 参考手册》
《计算机体系结构基础》第3版
《计算机体系结构量化研究方法》第5版