Lazy loaded imageICS04 处理器体系结构

type
status
date
slug
summary
tags
category
icon
password

Processor Architecture

处理器体系结构是计算机系统设计的核心领域之一,它连接了软件和硬件的世界。通过指令集架构(ISA),处理器设计者提供了一个抽象层,允许操作系统和应用程序以硬件可理解的方式运行指令。同时,处理器体系还包含逻辑设计部分,涉及如何用物理电路实现这些指令的执行。
处理器体系结构是计算机系统设计的核心领域之一,它连接了软件和硬件的世界。通过指令集架构(ISA),处理器设计者提供了一个抽象层,允许操作系统和应用程序以硬件可理解的方式运行指令。同时,处理器体系还包含逻辑设计部分,涉及如何用物理电路实现这些指令的执行。

ISA

指令集架构(ISA)是连接计算机软件与硬件的一座桥梁,定义了程序如何与底层硬件进行交互。ISA的设计覆盖从应用程序到物理芯片的整个软硬件协作过程:
 
page icon
Application → Compiler → OS → ISA → CPU → Circuit → Chip
  • ISA之上:专注于程序设计和指令序列的执行。
  • ISA之下:关注指令的高效执行和硬件设计指导。

Y86-64

CSAPP提出了一种x86-64架构的简化版本,即Y86-64,其主要内容如下:

硬件组成

  • 寄存器堆:15个寄存器,每个64位。
  • 条件码:包括ZF(零标志)、SF(符号标志)、OF(溢出标志);不包含CF(进位标志)。
  • 程序计数器(PC)%rip,存储当前指令地址。
  • 程序状态(Program Status):用于指示指令执行状态(如错误)。
  • 内存:动态随机存储器,采用小端法存储。

指令格式

  • 每条指令长度为1-10字节,从内存中逐字节读取。第一个字节决定指令的长度。
  • 指令设计比x86-64简化。
  • 每条指令都会访问或修改程序状态。

Y86-ISA指令示例

编码规则

  • 寄存器通过4位ID编码,与x86一致;F(15)表示“无寄存器”。
  • 指令由两部分组成:
    • 前4位:Instruction Code(指令码)。
    • 后4位:Function Code(功能码)。

示例1:加法指令addq

占用2字节,机器码格式为6 0 ra rb 功能:将寄存器ra和rb的值相加,并将结果存放到rb中。

示例2:数据传送指令XXmovq

  • 寄存器到寄存器传送rrmovq - 2 0 ra rb
  • 立即数到寄存器传送irmovq - 3 0 F rb imm(10字节)。
  • 寄存器到内存传送rmmovq - 4 0 ra rb imm(10字节)。
  • 内存到寄存器传送mrmovq - 5 0 rb ra imm(10字节)。

示例3:跳转指令jXX

占用9字节,机器码格式为7 fn imm 功能码fn决定跳转条件,立即数imm为绝对地址。

特殊指令

  • nop01,无操作。
  • halt00,停止指令执行。

Y86的运行时栈

  • 栈顶地址存储在%rsp寄存器中。
  • 栈从高地址向低地址增长:
    • push操作:先减小%rsp,再将数据写入栈顶。
    • pop操作:先读取栈顶数据,再增加%rsp
pushpop的指令码分别为AB

Y86的程序状态(Status Condition)

  • AOK:正常执行。
  • HLT:遇到halt指令,终止运行。
  • ADR:地址错误(如非法内存访问)。
  • INS:遇到非法指令。

Review:CISC vs RISC

CISC(Complex Instruction Set Computer)

  • 基于栈的指令集,使用栈传递参数。
  • 算术指令可直接操作内存中的数据。
  • 使用条件码(如ZF、SF、OF)记录算术/逻辑操作结果。
  • 指令集庞大,包含复杂指令。
  • Memory-Oriented:对内存的频繁访问。
  • 历史原因
    • 早期内存昂贵,代码长度需尽量缩短。
    • 编译器能力有限,复杂指令能降低编译需求。
  • 局限性
    • 处理器设计复杂,功耗高,价格昂贵。

RISC(Reduced Instruction Set Computer)

  • 精简的指令集,指令长度和执行时间固定。
  • Register-Oriented:更多地依赖寄存器操作,减少对内存的访问。
  • 固定长度指令:简化取指和解码。
  • 精简指令功能:执行、访存、回写更加高效。
  • 更少指令类型:多为简单操作。
  • 优势:更适合流水线操作:
    • 固定执行时间避免流水线停滞。
    • 简化指令集便于并行处理。

示例:RISC/ARM

  • 基于寄存器的指令集,典型设计包含32个通用寄存器。
  • 仅加载和存储指令(Load/Store)访问内存。
  • 不使用条件码,测试结果存储于寄存器中。

Logic Design(逻辑设计)

逻辑门与组合逻辑

  • 基本逻辑门与门(AND)、或门(OR)、非门(NOT)。
  • 输入高/低电平,输出对输入持续响应(有微小延迟)。
  • 组合逻辑:由逻辑门网络组成的电路,例如比较器、加法器等。
  • Hardware Control Language (HCL)
    • 类似C语言逻辑表达式,用于描述硬件控制逻辑。
    • 不同于C语言的短路逻辑,HCL表达式不会省略任何分支。
多选器(MUX)
ALU:Arithmetic Logic Unit,算术逻辑单元
  • 堆叠多个运算黑箱(例如累加器、减法、与、异或等)
  • 控制信号选择要计算的功能
  • 同时改变条件码

时序逻辑与寄存器

寄存器是电路中重要的存储单元。时序逻辑中的“寄存器”不同于上一章提到的“通用目的寄存器”,可称它为“时钟寄存器”,因为它的行为受时钟信号控制。
时钟寄存器由逻辑门组成的D触发器堆叠而成,它在时钟信号上升时加载输入,下降时保持输出稳定。配备时钟寄存器后的时序电路由于不会出现震荡,可以实现反馈与循环。
page icon

震荡(Oscillation)

在数字电路设计中,震荡是指电路输出在短时间内反复切换(高电平和低电平之间快速变化)的现象。这种现象通常是由于电路设计不当或信号反馈路径不稳定引起的。
在电路中,如果组合逻辑电路的输出直接反馈到输入,且没有适当的时钟信号控制,可能会导致输出信号在短时间内反复变化。
震荡会导致电路输出无法稳定在预期的逻辑电平,从而使电路无法正常工作。 震荡会导致电路中的晶体管频繁切换,增加动态功耗。 震荡会引入噪声,影响信号完整性,可能导致后续电路误判
通过使用时钟信号和同步设计方法,可以有效避免震荡,确保电路稳定工作。

寄存器堆与RAM

这里的寄存器是“通用目的寄存器”等用于存取数据的寄存器。寄存器堆(Register File)是多个寄存器的集合。寄存器堆通常包括读口和写口,读口通过多选器选择寄存器,输出对应值。写口接收写操作的寄存器地址和数据,并将数据写入对应寄存器中。
寄存器堆的典型设计是2个读口、1个写口。

SEQ/顺序硬件架构

page icon
计算机的核心任务是执行程序代码,而程序的执行过程可以被抽象为一系列指令的处理。在硬件层面,这些指令的处理需要经过取指(Fetch)、译码(Decode)、执行(Execute)、访存(Memory Access)和回写(Write Back)五个基本阶段,这些阶段共同构成了一个完整的指令周期(Instruction Cycle)。

SEQ的主要状态单元

  • PC(Program Counter)寄存器:存储下一条指令的地址。
  • CC(Condition Code)寄存器:存储条件码,用于记录算术逻辑单元(ALU)运算的结果状态。
  • 寄存器堆(Register File):用于存储临时变量,由程序显式管理的寄存器集合。
  • Memories:包括指令内存(Instruction Memory)和数据内存(Data Memory)。

SEQ的指令处理过程

顺序执行架构处理指令的过程分为以下五个阶段:
阶段
描述
取指
访问指令内存 根据指令长度增加PC,获取下一条指令 输出指令码
译码
解码指令,提取出源操作数、目标操作数 从寄存器文件中读取操作数值 输出源操作数值
执行
对源操作数值执行ALU操作 更新条件码寄存器(CC) 输出计算结果
访存
如果需要,访问数据内存(加载/存储)
写回
将结果保存到寄存器文件 更新PC以获取下一条指令
这五个阶段紧密衔接,将顺序电路的稳定性和指令的连续性结合起来。

1. 取指(Fetch)

取指阶段的主要任务是从指令内存中读取指令,具体步骤如下:
  1. PC的作用:程序计数器(PC)存储当前指令的地址。
  1. 读取指令:从指令内存中读取PC所指的指令内容,通过Split和Align分为icodeifunrArBvalC
  1. 更新PC:将PC加上指令长度,生成下一条指令的地址valP
notion image

2. 译码(Decode)

译码阶段的任务是解析指令并获取操作数,具体步骤如下:
  1. 解析操作码:从指令中提取操作码(Opcode)和寄存器序号srcAsrcB
  1. 确定操作类型:根据操作码识别指令类型(如加法、减法、跳转等)。
  1. 读取操作数:从寄存器堆中提取源操作数(Source Operands)valAvalB
notion image

3. 执行(Execute)

执行阶段是指令处理的核心,完成算术或逻辑操作。具体步骤如下:
  1. ALU操作:将源操作数valAvalB输入算术逻辑单元(ALU),执行指定的运算(如加法、减法、逻辑与等),输出运算结果valC
  1. 条件判断:根据运算结果修改条件码寄存器(CC)。
  1. 若指令需要做条件判断,Cond根据CC内容和ifun计算Cnd ,也即是否跳转。
notion image
page icon
在SEQ中,为了更好集成电路线,简化硬件设计,我们规定:
对于不需计算、直接传值的指令(rrmovq/irmovq),将寄存器序号编码在srcA中,译码阶段提取出valA,将valB置为0,ALU输出的valC即为valA的值。
对于地址传送指令(如pushq/popq/call/ret),它们都需要将地址加上或减去8;将寄存器序号编码在srcB中,译码阶段提取出valB,将valA置为+8或-8,通过ALU计算地址。

4. 访存(Memory Access)

访存阶段负责与数据内存交互,仅在需要访问内存的指令中执行,也即load/store指令,在Y86-64中是mrmovqrmmovq。具体步骤如下:
  1. 读取内存:从指定地址加载数据valM
  1. 写入内存:将数据valAvalP存储到指定的内存地址valE
notion image

5. 回写(Write Back)

回写阶段将计算结果写回寄存器堆,以供后续指令使用。具体步骤如下:
  1. 寄存器更新:将ALU或内存操作的结果存储到目标寄存器。
  1. PC更新:根据valP为下一条指令准备好新的PC。
寄存器堆的更新仅在时钟上升沿执行,确保写入的稳定性。
notion image

实现示例

OPq rA rB

  1. 取指
      • 以PC为索引,从指令内存读取指令的操作码OPq
      • 读取寄存器索引rArB,作为srcAsrcB
      • 该指令长度为2字节,计算下一条指令的PC值(PC+2),存放到valP
  1. 译码
      • 从寄存器堆中读取srcAsrcB的值,记为valAvalB
  1. 执行
      • 使用ALU对valAvalB执行运算,结果存放到valE
      • 根据结果更新CC寄存器。
  1. 访存:无访存操作,跳过该阶段。
  1. 回写
      • valE写回寄存器堆的rB寄存器。
  1. PC更新为valP

cmovXX rA rB

  1. 取指
      • 以PC为索引,从指令内存读取指令的操作码cmovXX
      • 读取寄存器索引rArB,作为srcAsrcB
      • 该指令长度为2字节,计算下一条指令的PC值(PC+2),存放到valP
  1. 译码
      • 从寄存器堆中读取srcA的值,记为valA
  1. 执行
      • 上面已经提到过,对于不需计算只取原值的操作,将valB 置为0。
      • 通过ALU计算valA + valB,结果存放到valE
      • 根据CC寄存器内容和指令类型计算cond,判断条件是否满足,如果不满足,将rB设置为F(不存在)。
  1. 访存:无需访存。
  1. 回写
      • valE写回rB,如果rBF则跳过写回。
  1. PC更新为valP

call dest

  1. 取指
      • 读取要跳转到的目标地址dest,作为valC
      • 该指令长度为9字节,计算下一条指令的PC值(PC+9),存放到valP
  1. 译码
      • 从寄存器堆中读取rsp的值,记为valB
  1. 执行
      • 上面已经提到过,call需要计算地址的操作(减去8),将valA置为-8。
      • 通过ALU计算valA + valB,结果存放到valE (也即新的rsp值)。
  1. 访存:
      • call指令需要将下一条指令的地址压入栈中。以valE 为地址,将值valP 写入到内存中。
  1. 回写
      • valE写回rsp(更新rsp)。
  1. PC更新为valC

最终,我们得到了SEQ的整体设计:
notion image

PIPE/流水线硬件架构

page icon
流水线(Pipeline)技术是现代处理器架构中至关重要的一部分,它通过将指令的执行过程划分为多个阶段,以提高指令的吞吐量。流水线化的设计不仅能提升处理器的执行效率,还能有效利用硬件资源。然而,流水线架构也伴随了许多挑战,包括冒险、数据依赖、控制依赖等问题。理解流水线的工作原理及其优化策略,对于设计高效的处理器和编写高性能的代码至关重要。

基本思路

流水线技术的思想来源于日常生活中的许多流水线操作。例如,餐厅做菜时需要经过洗菜、切菜、烹饪、出餐四个步骤,假设每个步骤需要10分钟;在上文SEQ的语境下,餐厅同一时间只处理一道菜,每道菜的出品需要40分钟,餐厅每40分钟才能出品一道菜;然而我们可以引入流水线机制,将四个步骤分配给四个员工处理,洗菜工洗完菜后将菜交给切菜工,立即开始洗下一份菜;这样,虽然每道菜的出品时间仍是40分钟(由于分工带来的时间成本,还可能更慢),但餐厅每10分钟即可出品一道菜。
体现在处理器设计中,流水线将顺序执行的指令流划分为多个阶段,每个阶段由不同的硬件单元执行,允许多条指令并行处理。
首先,我们将SEQ架构抽象为一大块组合逻辑+一组时钟寄存器。时钟信号控制着时钟寄存器的更新周期。假设组合逻辑的延迟为300ps,寄存器读写延迟为20ps,则总延迟为320ps,执行速率(每秒执行的指令数)为3.12GIPS;这意味着时钟周期需要大于320ps。
按照流水线的思路,我们将组合逻辑拆分成三个部分,每个部分的延迟为100ps,并使用三个时钟寄存器进行间隔。这样,每条指令的总延迟变为360ps;虽然指令执行变慢,但每120ps(上一条指令的第一部分执行完毕后)可以接入一条新指令,执行速率提升至8.33GIPS。
这些时钟寄存器称为流水线寄存器,它们在时钟上升沿写入数据,在时钟稳定期间保持稳定输出。时钟周期应大于流水线中最慢部分的延迟。
为进一步提高执行速率,可以将组合逻辑细分为更多的部分,进而引入更多的流水线寄存器。然而随着流水线的加深,寄存器延迟占据总延迟的比例逐渐升高,边际效益递减,收益下降。此外,处理冒险、分支错误和清除流水线的代价也会增加。

PIPE Hardware

区分数据类型

在流水线架构中,常常需要区分不同阶段之间的数据。数据有时稳定地存储在寄存器中,有时暂时地流经组合逻辑。
我们在数据名前面加上大写的S,表示这是存储在阶段S前的流水线寄存器(以S命名)中的数据。 数据名前加上小写的s,表示这是流经阶段S的组合逻辑中的数据。

反馈路径(Feedback Path)

反馈路径是指从后阶段传递到前阶段的电路线,它主要用于传递与指令执行相关的关键信息,例如:
  • 预测的程序计数器(PC)值。
  • 分支信号(指令是否需要跳转)。
  • 从内存中读取的指令地址。
由此,我们在SEQ的五阶段间加入流水线寄存器,调整PC更新位置,即可得到PIPE-架构图:
notion image

冒险(Hazards)

在流水线执行过程中,常常会遇到冒险问题,主要包括数据冒险、控制冒险和结构冒险。数据冒险是最常见的一种,指令之间的依赖关系可能导致错误的执行顺序。

数据冒险

数据冒险发生在前一条指令的结果作为后一条指令的操作数时(Read after Write,RAW)。例如我们有如下Y86-64汇编指令序列:
由于我们在PIPE-中部署了五阶段流水线(F-D-E-M-W),在movq 指令执行到D(译码)阶段时,需要从寄存器堆中提取rbx 的值;但此时addq 指令只执行到E(执行)阶段,rbx 的新值要等到W(写回)阶段结束后才能写入寄存器堆。
在实际程序中,这种情况非常常见,通常需要采取相应的策略来解决。
  • 策略1:使用nop指令填充
    • 在两条指令之间插入空操作(nop指令),以确保后一条指令的操作数已被前一条指令写入寄存器。这种方法需要在软件层面修改汇编代码。在上面的例子中,可以在两条指令间插入三条nop 指令:
       
  • 策略2:硬件层面插入气泡(Bubble)
    • 与策略1类似,硬件可以在指令进入解码阶段时,检查寄存器堆是否有需要写入的操作。如果有,将该指令暂停执行,直到相关的写入操作完成。此策略通过硬件实现类似nop的功能,称为“bubble”。
page icon
我们需要在PIPE-的基础上加入流水线控制单元,用于实现stall和bubble。stall使寄存器内容被冻结,仍存放上一个周期中的内容;bubble则清除原寄存器内容,改为nop指令的相关内容。
  • 策略3:数据转发(Bypass)
    • 数据转发是一种常用的优化策略。当指令进入解码阶段时,如果它需要访问的寄存器值已经在后续的执行阶段(例如内存或写回阶段),可以通过转发路径将该值直接传递给当前指令。这可以避免将指令暂停,提升流水线效率。
    • 局限性:如果后续阶段的值仍然在内存中(例如在mrmovq指令执行时,该值仍在Memory阶段),此时无法进行数据转发,称为“Load-Use”;可能需要插入“bubble”来兜底。
下图中,通过在Decode阶段加入“Forward”元件,我们可以将所有青色线的起点数据转发到Decode阶段。
notion image

控制冒险:预测PC的策略

控制冒险发生在指令的控制流发生变化时,特别是当指令需要更新程序计数器(PC)以确定下一条指令的位置。上一条指令完成Fetch、进入Decode阶段时,处理器就需要计算出下一条指令的地址(更新PC),这会带有一定风险性。具体而言可以分为以下几类:
  • 不传递控制:对于没有控制转移的指令,直接将valP(PC Increment的输出)设置为下一条PC值,通常不会出错。
  • 无条件跳转(Jump)/调用(Call):将valC设置为下一条PC值(总是正确的)。
  • 条件跳转:对于条件跳转,“总是跳转”策略将valC设置为下一条PC值,但只有在跳转条件成立时才是正确的。一般来说,“总是跳转”策略的准确性大约为60%。
    • 其他策略:当跳转发生在前面时,总是执行跳转;当跳转发生在后面时,通常不执行跳转。
    • 分支预测错误:若预测错误,可以通过将所有不该执行的指令设置为“bubble”来恢复正确的控制流。
  • 返回指令(ret):当ret指令完成Memory阶段、输出应跳转到的地址前,处理器将不进行任何预测,始终插入Bubble;共需要插入3个“bubble”来确保正确执行。

总结

在加入Forward/转发模块和处理器控制逻辑后,仍有三种可能的冒险情况,分别需要1、2、3个周期的Bubble:

Load-Use冒险

mrmovq指令紧跟着一条需要使用其目的寄存器的指令。例如:
andq 指令进入Decode阶段时,mrmovq 指令刚结束Decode、进入Execute阶段,还无法取出rax 应存放的数据。需要在Decode阶段插入一个Bubble,待momovq 指令进入Memory阶段、从内存中取出数据后,andq 指令才能顺利执行Decode阶段,从m_valM转发rax的值。

分支预测错误:

当条件跳转指令jXX实际不应该被执行时,需要清空流水线。例如:
jne指令进入Decode阶段时,“总是跳转”策略选择让dest处的subq指令进入Fetch阶段;当jne 执行Execute阶段时,处理器控制逻辑发现Cnd为非(也即不应跳转),它需要将Decode和Execute的寄存器插入两个Bubble,清空被错误执行的subqaddq指令,并将Fetch阶段PC更新为正确的xorq 所处地址。

RET冒险

ret指令进入Decode阶段时,需要插入三个Bubble,等待ret 指令完成Memory、进入Writeback阶段时,再从W_valM 中取出地址更新PC。具体而言,当ret 指令在Decode/Execute/Memory阶段时,都需要将Fetch寄存器置为Stall、将Decode寄存器置为Bubble。

总而言之,三种冒险的处理如下:
notion image
Prev
ICS03 汇编语言
Next
ICS05 存储器层次结构
Loading...
Article List
SunVapor的小站
计算机系统导论
文档