鲸鱼的抽屉  

处理器体系结构

目录


处理器体系结构

我们看到的计算机系统都只限于机器语言程序级。处理器执行一系列指令每天指令执行某个简单操作,它们被编码为由一个或多个字节序列组成的二进制格式。一个处理器支持的指令和指令的字节集编码成为它的指令集体系结构(ISA)。

在本章的学习中,我们的学习目标如下:

  1. 了解ISA抽象的作用
  2. 掌握ISA,并能举一反三学习其他体系结构
  3. 了解流水线和实现方式

各章节需要掌握的重点内容如下:

  1. 了解Y86-64指令集体系结构,掌握汇编指令与机器码之间的转换,学会将C代码、X86汇编代码翻译为Y86-64代码;
  2. 了解逻辑设计和硬件控制语言HCL,掌握组合电路和HCL布尔表达式;
  3. 了解SEQ顺序处理器执行的6个阶段,掌握Y86-64所有指令个阶段的处理情况;
  4. 了解SEQ硬件结构,掌握分析硬件结构图的方法,以及某个寄存器的HCL描述;
  5. 了解流水线的通用原理,处理冲突和冒险的方法。

返回目录

一、Y86-64指令集体系结构

程序员可见状态

这里的程序员既可以是用汇编代码写程序的人,也可以是产生机器级代码的编译器(详见第一章图1.3编译系统)。

编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)

程序员可见状态为15程序寄存器(RF)、3个条件码(CC)、程序状态(Stat)、程序计数器(PC)和内存(DMEM),如下图所示:

  • 程序寄存器:每个程序寄存器存储一个64位(即8字节)的字,这与Y86-64指令集的指令长度相匹配,它只包括8字节整数操作,称之为“字”不会产生歧义。
  • 条件码:包括ZF、SF、OF3个一位的条件码,保存着最近的算数或逻辑指令所造成影响的有关信息。
  • 状态码:表明程序执行的总体状态,一共包含 AOK、HLT、ADR、INS四种状态码,代码的值和含义如下表所示:

  • 程序计数器:存放当前正在执行指令的地址
  • 内存:是个很大的字节数组,存放程序和数据。

Y86-64指令

想要了解Y86-64指令集及其对应的编码,只要看懂下面这三个图就够了:

第一张图是Y86-64指令编码示意图,其中黄色框内的三个指令并非真实的Y86-64指令,OPq代表4个整数操作指令、jXX代表7个跳转指令、comvXX代表6个条件传送指令,其它指令如图所示:

如图所示,Y86-64指令集编码长度从1字节到10字节不等,这里我们将每字节(8位)用两个十六进制数(4位)表示,指令的详细解释如下:

1.halt指令用于停止指令的执行,执行halt会导致处理器停止,并将状态码设置为HLT。该指令占1个字节,用十六进制表示为「00」;

2.nop指令为占位指令,其作用参考NOP指令作用,这里我们不展开描述。该指令占1个字节,用十六进制表示为「10」;

3.rrmovq rA,rB指令作用是将rA寄存器中的值放入rB寄存器中。该指令占2个字节,用十六进制表示为「20 rArB」,其中“rA”、“rB”为两个代表寄存器的4比特数,要根据实际指令使用哪个寄存器来决定(其他涉及寄存器的传送指令同理),寄存器与数字编码的对应关系如下图所示:

例如:指令“rrmovq %rbx,%rcx”对应的编码为:「20 31」

4.irmovq V,rB指令作用是将立即数V放入寄存器rB中。该指令占10个字节,用十六进制表示为「30 FrB V」,其中“V”为八字节小段方式存储的立即数,像irmovq、pushq、popq这些只用到一个寄存器的指令,会将另一个寄存器指示符设为0xF,表示没有寄存器;

『例如』:指令“irmovq $15,%rbx”对应的编码为:「30F30F00000000000000」。

『数字转换计算过程』:由于“15”的十六进制表示为“F”,所以要在其前面添加15个“0”凑成八字节的值“000000000000000F”,再将其写成小端方式为:“0F000000000000”

5.rmmovq rA,D(rB)指令的作用是将rA寄存器中的值放入以“rB+D”为地址的内存单元中。该指令占10个字节,用十六进制表示为「40 rArB D」,其中“D”为八字节小段方式存储的代表偏移量的数;

『例如』:指令“rmmovq %rcx,-3(%rbx)”对应的编码为:「4013FDFFFFFFFFFFFFFF」。

『数字转换计算过程』:由于“-3”十六进制表示为“83”,扩展为八字节为“8000000000000003”,计算机中负数是用补码表示的,所以将其装换为补码为“FFFFFFFFFFFFFFD”,再将其写为小端方式为:“FDFFFFFFFFFFFFFFFF”

6.mrmovq D(rB),rA指令的作用是将以“rB+D”为地址的内存单元中的数放入寄存器rA中。与rmmovq类似,该指令占10个字节,用十六进制表示为「50 rArB D」,其中“D”为八字节小段方式存储的代表偏移量的数;

7.OPq rA,rB为整数操作指令,其作用是将寄存器rA和寄存器rB中的值做整数运算,并把结果存入rB寄存器中。该指令占4个字节,用十六进制表示为「6fn rArB」,其中fn代表指令的“功能码”,由具体执行“addq、subq、andq、xorq”中的哪条指令决定fn的值,其对应关系如下图“Y86-64指令集的功能码”所示:

所有指令的前4位编码成为指令的“代码部分”,如OPq指令中的“6”,功能值只有在一组相关指令共用一个代码时才有用(除了OPq,还有前面所提的jXX和comvXX都属于共用相同代码的指令)

8.jXX Dest为跳转指令,其作用是跳转到以Dest为地址(准确的说是逻辑地址)的代码处,根据分支指令的类型(处理器根据fn确定)和条件码的设置来选择分支。该指令占9个字节,用十六进制表示为「7fn Dest」,与OPq类似,其中fn为功能码,由具体执行“jmp、jle、jl、je、jne、jge、jg”中的哪条指令决定fn的值,其对应关系如“Y86-64指令集的功能码”所示,Dest为用小端方式表示的8字节绝对寻址方式的地址值(也可以是某个语句标号);

『例如』:下述代码中指令“jmp loop”对应的编码为:「700C01000000000000」

.pos 0x100 #Start code at address 0x100
    irmovq ……
    rrmovq ……
loop:
    rmovq ……
    addq ……
    jmp loop

『数字转换过程』:“.pos 0x100”的意思是目标代码起始地址为0x10C,即“irmovq ……”那行对应的地址为0x100,因为irmovq指令和rrmovq指令编码长度分别为10和2,所以“rrmovq ……”那行指令的地址为0x100+0xa=0x10a,“loop:”那行的地址为“0x10a+2=0x10c”,因此此处Dest的值为0x10c,用小端方式表示为“0C01000000000000”

9.cmovXX rA,rB为条件传送指令,其指令格式同rrmovq指令,不同点是只有当条件码满足需要的约束时才会更新目的寄存器的值。该指令占2个字节,用十六进制表示为「2fn rArB」;fn由具体执行“rrmovq cmovle cmovl cmove cmovne cmovge cmovg”中的哪条指令决定,其对应关系如“Y86-64指令集的功能码”所示;

10.call Dest指令的作用是将返回地址入栈,然后跳到Dest指向的目的地址。该指令占9个字节,用十六进制表示为「80 Dest」,其中Dest的作用类似于条件跳转指令;

11.ret指令的作用是从call指令的调用中返回。该指令占1个字节,用十六进制表示为「90」;

12.pushq rA为入栈指令,其作用是将rA寄存器中的值压入栈顶。该指令占2个字节,用十六进制表示为「A0 rAF」;

13.popq rA位出栈指令,其作用是将栈顶元素弹出到rA寄存器中。该指令占2个字节,用十六进制表示为「B0 rAF」。

应用部分

为了检验我们是否掌握了Y86-64指令与指令编码之间的转换方法,我们可以尝试着做做教材练习题4.1和4.2。这里将字节序列转换为Y86-64指令的方法总结如下:

  1. 通过代码部分确定指令长度,从而以指令为单位划分字节序列;
  2. 通过功能部分确定具体的指令;
  3. 通过寄存器指示符字节确定指令中涉及的寄存器;
  4. 通过转换数值部分以小段法编码的数字来确定立即数、偏移量、绝对地址等值。

进一步的,为了学会写Y86-64程序,我们可以从“改编x86-64汇编代码”入手,但是这里有几点需要注意(详细转换细节请见我的课下实践博客):

  1. Y86-64中要把movq指令转换为具体的rrmovq,rmmovq,mrmovq指令;
  2. Y86-64中OPq只对寄存器数据进行操作,可以借用%r8-%r14这些寄存器,先用“irmovq”指令将立即数放入寄存器中,再进行相关计算;
  3. Y86-64中没有加载有效地址指令leaq,需要用“addq”等指令来代替其功能;
  4. Y86-64中没有比较指令“cmpq”,可以用两个寄存器存放操作数的值然后用subq命令使两数相减来设置条件码;
  5. Y86-64中没有乘指令mulq,需要用addq来替换;
  6. “movq %fs:40, %rax”指令,需要弄清楚%fs:40的含义和类型;
  7. 由于Y86-64指令集中所以操作都以8个字节为单位,所以在转换“movl,addl”这些四字节指令时要额外注意,有时需要保护高四字节的值;
  8. cltq指令:用于把%eax符号扩展到%rax,所以要先判断%eax的符号位,再决定给高四个字节置1还是0;
  9. Y86-64指令集中没有leave指令,要用“rrmovq %rbp,%rsp”+“popq %rbp”来代替。
  10. 我们常常用异或一个数本身来代替直接将其赋值为0,可以防止字符串提前以“0”结束引发的数据丢失。
  11. 为了更加深刻的领会Y86-64指令的含义和用法,可以试着做做教材练习题4.3、4.4、4.5和4.6

书中给出了一个叫做“YIS”的指令集模拟器的工具,它的目的是模拟Y86-64及其代码程序的执行,而不用试图去模拟任何具体处理器实现的行为,我们要学会使用它来调试程序、模拟在硬件运行商的结果。

参考老师的给出的实验楼资源,构建YIS步骤如下:

  1. cd ~/Code/shiyanlou_cs413(根据实际情况修改,选择自己的路径)
  2. wget http://labfile.oss.aliyuncs.com/courses/413/sim.tar
  3. tar -xvf sim.tar
  4. cd sim
  5. sudo apt-get install bison flex tk
  6. sudo ln -s /usr/lib/x86_64-linux-gnu/libtk8.6.so /usr/lib/libtk.so
  7. sudo ln -s /usr/lib/x86_64-linux-gnu/libtcl8.6.so /usr/lib/libtcl.so
  8. make

测试YIS步骤如下:

  1. cd y86-code
  2. 进入测试代码,教材p239页代码为asuml.ys,可以通过make asuml.yo进行汇编,asuml.yo就是汇编后的结果,见教材p238。
  3. make all可以汇编运行所有代码结果

返回目录


二、逻辑设计和硬件控制语言HCL

为实现一个数字系统需要三个主要的组成部分:计算对位进行操作的函数的组合逻辑、存储位的存储单元,以及控制存储器单元更新的时钟信号。

HCL与HDL的区分

  • HCL(硬件控制语言)用来描述不同处理器设计的控制逻辑。
  • HDL(硬件描述语言)是一种文本表示,类似于编程语言,只不过是用来描述硬件结构,HCL只是表达硬件设计的控制部分,只有有限的操作集合,没有模块化。

HCL表达式中AND用“&&”表示,OR用“||”表示,NOT用“!”表示,他们只对单个位的数进行操作,而不是整个字。与、或、非三种逻辑门可以用一种与非门或者或非门实现。逻辑门的类型如下图所示:

多个逻辑门组合成网就能构成计算快,称为组合电路,构建这些网时有以下几个限制:

  1. 每个逻辑门的输入必须连接到下述选项之一:1)一个系统输入(称为主输入),2)某个存储器单元的输出,3)某个逻辑门的输出。
  2. 两个或多个逻辑门的输出不能连接在一起。
  3. 这个网必须是无环的。

我们要学会通过组合电路写出HCL布尔表达式,例如,下图中的组合电路用HCL表达式表示为:「bool eq = (a&&b) || (!a&&!b)」

而下面这个组合电路由控制信号s决定选出那个输入信号用在多路复用器(MUX)中,其HCL表达式为:「bool eq = s&&a) || (!s&&!b)」。

字级组合电路和HCL整数表达式

HCL中多路复用函数是用情况表达式来描述的,其通用格式如下:

[
    select 1 : expr 1
    select 2 : expr 2
    .
    .
    .
    select k : expr k
]

select i表示第i种情况的布尔表达式(表明什么时候选择这种情况),expr i表示在这种情况下的表达式(表明得到的值)。

用情况表达式来描述下图字级多路复用选择电路为:

word Out = [
    s: A;
    1: B;//选择表达式为“1”,表示如果前面没有情况被选中,那就选择这种情况
];

此外,HCL中还支持字级上比较的表达式“bool Eq = (A==B)”以及“<=”、">="等比较符号。

我们可以尝试做做教材练习题4.11和4.12来加深对书写HCL整数表达式的理解。

算数/逻辑单元(ALU)是一种根据控制信号的输入决定对其他两个输入A和B进行什么样的操作,如下图所示:

集合关系

我们可以用下面这种形式简化的表示集合关系:

iexpr in {iexpr1,iexpr2,…,iexprk}//iexpr为被测试的值,iexpr1~iexprk位待匹配的值,他们均为整数表达式

例如想从一个两位信号code中选择高位和地位来为下图中的思路复用器产生信号s0和s1,可以用bool s1 = code in {2,3}; bool s0 = code in {1,3};来简化的表示s0和s1的产生。

存储器和时钟

为了产生时序电路,我们引入按位存储信息的设备,存储设备是由同一个时钟控制的。

  • 时钟寄存器:简称寄存器,存储单个位或字。Y86-64中使用时钟寄存器保存程序计数器PC、条件代码CC和程序状态Stat。
  • 随机访问存储器:简称内存,存储多个字。eg:处理器的虚拟内存系统;寄存器文件。处理器中用来存储程序数据的随机访问存储器示意图如下:

寄存器的概念:

  • 硬件寄存器:将其输入和输出的线连接到电路的其他部分。当时钟上升沿到来才会加载新的输入信号并输出,是电路不通部分中组合逻辑之间的屏障。(后续我们介绍的寄存器是指硬件寄存器)
  • 程序寄存器:CPU中为数不多的可寻址的字,这里的地址为寄存器ID,这些字通常都存在寄存器文件(不是组合电路)中。

一个典型的寄存器文件包括两个读端口、一个写端口和时钟,如下图所示:

其中读端口的srcA和srcB为寄存器ID(即寄存器标识符),一旦设定,在一定延迟后,存储在相应寄存器的值就会出现在valA和valB上;写端口的dstW代表地址输入,valW代表数据输入。

  • 寄存器操作示意图如下,可见每当时钟到达上升沿时,值才会从寄存器的输入传送到输出。

返回目录


三、Y86-64的顺序实现

通过前两节的学习我们了解了Y86-64指令集的编码、组合电路、HCL表达式以及存储器和时钟的概念。有了Y86-64处理器所需要的部件,我们要了解Y86-64的实现过程。

首先我们描述一个顺序的处理器SEQ),它是最终实现高效的、流水线化的处理器的基础。

将处理组织成阶段

在设计硬件时,一个非常简单而一致的结构是非常重要的,降低复杂度的一种方法是让不同的指令共享尽可能多的硬件。为了设计一个充分利用硬件的处理器,我们将每个指令包含的诸多操作划分为统一的“六阶段”框架:

  1. 取值:从内存读取指令字节,地址为程序计数器PC的值,同时给valC(从指令字节中读取的数值)和valP(根据当前指令长度计算下一条指令PC值)赋值。
  2. 译码:从寄存器文件 读入最多两个操作数,记为valA和valB。
  3. 执行:算数/逻辑单元(ALU)根据指令的功能部分计算内存引用的有效地址或修改栈指针。计算结果记为valE。
  4. 访存:将数据写入内存或从内存读出数据。读出的数据记为valM。
  5. 写回:最多两个结果到寄存器文件
  6. 更新:将PC设置成下一条指令的地址。

下面展开介绍在下图的代码中Y86-64指令在上述各个阶段是怎么处理的:

[注]:

  1. 符号icode:ifun表明指令字节的两个组成部分
  2. 符号rA:rB表明寄存器指示符的两个组成部分
  3. 符号M1[x]表示访问内存位置x处的一个字节,M8[x]表示访问八个字节

1.OPq指令的通用规则和在上图代码第3行中的具体实例:

2.rrmovq指令的通用规则:

3.irmovq指令的通用规则和在上图代码第4行中的具体实例:

4.rmmovq指令的通用规则和在上图代码第5行中的具体实例:

5.mrmovq指令的通用规则:

6.pushq指令的通用规则和在上图代码第6行中的具体实例:

7.popq指令的通用规则和在上图代码第7行中的具体实例:

8.jXX指令的通用规则和在上图代码第8行中的具体实例:

9.call指令的通用规则和在上图代码第9行中的具体实例:

10.ret指令的通用规则和在上图代码第13行中的具体实例:

11.nop指令只是简单地经过这六个阶段,除了将PC加1,不进行任何处理。

12.halt指令使得处理器状态被设置为HLT,导致处理器停止运行。

SEQ硬件结构

根据Y86-64指令所需要的计算被组织成的6个基本阶段,我们通过观察下面这张SEQ的抽象视图,来初步了解这些计算的硬件的抽象表示:

其中需要说明的是:

  1. 寄存器有两个写端口,端口E用来写ALU计算出来的值,而端口M用来写从数据内存中读出的值。
  2. 条件码寄存器(CC)有三个码位(依次为ZF、SF、OF),当执行跳转指令时,会根据条件码和跳转类型来计算分支信号Cnd,具体Cnd是怎么实现分支功能、分支后去执行了什么我们可以观察下面SEQ的硬件结构这张图,它更详细的给出了实现SEQ所需要的硬件:

其中需要说明的是:

  1. 左下角“instr_valid”和“imem_error”两个信号当指令地址越界时会产生,用于设置访存阶段的状态码。
  2. 灰色圆角方框表示控制逻辑,这些块用来从一组信号源中进行选择,或者用来计算一些布尔函数。其目标是在不同硬件单元之间传送数据,操作这些单元,使得对每个不同的指令执行指定的运算。
  3. 白色方框表示时钟寄存器。程序寄存器PC是SEQ中唯一的时钟寄存器。
  4. 浅蓝色方框表示硬件单元,包括内存、ALU等。
  5. 宽度为字长的数据连接用中等粗度的线表示,宽度为字节或更窄的数据连接用细线表示,单个位连接用虚线表示。

SEQ的时序

SEQ的实现包括:

  • 组合逻辑:不需要任何时序或控制——只要输入变化,值就通过逻辑门网络传播;
  • 两种存储器设备
    • 时钟寄存器(程序计数器和条件码寄存器):这两个硬件单元的时序进行控制。
    • 随机访问寄存器(寄存器文件、指令内存和数据内存):将读随机访问寄存器看成和组合逻辑一样的操作。
      • 其中指令内存只用来读,所以将该单元看成组合逻辑。
      • 数据内存和寄存器文件这两个硬件单元的时序进行控制。

综上,我们需要对程序计数器、条件码计数器、数据内存和寄存器文件这四个硬件单元的时序进行控制。每个时钟周期,程序计数器都会装载新的指令地址;只有执行整数运算指令时,才会装载条件码寄存器;只有在执行rmmovq、pushq或call指令时,才会写数据内存

我们要在组织计算时遵循以下原则:

  • 从不回读:处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态。

当一个时钟的上升沿到来时,状态单元(程序计数器、条件码寄存器、寄存器文件和数据内存)是根据前一条指令设置的。

SEQ阶段的实现

本节介绍了实现SEQ所需要的控制逻辑块的HCL描述。下图为控制逻辑中必须被显示引用的常数,这些值表示的是指令、功能码、寄存器ID、ALU操作和状态码的编码:

注:条件传送指令“comvXX”的指令代码为IRRMOVQ。

1.取值阶段

取值阶段从指令中读出10个字节,第一个字节为“指令字节”(Split),被分为两个4位的数交给标号为“inode”和“ifun”的控制逻辑快计算指令和功能码,根据icode的值我们可以确定下面三个一位的信号:

  1. instr_valid:判断是否是一个合法指令(是否以0-B开头),用于发现不合法指令;
  2. need_regids:判断该指令是否包含寄存器指示符字节;
  3. need_valC:判断该指令是否包含一个常数。
  4. 信号instr_valid和imem_error用来在访存阶段设置条件码。

结合“HCL描述中使用的常数值”和第一节Y86-64的指令编码示意图,我们可以写出need_regids、 need_valC等信号的HCL描述:

bool need_regids=
    icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ,
               IIRMOVQ, IRMMOVQ, IMRMOVQ};
               
bool need_valC =
    icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL };

取值阶段取出的其余9个字节为寄存器指示符字节和长数字的组合编码,标号为“Align”的硬件单元对其进行处理,产生rA、rB和valC。

PC增加器通过当前PC值“p”和两个信号need_regids的值“r”、need_valC的值“i”产生新的IP值=p+1+r+8*i

2.译码和写回阶段

SEQ的译码和写回阶段详细情况的示意图如下:

寄存器文件有四个端口,它支持同时进行两个读(端口:A、B)和两个写(端口:E、M),底部四个块dstE、dstM、srcA、srcB产生四个不同的寄存器文件的寄存器ID,以“src-”开头的表明要从哪个寄存器中读,“dst-”开头的表明要写入哪个寄存器中。

结合“HCL描述中使用的常数值”、第一节Y86-64的指令编码示意图和Y86-64的顺序实现中每个指令六个阶段的操作,我们可以写出srcA、srcB、dstE和dstM的HCL描述(其中RRSP为寄存器%rsp的寄存器ID):

word srcA = [
    icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : rA;
    icode in { IPOPQ, IRET } : RESP;
    1 : RNONE; # Don’t need register
];

word srcB = [
    icode in { IOPQ, IRMMOVQ, IMRMOVQ } : rB;
    icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RESP;
    1 : RNONE; # Don’t need register
];

int dstE = [
    icode in { IRRMOVQ } : rB;
    icode in { IIRMOVL, IOPL } : rB;
    icode in { IPUSHQ, IPOPL, ICALL, IRET } : RESP;
    1 : RNONE; # Don’t need register
];

int dstM = [
    icode in { IMRMOVL, IPOPL } : rA;
    1 : RNONE; # Don’t need register
];

注:

  1. 这里srcA和srcB表示该读哪个寄存器以产生信号valA和valB,有的指令虽然用到了“rA”,但并不是把它作为读取数据的源寄存器,如“mrmovq”,所以在srcA的HCL的描述中第一行没有出现“IMRMOVQ”。
  2. 有的源寄存器是与堆栈有关的%rsp寄存器,要单独分一种情况进行讨论。
  3. dstE所指向的寄存器接收的是有ALU计算出来的值,dstM寄存器接收的是从内存中读出的值。
  4. 只有popq指令会同时用到寄存器文件的两个写端口,E、M两个端口中M端口的优先级高,实现了将从内存中读出的值放入%rsp。
执行阶段

SEQ执行阶段的详细情况示意图如下所示:

执行阶段包括算数/逻辑单元(ALU),它根据alufun信号(来源:icode和ifun)信号的设置,对输入aluA和aluB进行整数运算,或作为加法器。根据ALU的值(valE)设置条件码寄存器,检测调配间吗的值,判断是否该选择分支。

根据指令的类型,aluA的值可以是valA、valC,或者是-8或+8,结合“HCL描述中使用的常数值”、第一节Y86-64的指令编码示意图和Y86-64的顺序实现中每个指令六个阶段的操作,aluA和aluB的HCL描述如下:

int aluA = [
    icode in { IRRMOVQ, IOPQ } : valA;
    icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ } : valC;
    icode in { ICALL, IPUSHL } : -8;
    icode in { IRET, IPOPQ } : 8;
    # Other instructions don’t need ALU
];

int aluB = [
    icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL,
    IPUSHQ, IRET, IPOPQ } : valB;
    icode in { IRRMOVQ, IIRMOVQ } : 0;
    # Other instructions don’t need ALU
];

alufun信号决定了ALU执行整数运算还是作为加法器,其HCL描述如下:

int alufun = [
    icode == IOPQ : ifun;
    1 : ALUADD;
];

set_CC信号用来控制是否该更新条件码寄存器(只有在执行OPq指令时才更新条件码寄存器),其HCL描述如下:

bool set_cc = icode in { IOPQ };

标号“cond”的硬件单元会根据条件码和功能码来确定是否进行条件分支或者数据传送。它产生信号Cnd,用于设置条件传送dstE,Cnd信号可以被置为0或1。条件传送指令“comvXX”的指令代码为IRRMOVQ,我们可以通过课后练习4.24来检验自己对执行阶段和写回阶段HCL描述的掌握程度。

访存阶段

SEQ访存阶段的具体情况示意图如下:

SEQ访存阶段的任务是读或者写程序数据,下侧两个控制块产生内存地址和内存输入,左侧两个控制块表明应该执行读操作还是写操作,其中内存读和写的地址“Mem.addr”总是valE或valA,其中内存写的数据“Mem.data”总是valA或valP,这两个单元块的HCL描述为:

word mem_addr = [
    icode in { IRMMOVQ, IPUSHQ, ICALL, IMRMOVQ } : valE;
    icode in { IPOPQ, IRET } : valA;
    # Other instructions don’t need address
];

word mem_data = [
    # Value from register
    icode in { IRMMOVQ, IPUSHQ } : valA;
    # Return PC
    icode == ICALL : valP;
    # Default: Don’t write anything
];

控制读内存或者写内存的信号Mem.read和Mem.write的HCL描述为:

bool mem_read = icode in { IMRMOVQ, IPOPQ, IRET };

bool mem_write = icode in { IRMMOVQ, IPUSHQ, ICALL };

访存最后的功能是根据取值阶段产生的icode、imem_error、instr_valid值以及数据内存产生的dmem_error信号,从指令执行的结果来计算状态码Stat,其HCL描述如下:

int Stat = [
    imem_error || dmem_error : SADR;
    !instr_valid: SINS;
    icode == IHALT : SHLT;
    1 : SAOK;
];

注:其中SADR表示遇到非法地址,SINS表示遇到非法指令,SHLT表示遇到halt指令,SAOK表示正常操作。这些状态码的设置条件,与信号imem_error、dmem_error、dmem_error和icode的产生条件息息相关。例如,当取值阶段解析出该指令为halt指令时,会将icode赋值为IHALT,所以这里会有icode == IHALT : SHLT;

更新PC阶段

SEQ更新PC阶段详细情况的示意图如下:

SEQ中的第六个阶段会产生新的PC值,依据指令类型和是否要选择分支,新的PC可能是valC、valM或valP,其HCL描述为:

int new_pc = [
    # Call. Use instruction constant
    icode == ICALL : valC;
    # Taken branch. Use instruction constant
    icode == IJXX && Cnd : valC;
    # Completion of RET instruction. Use value from stack
    icode == IRET : valM;
    # Default: Use incremented PC
    1 : valP;
];

返回目录


四、流水线的通用原理

流水线的一个重要铁星就是提高了系统的吞吐量,也就是单位时间内服务的顾客总数,不过它也会轻微的增加延迟,也就是从头到尾执行一条指令所需要的时间,它等于吞吐量的倒数。

在现代逻辑设计中,电路延迟以微微秒或皮秒(ps)也就是10^-12秒为单位来计算。吞吐量以每秒千兆条指令(GIPS),也就是每秒十亿条指令为单位来描述。

吞吐量的计算公式如下,其中lns=10^-9s:

下面有几个因素会降低流水线的效率:

  1. 不一致的划分
  2. 流水线过深,收益反而下降

此外我们将流水线技术引入Y86-64处理器时,必须正确处理反馈的影响。

返回目录


五、Y86-64的流水线实现

本章的主要任务:设计一个流水线化的Y86-64处理器,首先要对顺序的SEQ处理器做一点改动,将PC的计算移到取值阶段,在各个阶段之间加上流水线寄存器。为了实现一个高效的、流水线化的实现Y86-64ISA的处理器,我们对SEQ的具体改进如下所述:

1.PC的计算挪到取指阶段

这么做是为了使更新PC阶段在一个时钟周期开始时执行,计算当前指令的PC值。创建状态寄存器来保存在一条指令执行过程中计算出来的信号,我们将这些寄存器标号为“pIcode”、“pCnd”等等,保存前一个周期中产生的控制信号。SEQ与SEQ+在计算CP极端的对比图如下:

2.电路重定时

SEQ到SEQ+中对状态单元的改变是一种很通用的改进的例子,这种改进称为“电路重定时”,重定时改变了一个系统的状态表示,但是并不改变它的逻辑行为。通常用它来平衡一个流水线系统中各个极端之间的延迟。

3.插入流水线寄存器

我们要在SEQ+的各个阶段之间插入流水线寄存器,并对信号重新排列,得到PIPE-处理器,其抽象结构如下图所示,其中黑色方框表示流水线寄存器,白色方框表示每个寄存器包括的不同字段,表示实际的硬件组成:

其中流水线寄存器按如下方式标号:

  • F 保存程序计数器的预测值
  • D 位于取值和译码之间
  • E 位于译码和执行阶段之间
  • M 位于执行和访存阶段之间
  • W 位于访存阶段和反馈路径之间。

SEQ+的硬件结构如下图所示:

4.对信号进行重排和标号

在流水线的设计中,与各个指令相关联的各种信号值有多个版本,但是税讯实现SEQ、SEQ+在一个时刻只能处理一条指令,我们需要确保使用的是正确版本的信号。我们采用命名机制是:在信号名前面加上大写的流水线寄存器名字作为前缀,存储在流水线寄存器中的信号可以唯一地被标识。例如,4个状态码可以被命名为D_stat、E_stat、M_stat和W_stat。

在PIPE-中,会在流水线中一直携带dstE、dstM这些信号穿过执行和访存阶段,直到写回阶段才送到寄存器文件,这样确保写端口的地址和数据输入是来自同一条指令。

PIPE-中有一个块在相同表示形式的SEQ+中是没有的,那就是译码阶段中标号为“Select A”的块,这个块是为了减少要携带给流水线寄存器E和M的状态数量。

5.处理流水线冒险

①用暂停来避免冒险

②用转发来避免数据冒险

③加载/使用数据冒险

④避免控制冒险

返回目录


教材课后习题总结


4.1题:解析已作为介绍Y86-64指令编码的例子给出。

4.2题:考察“字节序列与Y86-64指令之间的转换”。此题关键要根据“代码部分”(即指令编码的前4位)确定其指令编码长度,从而将一个整体的编码划分为不同的指令。

『例如』:A.0x100:30f3fcffffffffffffff40630008000000000000字节序列转换为Y86-64指令为:

0x100: irmovq $-4,%rbx
0x10a: rmmovq %rsi,0x0800(%rbx)

『解析』:通过“30”我们得知第一个指令为irmovq ……,编码长度为10字节,所以可以确定从“40”开始为下一个指令rmmovq ……;再看寄存器指示符字节和数值部分,通过观察“Y86-64寄存器标识符”可以确定“30f3fcffffffffffffff”中的第二字节“f3”表示该指令至用到一个寄存器“%rbx”,后面八字节数字为以小端方式组织的立即数,将“fcffffffffffffff”转换为真值为-4。其他字节序列的转换过程类似,这里不再赘述。

4.3题:有了iaddq指令,我们可以省去P251页Y86-64代码中的第2-3行,直接用iaddq $8,%rdiiaddq $-1,%rsi两条指令替换10-11行。

4.4题&4.5题:此题涉及到我们常用的几种操作:

  • xorq %rax,%rax:用异或来给某个数置零
  • andq %rsi,%rsi: 自身相与来设置条件码,判断一个数是否为0
  • pushq ……popq ……来保护某个寄存器中的值
  • xorq %r11,%r11subq %r10,%r11加上跳转语句jle……来取一个数的绝对值:当前%r11中的值为%r10的值的相反数,如果执行subq %r10,%r11后%r11小于等于0,则说明%r10中的值为正数,绝对值为其本身,如果%r11的值大于0,则说明%r10的值为负,用rrmovq %r11,%r10给%r10取反。

此外,我们还经常用“x∧0”来给一个数置0,用“x∨1”来给一个数置1.

4.11题:将第二行判断换成“A<=C:B”或“B<=C:B”,因为进入到第二行的前提是A不是最小的如果在此基础上A还小于等于C,则说明B是最小的,换个角度思考,如果A不是最小的,那么B和C中较小的就是最小的;

4.12题:这个题除了可以像答案中那样分每种情况讨论,也可以结合“&&”和“||”等符号,将同时输出“B”的情况连接在一起,如“(A<=B && B<=C) || (C<=B && B<=A) : B;”

4.24题:这里我们要改编dstE的HCL代码来实现条件传送指令,其实只需要在rrmovq对应的分支加一个判断Cnd信号的操作,如下所示:

int dstE = [
    icode in { IRRMOVQ } && Cnd: rB;
    icode in { IIRMOVL, IOPL } : rB;
    icode in { IPUSHQ, IPOPL, ICALL, IRET } : RESP;
    1 : RNONE; # Don’t need register
];
posted on 2017-12-17 11:42  鲸鱼的抽屉  阅读(2001)  评论(0编辑  收藏  举报