第四章 处理器体系结构
一个处理器支持的指令和指令的字节级编码称为它的ISA(instruction-set architecture,指令集体系结构)。不同家族处理器有不同的ISA。ISA在编译器编写者和处理器设计人员之间提供了一个概念抽象层,编译器编写者只需要知道允许哪些指令,以及他们是如何编码的;而处理器设计者,必须建造出执行这些指令的处理器。
ISA模型看上去是顺序执行的,实际上同时处理多条指令的不同部分,可以提升性能。
本章自定义设计一个Y86处理器,它的指令集称为Y86指令集。
4.1 Y86指令集体系结构
我们设计的Y86处理器,类似于IA32,包含下面四部分:
八个程序寄存器:每个寄存器存储一个字,寄存器%esp被入栈,出栈调用和返回指令作为栈指针,其他寄存器没有固定含义或固定值。
三个一位的条件码:它们保存最近的运算的相关信息。
程序计数器(PC):存放着当前正在执行指令的地址。
存储器:保存程序和数据,可以看成很大的数组。Y86用虚拟地址来引用存储器位置。硬件和软件操作系统协作将虚拟地址转换成物理地址。
下图是我们设计的Y86指令集描述,这个就是我们要设计的处理器要实现的目标。左边是汇编表示,右边是字节编码。我们使用小端法。
指令的字节编码最多6个字节(0xff是8位,一个字节)
第0字节:称为指令指示符。fn字段表示是某个整数操作(OP1)或分支条件(jXX)。
我们把IA32的movl拆分成4个指令,显示地指明源和目的的类型,i立即数,r寄存器,m存储器。
opl:是4个整数操作指令addl,subl,andl,xorl。它们只对寄存器数据进行操作,这些指令会设置条件码。
jXX:跳转指令jmp,jle,jl,je,jne,jg,jge。根据分支指令类型和条件码选择分支。dest是绝对地址。
call指令将返回地址入栈,然后调到目的地址。dest是绝对地址。
ret指令从call指令的调用中返回。
pushl和popl实现入栈和出栈。
halt:停止执行指令。
fn指令字节码
如下图所示,8个寄存器都有相应的寄存器标识符。程序寄存器被存放在cpu的一个寄存器文件中,这个寄存器文件就是一个小的,以寄存器ID作为地址的随机访问存储器。图4.2中的rA,rB就对应这8个寄存器。当操作数不是寄存器时,就用ID为8的表示,图4.2中的8就是这个意思。
通过上面这些规则,我们可以把汇编语言转换成十六进制字节码,反之亦然。
例如
rmmovl %esp 0x12345(%edx)的转换过程如下:
rmmovl %esp 0x12345(%edx) -->404245230100
rmmovl = 40
%esp = rA = ID 4
%edx = rB = ID 2
0X12345 = D = 0x12345补齐8位=0x00012345 = 小端452301100
指令集的一个重要特性就是字节编码必须有唯一解释 。任何一个字节序列只能对应唯一的指令。
4.2 逻辑设计和硬件控制语言HCL
4.2.1 逻辑门
下图是3个逻辑门的标准符号,下面是对应的HCL表达式。逻辑门只对位进行操作。
4.2.2 组合电路和HCL布尔表达式
将多个逻辑门组合起来,可以得到组合电路,有两个基本原则:
1. 逻辑门的输出不可以连到一起,因为会相互影响。
2. 这个网必须是无环的,因为该组合电路的计算函数有歧义。
两个简单的组合电路
4.2.3 字级的组合电路和HCL整数表达式
通常需要字级操作,这就需要更复杂的组合电路,下面列了几个组合电路的符号图和HCL表达式
1. 字等于电路
2. 字等于的HCL表达式
bool Eq = (A == B);
字级多路复用HCL表达式
int Out = [
S: A;
1: B;
];
3. 多路选择的多路复用
HCL表达式:
int Out4 = [
!S1 && !S0: A;
!S1: B;
!S0: C;
1: D;
];
4. 取最小值
HCL表达式:
int Min3 = [
A < B && A < C: A;
B < A && B < C: B;
1: C;
];
算术逻辑运算单元ALU是非常重要的组合电路,其抽象如下图
A和B是数据输入,上方的0、1、2、3是控制输入,对应Y86指令集的四个整数操作addl,subl,andl,xorl。
4.2.4 集合关系(Set Membership)
处理器设计中,经常需要判断一个信号是否属于一组信号中的一个,使用表达式
iexpr in {iexpr1, iexpr2,...iexpr10}
4.2.5 存储器和时钟控制
为了产生时序电路(sequential circuit),也就是有状态,并且在这个状态上的系统,需要引入安位存储信息的设备,考虑如下两类:
时钟寄存器:简称寄存器,存储单个位或字。时钟信号控制寄存器加载输入值。
随机访问存储器:简称存储器,存储多个字,用地址来选择读写哪个字。常见的有处理器的虚拟存储器系统和寄存器文件。
硬件通过时钟控制化寄存器的值,如高电平保持,低电平变化。
下图是一个寄存器文件,虽然不是硬件组合电路,但原理是一样的,当向src*写入一个值后,过一段时间,就可以在val*上得到对应的值。例如向srcA设置为3,就会读%ebx的值,然后这个值就会出现在valA上。
4.3 Y86的顺序(sequential)实现
SEQ:顺序处理器
4.3.1 将处理组织成阶段
取址(fetch):从存储器读入指令,地址为PC的值。从指令中抽取出指令指示符字节两个4位部分,称为icode指令代码和ifun指令功能。取的值有多种可能:
1. 一个寄存器指示符字节,指明一个或两个寄存器操作数指示符rA和rB。
2. 四字节常数valC。
人理解L取出的内容是4.1中的字节码,可以转换成汇编语言后理解。
它按顺序的方式计算下一条指令的地址valP,valP等于PC的值加上已取出的指令的长度。
解码(decode):从寄存器文件读入最多两个操作数,得到valA或/和valB。通常它读入指令rA和rB字段指明的寄存器,也有些指令是读寄存器%esp的。
执行(execute):ALU进程工作,有如下几种:
1. 执行指令指明的操作(根据ifun的值),计算存储器引用的有效地址;
2. 增加或减少栈指针;
3. 设置条件码,对于跳转指令来说,这一阶段会检查条件码和(ifun)给出的分支条件,看是否选择分支。
访存(memory):将数据写入存储器或从存储器读出数据,读出的值为valM。
写回(write back):最多可以写两个结果寄存器文件。
更新PC:将PC设置成下一条指令地址。
处理器循环执行上述几个阶段,只有在遇到halt或发生错误时才会停止。
下图展示了三个机器指令在每个阶段的动作。
以这个例子具体来看
第3行指令subl
第5条指令rmmovl
第6行pushl
第8行je
第13行ret
4.3.2 SEQ硬件结构
通过下面的硬件抽象图,学习图中的硬件单元和各个阶段关联
取址:指令存储器将PC作为地址,读取一个指令大小的字节,PC增加器计算valP。
解码:从寄存器文件的A、B两端读取valA和valB。
执行:ALU根据指令类型进行相应计算valE,可能会设置条件码。
访存:数据存储器读出或写入一个存储器字。指令和数据存储器访问的是相同的存储器位置,但是用于不同的目的。
写回:寄存器文件有两个写端口,M端口用于写从存储器中读出的值,E端口用来写ALU计算出的值。
下图是一个更详细的SEQ所需硬件,简单说明下图形表示:
用带淡点的浅灰色方框:表示硬件单元。比如存储器,ALU等。
灰色圆角矩形:表示控制逻辑块。用于从一组信号源中进行选择,或用来计算布尔函数。
白色圆角方框:线路的名字。
中等粗的线:表示宽度为字长的数据连接。实际上代表一簇32根线。
比字长线细一点的线:表示宽度为字节或者更小的数据连接。根据数据类型,实际上代表一簇4或8根线。
点虚线:表示单个位的连接。代表芯片上单元与块之间传递的控制值。
4.3.3 SEQ的时序(timing)
提前总结,就是新周期开始的上升沿,会把逻辑组合的值更新到寄存器,计数器等状态设备。
以下图为例介绍,硬件如何执行第3、4条指令的。
在时钟周期3开始的时候,状态元素使第2条命令结束的状态,地址0x00c载入程序计数器中,这样就会取出和处理第3条指令。值沿着组合逻辑流动,包括读随机访问存储器,在3周期末尾,组合逻辑为条件码产生了新值000,但没更新到条件码寄存器;更新了程序寄存器%ebx,以及程序计数器的新值0x00c,此时逻辑组合已经更新了,但是状态没有更新。
在时钟周期4开始的时候,有上升沿,会更新条件码寄存器,程序计数器,寄存器文件。但组合逻辑还没对这些变化做出反应。在这个周期内会取出和执行第4条指令。
4.3.4 SEQ的阶段实现
常用的HCL如下表
取址阶段:
如下图所示,取指阶段包括指令存储器硬件单元。以PC作为第一个字节(字节0)的地址,这个单元一次从存储器读取6个字节。
第一个字节被当成指令字节,被分成icode和ifun。根据icode的值可以计算3个1位的信号(用虚线表示):
instr_valid: 用于表示这个指令是否是一个合法的Y86指令;
need_regids: 用于表示这个指令是否包含寄存器指示字节;
need_valC: 用于表示这个指令是否包括一个常数;
比如need_regids的HCL语言描述如下:
need_regids = icode in {IRRMOVL, IIRMOVL, IRMMOVL, IMRMOVL, IOPL, IPUSH, IPOPL};
剩余的5个字节是:寄存器指示符字节和常数数字的组合编码。标号称为Align的硬件单元会处理这些字节,将他们放入寄存器字段和常数字中。
当need_regids=1时,字节1被分开装入rA和rB中,否则这两个字段会被设为8。同时根据need_regids来确定1~4还是1~5字节是常数。
PC增加器硬件单元,计算valP, valP=p(PC)+r(need_regids)+4*1(need_valC)
解码和写回阶段:
下图实现解码和会写阶段的逻辑
srcA和srcB是读端口valA和valB的地址输入
dstE和dstM是写端口valE和valM的地址输入
根据指令代码icode和寄存器指示值rA和rB会产生4个不同的寄存器文件的寄存器ID.
寄存器ID srcA表明应该读哪个寄存器以产生valA,srcA的HCL语言描述如下:
int srcA = [
icode in {IRRMOVL, IRMMOVL, IOPL, IPUSHL} : rA;
icode in {IPOPL, IRET} : RESP;
1 : RNONE; #Don't need register
];
srcB的HCL描述如下:
int srcB = [
icode in {IOPL, IRMMOVL, RMRMOVL} : rB;
icode in {IPUSHL, IPOPL, ICALL, IRET} : RESP;
1 : RNONE; #Don't need register
];
寄存器ID dstE表明写端口E的目的寄存器,计算出来的值valE将放在那里。其HCL描述如下:
int dstE = [
icode in {IOPL, IRRMOVL, IIRMOVL} : rB;
icode in {IPUSHL, IPOPL, ICALL, IRET} : RESP;
1 : RNONE; #Don't need register
];
dstM的
int dstM = [
icode in {IOPL, IMRMOVL} : rA;
1 : RNONE; #Don't need register
];
执行阶段:
如下图所示,根据ALU fun信号,对ALU A和ALU B执行运算
ALU A的HCL 表达
int alua = [
icode in {IRRMOVL, IOPL} : valA;
icode in {IIRMOVL, IRMMOVL, IMRMOVL} : valC;
icode in {ICALL, IPUSHL} : -4;
icode in {IRET, IPOPL} : 4;
# other instruction don't need ALU
];
对应OPL指令,我们希望ALU使用ifun指令字段中编码的操作,所以ALU控制的HCL描述可以写成:
int alufun = [
icode == IOPL : ifun;
1 : ALUADD;
];
执行阶段包括条件码寄存器,每次运行时ALU都会产生条件码相关的信号,不过我只希望执行OPL时设置条件码寄存器,所以创建了一个信号set_cc进行控制.
bool set_cc = icode in {IOPL};
名字为bcond的硬件单元,决定一条指令是否跳转,并产生信号Bch。
访存阶段:
存储器的读写地址总是valE和valA,用HCL描述
int mem_addr = [
icode in {IRMMOVL, IPUSHL, ICALL, IMRMOVL} : valE;
icode in {IPOPL, IRET} : valA;
];
更新PC阶段:
HCL描述如下:
int new_pc = []
icode == ICALL : valC;
icode == IJXX && Bch : valC;
icode == IRET : valM;
1 : valP;
;
4.3.5 SEQ+: 重新安排计算阶段
我们把将计算PC放在时钟开始阶段,称这样的处理器为SEQ+。
4.4 流水线的通用原理
介绍流水线原理
4.4.1 计算流水线
计算流水线的效率
4.4.2 流水线操作的详细说明
4.4.3 流水线的局限性
1. 不一致的划分
2. 流水线过深,反而影响效率
4.4.4 带反馈的流水线
带反馈的流水线容易产生错误
4.5 Y86的流水线实现
设计一个流水线话的Y86处理器。以SEQ+为基础,在各个阶段添加流水线寄存器。
4.5.1 插入流水线寄存器
先设计一个初级版本PIPE-的处理器,抽象结构如下图所示
灰色方框表示流水线寄存器:
F:保存程序计数器的预测值;
D:位于取址和解码之间,它保存关于最新取出的指令的信息,即将由解码阶段进行处理;
E:位于解码和执行之间,它保存关于最新解码的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理。
M:位于执行和访存阶段之间,它保存最新执行的指令的结果,即将由访存阶段进行处理。它还保存关于用于处理条件转移的分支条件和分支目标的信息。
W:位于访存阶段和反馈路径之间,反馈路径将计算出来的值提供给寄存器文件写,而当完成ret指令时,它还要向PC选择逻辑提供返回地址。
下图用于说明代码是如何在流水线执行的,时间是从左往右增大,上面的数字表明各个阶段发生的时钟周期。
下图是一个更为详细的硬件结构,每个流水线寄存器包含多个字段(白色方块),对应于不同的指令通过流水线的相关信号。
先介绍SEQ+和PIPE-结构的差别
4.5.2 对信号进行重新排列和标号
1. 解码阶段产生的dstE和dstM会一直贯穿执行和访存阶段,直到写回阶段才送到寄存器文件, 这样可以确保寄存器文件的写端口的地址和数据都来自解码阶段的同一条指令。否则会将写回阶段的指令的值写入,而寄存器ID却来自于解码阶段的指令。作为一条通用原则,我们要保存处于一个流水线阶段中的指令的所有信息。
2. seletc A块,从寄存器文件的A端口或者流水线寄存器D的valP中选一个作为流水线寄存器E的valA。PIPE-增加这个块的目的是为了减少要携带给流水线寄存器E和M的状态数量。因为,在所有指令中,只有call在访存阶段需要valP的值,只有跳转指令在执行阶段需要valP的值,而这些指令又都不需要从寄存器文件中读取的值,因此我们合并这两个信号,将他们作为valA携带穿过流水线,从而减少流水线寄存器的状态数量。在硬件设计中,像这样仔细确认信号,然后通过合并信号来减少寄存器状态和线路的数量,是很常见的。
4.5.3 预测下一个PC
为了实现每个时钟执行一条命令,需要在取出当前指令后,马上确定下一条指令的地址。但是有些情况下(如跳转指令和ret)无法确定,这就需要进行预测。
图4.41中取指阶段的Predict PC块会从PC增加器计算出的valP和取出的指令中得到的valC中选一个存放到流水线寄存器F的predPC中,作为程序计数器的预测值。Select PC块从三个值中选择一个作为指令存储器的地址。
4.5.4 流水线冒险(hazard)
将流水线引入带反馈的系统会导致相邻指令间在发生相关时出现问题,这些相关有如下2形式:
1. 数据相关,下一条指令会用到这指令计算出的结果。
2. 控制相关,一条指令要确定下一条指令的位置。例如在执行跳转、调用或返回指令时。
这些相关可能会导致流水线产生计算错误,称为冒险。同相关一样,冒险也分为2类:数据冒险和控制冒险。
我们通过下图进一步了解流水线冒险
在周期6,此时addl指令从寄存器文件读取它的操作数。此时,第一条irmovl指令已经通过了写回阶段,因此%edx的值已经更新。第2条irmovl指令处于写回阶段,因此对%eax的写要到第7周期开始,时钟上升时才会发生。结果addl指令在解码阶段时会读出错误的%eax值,因为对该寄存器的写还没发生。 如果有3条nop指令就不会发生错误,如果nop减少,则有更多的错误。
之所以会出现这些冒险,是因为流水线化处理器,是从解码阶段从寄存器文件中读取指令的操作数,而要到3周期后,指令经过写回阶段时,才会将指令的结果写到寄存器文件。
4.5.5 用暂停(stalling)来避免数据冒险
暂停时,处理器会停止流水线中的一条或多条指令,直到冒险条件不再满足。只要一条指令的源操作数会被流水线后面某个阶段中的指令产生,处理器就会通过将指令阻塞在解码阶段来规避数据冒险。
暂停技术就是就是让一组指令阻塞在他们的阶段,而允许其他指令继续通过流水线。