Back to Notes

计算机系统 - 体系结构

This post is not yet available in English. Showing the original version.
Table of Contents
Table of Contents

一、简单 CPU 设计实例(VS-CPU)

1.1 核心组件

一个最简 CPU 至少需要以下部件协同工作:

部件全称职责
PCProgram Counter存放下一条要取的指令地址
IRInstruction Register暂存当前刚取到的指令
MARMemory Address Register向内存发出要访问的地址
MDRMemory Data Register暂存从内存读到 / 要写入的数据
ALUArithmetic Logic Unit算术与逻辑运算
CUControl Unit解析指令、发出控制信号驱动各部件
通用寄存器R0, R1 …存放操作数和中间结果

MAR / MDR 是 CPU 与内存之间的"接口":CPU 想读内存时,先把地址放进 MAR,内存把数据送回 MDR;写内存时反过来,把数据放进 MDR、地址放进 MAR,然后发写信号。

1.2 指令执行周期(Instruction Cycle)

每条指令都经过以下阶段,不断循环:

flowchart LR
    F["取指 (Fetch)"] --> D["译码 (Decode)"] --> E["执行 (Execute)"]
    E --> F

取指(Fetch)

  1. MAR ← PC:把 PC 中的地址送到 MAR
  2. 内存根据 MAR 取出指令 → MDR ← Memory[MAR]
  3. IR ← MDR:指令存入 IR
  4. PC ← PC + 指令长度:PC 自增,指向下一条指令

译码(Decode)

执行(Execute)

VS-CPU 的简化之处:取指和执行共用同一组 MAR/MDR(所以取指和执行不能同时访问内存),这也是后面引入流水线时需要解决的核心冲突之一。

1.3 示例:一条 ADD 指令的完整数据通路

假设执行 ADD R0, R1(R0 = R0 + R1):

sequenceDiagram
    participant PC
    participant MAR
    participant MEM as 内存
    participant MDR
    participant IR
    participant CU as 控制器
    participant ALU

    Note over PC: PC = 0x0004
    PC->>MAR: ① MAR ← 0x0004
    MAR->>MEM: ② 发出地址
    MEM->>MDR: ③ 指令送入 MDR
    MDR->>IR: ④ IR ← 指令
    Note over PC: ⑤ PC ← 0x0008
    IR->>CU: ⑥ CU 译码:ADD R0, R1
    CU->>ALU: ⑦ 控制信号:做加法
    Note over ALU: ⑧ R0 + R1 → R0

1.4 控制器的状态机(FSM)

控制器(CU)的本质是一个有限状态机:每个时钟周期处于一个状态,根据当前状态和指令内容,决定发出哪些控制信号、跳转到哪个下一状态。

stateDiagram-v2
    [*] --> S0_取指
    S0_取指 --> S1_译码 : 指令已送入 IR
    S1_译码 --> S2_执行 : 操作码解析完成
    S2_执行 --> S3_访存 : 若为 load/store
    S2_执行 --> S4_写回 : 若为算术指令
    S3_访存 --> S4_写回
    S4_写回 --> S0_取指 : 下一条指令

状态机驱动一切:CPU 的节拍(时钟)每跳动一次,状态机就推进一步,输出一组控制信号去驱动各个器件。所以 CPU 的执行过程就是状态机不断"转圈"的过程。

1.5 器件控制信号

每个状态下,CU 通过控制信号线告诉各器件"该做什么"。以 VS-CPU 为例:

控制信号作用值为 1 时
PCoutPC → 总线将 PC 的值送上总线
MARin总线 → MARMAR 锁存总线上的地址
MemRead存储器读内存开始读操作,数据送入 MDR
MDRoutMDR → 总线将 MDR 的值送上总线
IRin总线 → IRIR 锁存总线上的指令
PCincPC 自增PC ← PC + 指令长度
Rin / Rout寄存器读写将指定寄存器读出/写入总线
ALUopALU 操作类型选择加/减/与/或等运算
MemWrite存储器写内存开始写操作

取指阶段的控制信号时序

取指过程拆成微操作后,每一步对应一组信号:

微操作激活的控制信号说明
MAR ← PCPCout, MARinPC 送地址到 MAR
MDR ← Memory[MAR]MemRead内存读,数据送入 MDR
IR ← MDRMDRout, IRinMDR 内容存入 IR
PC ← PC + 1PCincPC 指向下一条指令

执行阶段示例:ADD R0, R1

微操作激活的控制信号说明
ALU_A ← R0R0outR0 值送入 ALU 输入端 A
ALU_B ← R1R1outR1 值送入 ALU 输入端 B
R0 ← ALU_resultALUop=ADD, R0inALU 做加法,结果写回 R0

关键理解:一条汇编指令 = 若干微操作(micro-operation)= 若干组控制信号的时序组合。控制器的工作就是把指令"翻译"成这些信号序列。

1.6 控制器的两种实现:硬布线 vs 微程序

上面说的"状态机输出控制信号"只是逻辑模型,硬件上怎么实现这个状态机有两条路线:

硬布线控制器微程序控制器(微序列控制器)
实现方式组合逻辑电路直接产生信号将控制信号编码为微指令,存在控制存储器(CS)中
速度快(纯电路)稍慢(多一次查表)
灵活性差(改指令集要重新布线)好(改微程序即可)
典型应用RISC(指令简单,电路规模可控)CISC(指令复杂,硬布线太庞大)
类比把菜谱刻在灶台上把菜谱写在纸上,做菜时翻着看

1.7 微程序控制器详解

核心概念

术语说明
微操作 (μop)一个最小的硬件动作,如 PCoutMARin
微指令 (μ-instruction)一组同时执行的微操作的编码,一个时钟周期执行一条微指令
微程序 (μ-program)实现一条机器指令所需的微指令序列
控制存储器 (CS / Control Store)存放所有微程序的只读存储器(ROM)
μPC (微程序计数器)指向当前要执行的微指令地址
μIR (微指令寄存器)暂存当前正在执行的微指令

类比:机器指令就像"一道菜名",微程序就像"这道菜的详细食谱步骤",控制存储器就像"食谱大全"。

微指令的格式

一条微指令通常由两部分组成:

字段内容说明
控制字段(Control)每一位对应一根控制信号线决定本周期激活哪些微操作
下地址字段(Next Address)下一条微指令的地址决定下一步执行哪条微指令

控制字段的编码方式

水平微代码(Horizontal Microcode)= 直接编码

控制字段中每一位直接对应一根控制信号线,1=激活,0=不激活。

特点说明
微指令宽度很宽(有多少控制信号就有多少位,可达几十~上百位)
并行度 —— 一条微指令可以同时激活多个不互斥的信号
译码无需译码,控制字段直接驱动信号线
速度
缺点控制存储器占用空间大(每条微指令都很长)

直接编码示例(假设 8 个控制信号):

微指令PCoutMARinMemReadMDRoutIRinPCincRoutALUop下地址
取指 ①1100000000x02
取指 ②0010000000x03
取指 ③0001100000x04
取指 ④000001000→ 译码

注意取指 ① 中 PCoutMARin 同时为 1:这就是水平微代码的优势——一个周期内可以并行完成"PC 输出到总线"和"MAR 锁存"两个动作。

垂直微代码(Vertical Microcode)= 字段编码

互斥的控制信号分组,每组用较少的编码位表示,通过译码器还原为实际信号。

为什么可以分组? 因为某些信号天然互斥——同一时刻总线上只能有一个来源输出:

信号组(互斥)成员编码位数
总线来源PCout, MDRout, R0out, R1out(4 选 1)2 位
总线目标MARin, IRin, R0in, R1in(4 选 1)2 位
内存操作无, MemRead, MemWrite(3 选 1)2 位
ALU 操作无, ADD, SUB, AND, OR(5 选 1)3 位

对比同一条微指令(取指 ①:MAR ← PC):

编码方式表示总位数
水平(直接)PCout=1, MARin=1, 其余全08+ 位
垂直(字段)总线来源=00(PC), 总线目标=00(MAR), 内存=00(无), ALU=000(无)9 位

这个例子里差异不大,但当控制信号有 50~100 根时,垂直编码可以从 100 位压缩到 20~30 位,控制存储器体积大幅缩小。

水平 vs 垂直 总结

水平微代码垂直微代码
微指令宽度宽(= 信号数)窄(编码压缩)
并行能力强(直接指定多个信号)受限(每组只能选一个)
执行速度快(无需译码)稍慢(需译码器展开)
控制存储器大小
编程难度较难(要考虑信号兼容性)较易(类似写汇编)
实际应用高性能场景追求紧凑的场景

现实中多采用混合编码:对需要高并行度的信号组用直接编码,对互斥明显的信号组用字段编码,兼顾速度与空间。

微程序控制器的工作流程

flowchart TD
    A["机器指令送入 IR"] --> B["IR 中的操作码 → 映射逻辑"]
    B --> C["生成微程序入口地址 → μPC"]
    C --> D["CS[μPC] → μIR(取微指令)"]
    D --> E["μIR 控制字段 → 激活控制信号"]
    E --> F["各器件执行微操作"]
    F --> G{"下地址字段"}
    G -->|"顺序执行"| H["μPC ← 下地址"]
    H --> D
    G -->|"微程序结束"| I["回到取指微程序"]
    I --> D

完整执行示例:ADD R0, R1 的微程序

步骤μPC微指令内容激活信号
取指 10x00MAR ← PCPCout, MARin
取指 20x01MDR ← MemoryMemRead
取指 30x02IR ← MDRMDRout, IRin
取指 40x03PC++PCinc
译码0x04操作码 → 查找 ADD 的微程序入口
执行 10x10ALU_A ← R0R0out
执行 20x11ALU_B ← R1R1out
执行 30x12R0 ← ALU结果,跳回取指ALUop=ADD, R0in

取指微程序是所有指令共享的——无论哪条机器指令,取指阶段都执行同一段微程序(0x00~0x03)。译码之后才分流到各指令自己的微程序。


二、CISC 与 RISC

2.1 两种设计哲学

CISCRISC
全称Complex Instruction Set ComputerReduced Instruction Set Computer
核心思路少量复杂指令完成任务大量简单指令组合完成任务
代表架构x86 / x86-64ARM, RISC-V, MIPS

2.2 关键差异对比

特征CISCRISC
指令长度可变长(x86:1~15 字节)定长(通常 4 字节 / 32 位)
指令数量多(数百条)少(几十~百余条)
单条指令能力强,一条可完成复杂操作弱,每条只做一件简单的事
访存指令算术指令可直接操作内存只有 load / store 能访问内存(load-store 架构)
寄存器数量较少(x86 只有 8/16 个通用寄存器)多(32 个及以上)
执行周期一条指令可能需要多个时钟周期大多数指令 1 个周期完成
流水线友好度低(变长指令难以对齐)高(定长指令天然适合流水线)
代码密度高(一条指令做的事多,程序短)低(需要更多指令,程序更长)

2.3 一个直观的例子

实现 a = a + Memory[addr]

CISC(x86) — 一条指令搞定:

addl  (addr), %eax    # eax += Memory[addr],访存 + 运算一步完成

RISC(ARM 风格) — 拆成三步:

ldr   r1, [r2]        # ① load:r1 = Memory[r2]
add   r0, r0, r1      # ② 运算:r0 = r0 + r1
str   r0, [r3]        # ③ store:Memory[r3] = r0(如果需要写回)

现代趋势:x86 表面上仍是 CISC 指令集,但 CPU 内部会把复杂指令拆成微操作(μops),本质上用 RISC 的方式执行。所以现代 CPU 是"外 CISC 内 RISC"。

2.4 为什么 RISC 更容易做流水线?

定长指令意味着:


三、指令执行与流水线

3.1 从单周期到流水线

单周期 CPU

每条指令用一个完整时钟周期完成所有工作(取指 → 译码 → 执行 → 访存 → 写回)。

多周期 CPU

每条指令拆成多个时钟周期,每周期做一步。不同指令可以用不同数量的周期。

流水线 CPU

类似工厂的流水装配线:把指令执行过程拆成多个独立阶段,让多条指令同时处于不同阶段

3.2 经典五级流水线

阶段缩写工作
取指IF (Instruction Fetch)从内存取出指令,PC+4
译码ID (Instruction Decode)解析指令 + 读寄存器
执行EX (Execute)ALU 运算 / 计算地址
访存MEM (Memory Access)load/store 读写内存
写回WB (Write Back)将结果写入目标寄存器
gantt
    title 五级流水线时序
    dateFormat X
    axisFormat %s
    section 指令 1
        IF : 0, 1
        ID : 1, 2
        EX : 2, 3
        MEM : 3, 4
        WB : 4, 5
    section 指令 2
        IF : 1, 2
        ID : 2, 3
        EX : 3, 4
        MEM : 4, 5
        WB : 5, 6
    section 指令 3
        IF : 2, 3
        ID : 3, 4
        EX : 4, 5
        MEM : 5, 6
        WB : 6, 7
    section 指令 4
        IF : 3, 4
        ID : 4, 5
        EX : 5, 6
        MEM : 6, 7
        WB : 7, 8

理想加速比:5 级流水线理论上可以达到 5 倍吞吐量(不是每条指令变快了,而是单位时间完成的指令数增加了)。实际上由于冒险(hazard)的存在,达不到理想值。

3.3 流水线冒险(Hazards)

流水线并非总能顺畅运行,三类冒险会打断流水

① 结构冒险(Structural Hazard)

原因:多条指令同时需要同一个硬件资源。

典型场景:指令 1 在 MEM 阶段读内存,同时指令 4 在 IF 阶段也要读内存——如果只有一个内存端口,就冲突了。

解决

② 数据冒险(Data Hazard)

原因:后续指令需要用到前一条指令尚未写回的结果。

addl  %ebx, %eax    # 指令 A:eax = eax + ebx(结果在 WB 阶段才写回)
subl  %eax, %ecx    # 指令 B:需要用 eax 的新值 ← 但 A 还没写回!
gantt
    title 数据冒险示意
    dateFormat X
    axisFormat %s
    section 指令 A
        IF : 0, 1
        ID : 1, 2
        EX : 2, 3
        MEM : 3, 4
        WB(写回 eax): crit, 4, 5
    section 指令 B
        IF : 1, 2
        ID(读 eax ⚠️ 旧值): crit, 2, 3
        EX : 3, 4
        MEM : 4, 5
        WB : 5, 6

解决

③ 控制冒险(Control Hazard)

原因:遇到分支 / 跳转指令时,下一条该取哪条指令?流水线已经预取了后续指令,但如果分支跳转了,预取的就白费了。

cmpl  %eax, %ebx
je    target          # 如果相等就跳转 → 流水线已经开始取 je 后面的指令了
addl  $1, %ecx       # 这条被预取了,但如果跳转发生,它不该执行

解决

3.4 流水线性能小结

因素影响
流水线级数级数越多,理论吞吐量越高,但冒险代价也越大
冒险频率数据依赖 / 分支越多,气泡越多,实际吞吐量下降
转发 / 预测有效的转发和预测可以大幅减少停顿
CPI理想 CPI = 1(每周期完成一条指令),实际 CPI > 1

CPI(Cycles Per Instruction):平均每条指令消耗的时钟周期数。流水线的目标是让 CPI 尽可能接近 1;超标量 CPU 甚至可以做到 CPI < 1(每周期完成多条指令)。



0 / 2000
Loading comments...