0%
9 min read

计算机组成原理Notes

本篇笔记主要记录了在学习计算机组成原理过程中所做的笔记,内容涵盖了计算机系统的基本组成、指令系统、数据通路设计等方面的知识。

notes computer cpu

本门课程的主要内容

  • CPU架构
  • 汇编语言(主要是RISC-V,包含少量MIPS)

CPU架构

Core of CPU

ALU

CU

寄存器

  • 通用寄存器
  • 程序计数器(PC): 存储下一条指令的地址, 执行以后自动加4
  • 指令寄存器(IR): 存储当前正在执行的指令
  • 状态寄存器(SR): 存储CPU的状态信息,如条件码、控制位等
  • 浮点寄存器(FPU): 用于浮点运算的寄存器
  • 特殊寄存器(如堆栈指针SP、全局指针GP等): 用于特定功能的寄存器

Hazard Unit

数据通路设计

Pipline

Branch Prediction

Cache

Hazard Unit

BUS

RISC-V指令集

数据表示

  • 整数:有符号整数使用补码表示,无符号整数直接使用二进制表示。
  • 浮点数:使用IEEE 754标准表示,分为单精度(32位)和双精度(64位)。
  • 字符:使用ASCII编码,每个字符占用一个字节(8位)。

拓展

  • 零拓展(Zero Extension):将较小的数据类型扩展为较大的数据类型时,在高位填充0。例如,将一个8位的无符号整数0x7F(127)零拓展为16位,结果是0x007F。
  • 符号拓展(Sign Extension):将较小的数据类型扩展为较大的数据类型时,在高位填充符号位(即最高位)。例如,将一个8位的有符号整数0xFF(-1)符号拓展为16位,结果是0xFFFF。

数据对齐

RISC-V要求数据在内存中按照其大小进行对齐。例如,32位整数必须存储在4字节边界上,64位整数必须存储在8字节边界上。数据对齐可以提高内存访问效率,但也可能导致内存浪费。

RISC-V算术指令

add x1, x2, x3  # x1 = x2 + x3
sub x1, x2, x3  # x1 = x2 - x3
addi x1, x2, 10 # x1 = x2 + 10
sll x1, x2, 5   # x1 = x2 << 5
srl x1, x2, 5   # x1 = x2 >> 5
xor x1, x2, x3  # x1 = x2 ^ x3 
  • add: 加法
  • sub: 减法
  • addi: 加法立即数
  • sll: shift left logical
  • srl: shift right logical
  • xor: 按位异或
  • addu, subu…: 无符号加法、无符号减法等指令,区别在于是否考虑溢出。

RISC-V内存访问指令

lw x1, 0(x2)   # 从地址x2 + 0处加载一个字到x1
sw x1, 0(x2)   # 将x1中的字存储到地址x2 + 0处
  • lw: Load Word, 从第二个操作数指定的内存地址加载一个字(4字节)到第一个操作数指定的寄存器中。注意,不是将内存地址加载到寄存器,而是将内存地址处的内容加载到寄存器。
  • sw: Store Word

RISC-V控制流指令

beq x1, x2, label  # 如果x1 == x2,则跳转到label
bne x1, x2, label  # 如果x1 != x2,则跳转到label
j label           # 无条件跳转到label
jal x1, label      # 将返回地址存储到x1,并跳转到label
jalr x1, 0(x2)    # 将返回地址存储到x1,并跳转到x2 + 0
  • beq: Branch if Equal
  • bne: Branch if Not Equal
  • j: Jump
  • jal: Jump and Link
  • jalr: Jump and Link Register

我们来看一个简单的while循环:

loop:
    beq x1, x2, exit  # 如果x1 == x2,则跳转到exit
    addi x1, x1, 1   # x1 = x1 + 1
    j loop           # 跳转回loop标签处继续执行
exit:

如果是For循环则稍微复杂一些,尤其是操作数组(连续内存)时:

    li x1, 0          # i = 0
    li x2, 10         # n = 10
loop:
    beq x1, x2, exit  # 如果i == n,则跳转到exit
    lw x3, 0(x4)      # 从数组地址x4 + i*4处加载一个字到x3
    addi x1, x1, 1   # i = i + 1
    addi x4, x4, 4   # 移动到下一个数组元素的地址
    j loop           # 跳转回loop标签处继续执行

上述的实现是比较简洁的。

RISC-V系统调用

li a7, 1        # 系统调用号1:打印整数
li a0, 42       # 要打印的整数
blt a7, 0, exit   # 如果系统调用号小于0,跳转到exit
bge a7, 10, exit  # 如果系统调用号大于等于10,跳转到exit
bltu a7, 0, exit   # 如果系统调用号小于0,跳转到exit
bgeu a7, 10, exit  # 如果系统调用号大于等于10,跳转到exit
ecall           # 触发系统调用
  • li: Load Immediate
  • ecall: Environment Call (系统调用指令)

RISC-V伪指令

li x1, 1000     # 伪指令,加载立即数1000到x1
mv x1, x2       # 伪指令,x1 = x2

伪指令是汇编器提供的便利指令,实际会被翻译成一系列基本指令。

过程调用

在计算机中,我们通常需要调用程序来处理数据,这就涉及到过程调用。过程调用是指在程序中调用一个函数或子程序来执行特定的任务。在RISC-V中,过程调用通常使用jal和jalr指令来实现。

程序计数器

程序计数器(PC)是一个特殊的寄存器,用于存储下一条指令的地址。当执行jal或jalr指令时,PC会被更新为目标地址,同时将返回地址存储在指定的寄存器中,以便在函数执行完成后能够返回到调用点继续执行。

每执行一条指令,PC会自动加4(因为每条指令占用4字节),除非遇到跳转指令(如jal或jalr),此时PC会被更新为跳转目标地址。这就是为什么汇编程序会自动顺序执行,除非遇到跳转指令。例如,在以下代码中:

beq x1, x2, else  # 如果x1 == x2,则跳转到else
addi x1, x1, 1   # x1 = x1 + 1
else: add x3, x1, x2   # x3 = x1 + x2

此时,与C语言不同,程序不会自动识别“else”标签,无论如何else标签后面的指令都会被执行,除非在else标签前面有一个跳转指令(如beq)将程序跳转到例如exit之类的退出标签处。

寄存器约定

在RISC-V中,寄存器约定是一种约定俗成的规则,用于规定不同寄存器的用途和使用方式。以下是一些常见的寄存器约定:

  • x0: 零寄存器,始终为0
  • x1-x7: 临时寄存器,函数调用时不需要保存
  • x8-x15: 保存寄存器,函数调用时需要保存
  • x16-x23: 参数寄存器,用于传递函数参数
  • x24-x31: 保留寄存器,供操作系统使用
  • a0-a7: 用于函数返回值和参数传递的寄存器(caller reg)
  • s0-s11: 用于保存函数调用过程中需要保留的寄存器(callee reg)
  • t0-t6: 用于临时计算的寄存器(caller reg)
  • ra: 返回地址寄存器,用于存储函数返回地址(caller reg)
  • sp: 堆栈指针寄存器,用于管理函数调用的堆栈
  • gp: 全局指针寄存器,用于访问全局数据
  • tp: 线程指针寄存器,用于访问线程局部数据
  • fp: 帧指针寄存器,用于管理函数调用的栈帧

上述的寄存器约定有助于程序员在编写汇编代码时更好地组织和管理寄存器的使用,避免冲突和混乱。

Caller reg and Callee reg:

Caller reg指的是,调用之后Caller中需要恢复的寄存器,Callee reg指的是,调用之后Callee中需要恢复的寄存器

调用过程

在RISC-V中,调用过程通常涉及以下步骤:

  1. 将函数参数传递到指定的寄存器(如a0-a7)。
  2. 使用jal指令跳转到函数的入口地址,并将返回地址(一般是pc+4)存储到ra寄存器中。在jal命令中,我们只需要指定函数的入口地址,编译器会自动计算返回地址并存储到ra寄存器中。
  3. 在函数内部执行所需的操作,可能会使用s0-s11寄存器来保存需要保留的值。
  4. 使用jalr指令返回到调用点,跳转到ra寄存器中存储的返回地址。

Leaf Procedure

Leaf Procedure(叶子过程)是指在函数调用过程中不再调用其他函数的函数。由于Leaf Procedure不需要保存返回地址,因此可以直接使用jalr指令返回到调用点,而不需要使用jal指令跳转到函数入口地址。这种优化可以提高函数调用的效率,减少不必要的跳转和寄存器使用。

我们使用sp寄存器来管理函数调用的堆栈空间。在Leaf Procedure中,我们可以直接使用sp寄存器来分配和释放局部变量的空间,而不需要担心返回地址的保存和恢复。例如:

leaf_function:
    addi sp, sp, -16  # 为局部变量分配空间
    sw ra, 12(sp)     # 保存返回地址
    # 执行函数体的操作
    lw ra, 12(sp)     # 恢复返回地址
    addi sp, sp, 16   # 释放局部变量空间
    jalr x0, 0(ra)   # 返回到调用点

sp指针从高地址向低地址增长,因此在分配空间时需要将sp减去相应的字节数,在释放空间时需要将sp加回相应的字节数。如果不恢复栈指针,可能会导致栈空间泄漏或栈溢出等问题。

Non-Leaf Procedure

Non-Leaf Procedure(非叶子过程)是指在函数调用过程中会调用其他函数的函数。由于Non-Leaf Procedure需要保存返回地址,因此必须使用jal指令跳转到函数入口地址,并将返回地址存储到ra寄存器中。在Non-Leaf Procedure中,可能还需要使用s0-s11寄存器来保存需要保留的值,以便在调用其他函数时不丢失重要数据。

嵌套与递归

在RISC-V中,嵌套函数调用和递归函数调用都可以通过上述的调用过程来实现。对于嵌套函数调用,内层函数可以继续调用其他函数,而外层函数会等待内层函数执行完成后再继续执行。对于递归函数调用,函数可以直接或间接地调用自身,直到满足某个终止条件为止。

下面是一个简单的递归函数调用的例子:

factorial:
    # 基本情况: if n <= 1, return 1
    li   t0, 1
    ble  a0, t0, base_case
    
    # 递归情况
    addi sp, sp, -8         # 分配栈空间 (16字节对齐),从高到低
    sw   ra, 4(sp)          # 保存返回地址
    sw   s0, 0(sp)          # 保存 s0 (调用者保存),此处为上一层的参数a0
    # 每一次递归深入,保存当时的参数a0和ra,共8字节
    
    mv   s0, a0             # s0 = n,将参数保存到
    addi a0, a0, -1         # a0 = n-1
    jal  factorial          # 递归调用 factorial(n-1)
    
    # 此时 a0 = (n-1)!
    mul  a0, s0, a0         # a0 = n * (n-1)!
    
    # 恢复现场
    lw   s0, 0(sp) # 取出栈指针指向的数,存入s0,提供给上一层使用
    lw   ra, 4(sp)
    addi sp, sp, 8
    jr   ra

base_case:
    li   a0, 1              # return 1,将结果保存到a0
    jr   ra

我们来看一下指令

addi a0, x0, 5
jal ra, factorial

li a7, 1
ecall

指令组成

RISC-V 指令采用固定长度(32位)的编码格式,根据指令功能的不同,分为以下六种基本格式:

指令格式

格式名称用途结构
R-type寄存器型算术逻辑运算funct7 | rs2 | rs1 | funct3 | rd | opcode
I-type立即数型立即数运算、加载imm[11:0] | rs1 | funct3 | rd | opcode
S-type存储型存储指令imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode
B-type分支型条件分支imm[12,10:5] | rs2 | rs1 | funct3 | imm[4:1,11] | opcode
U-type上置立即数型加载高位立即数imm[31:12] | rd | opcode
J-type跳转型无条件跳转imm[20,10:1,11,19:12] | rd | opcode

各字段含义

  • opcode (7位): 操作码,决定指令类型
  • rd (5位): 目标寄存器编号
  • rs1/rs2 (5位): 源寄存器编号
  • funct3/funct7 (3/7位): 功能码,细化操作类型
  • imm: 立即数,不同格式中位数不同

指令分类

1. 算术运算指令

指令格式功能说明
ADDRrd = rs1 + rs2加法
SUBRrd = rs1 - rs2减法
ADDIIrd = rs1 + imm加立即数
LUIUrd = imm << 12加载高位立即数
AUIPCUrd = PC + (imm << 12)加PC到高位立即数

2. 逻辑运算指令

指令格式功能说明
AND/OR/XORR位与/或/异或逻辑运算
ANDI/ORI/XORII与/或/异或立即数立即数逻辑运算
SLL/SRL/SRAR逻辑/算术移位移位操作

3. 比较指令

指令格式功能说明
SLT/SLTURrd = (rs1 < rs2) ? 1 : 0小于置位(有/无符号)
SLTI/SLTIUIrd = (rs1 < imm) ? 1 : 0小于立即数置位

4. 分支指令

指令格式功能说明
BEQ/BNEB等于/不等于分支条件跳转
BLT/BLTUB小于分支(有/无符号)条件跳转
BGE/BGEUB大于等于分支(有/无符号)条件跳转
JALJrd = PC+4; PC += imm跳转并链接
JALRIrd = PC+4; PC = rs1 + imm寄存器跳转并链接

5. 加载存储指令

指令格式功能说明
LB/LH/LW/LDIrd = M[rs1+imm]加载字节/半字/字/双字
LBU/LHU/LWUI无符号加载零扩展加载
SB/SH/SW/SDSM[rs1+imm] = rs2存储字节/半字/字/双字

寻址方式

寻址方式是指确定操作数所在位置的方法。RISC-V 支持以下几种主要寻址方式:

1. 寄存器寻址 (Register Addressing)

原理:操作数直接存放在寄存器中,指令中给出寄存器编号。

格式:操作码  rd, rs1, rs2
示例:ADD x5, x6, x7    # x5 = x6 + x7

特点

  • 无需内存访问,速度最快
  • 所有 R-type 指令采用此方式
  • 寄存器数量有限(32个)

适用指令:ADD, SUB, AND, OR, XOR, SLL, SRL, SRA 等

2. 立即数寻址 (Immediate Addressing)

原理:操作数直接包含在指令中,作为指令的一部分。

格式:操作码  rd, rs1, imm
示例:ADDI x5, x6, 10   # x5 = x6 + 10

特点

  • 无需额外的内存访问
  • 立即数范围受限(I-type: 12位有符号数,范围 -2048~2047)
  • 常用于常数运算、小数值操作

适用指令:ADDI, ANDI, ORI, XORI, SLTI, SLLI, SRLI, SRAI 等

大范围立即数加载

# 加载 32位立即数 0x12345678 到 x5
lui  x5, 0x12345      # x5 = 0x12345000
addi x5, x5, 0x678    # x5 = 0x12345678

# 或使用伪指令
li   x5, 0x12345678   # 汇编器自动展开为上述指令
3. Base寻址 / 位移寻址 (Base/Displacement Addressing)

原理:操作数的有效地址 = 基址寄存器值 + 偏移量(立即数)

格式:操作码  rd, offset(base)
示例:LW x5, 8(x6)     # x5 = M[x6 + 8]
     SW x5, 4(x7)     # M[x7 + 4] = x5

地址计算

Effective Address=Base Register+SignExtended(offset)\text{Effective Address} = \text{Base Register} + \text{SignExtended}(\text{offset})

特点

  • 支持访问结构体、数组元素、栈帧中的变量
  • 偏移量为12位有符号数(-2048~2047)
  • 基址寄存器内容不变

典型应用

# 访问结构体成员(假设基址在 x10)
lw x5, 0(x10)    # 成员 offset 0
lw x6, 4(x10)    # 成员 offset 4
lh x7, 8(x10)    # 成员 offset 8

# 栈帧访问
sw ra, 4(sp)     # 保存返回地址到栈
lw ra, 4(sp)     # 恢复返回地址
4. PC相对寻址 (PC-Relative Addressing)

原理:目标地址 = 当前 PC 值 + 偏移量(指令编码中的立即数)

格式:操作码  rs1, rs2, label
      操作码  rd, label

示例:BEQ x5, x6, loop   # if(x5==x6) PC = PC + offset
     JAL x1, function   # x1 = PC+4; PC = PC + offset

地址计算

Target Address=PC+SignExtended(offset×2)\text{Target Address} = \text{PC} + \text{SignExtended}(\text{offset} \times 2)

特点

  • 偏移量是字(2字节)对齐的,因此指令编码中省略最低位(总是0)
  • B-type:13位有符号偏移(实际表示范围 ±4KB)
  • J-type:21位有符号偏移(实际表示范围 ±1MB)
  • 支持位置无关代码(PIC),代码可以在内存中任意位置执行

跳转范围

指令类型偏移位数实际范围用途
B-type13位PC ± 4KB条件分支(循环、if语句)
J-type21位PC ± 1MB函数调用、长跳转

示例

    addi x5, x0, 0       # i = 0
    addi x6, x0, 10      # max = 10
loop:
    beq  x5, x6, exit    # 如果 i == 10,跳转到 exit
                         # 偏移量 = exit地址 - (loop地址 + 4)
    addi x5, x5, 1       # i++
    jal  x0, loop        # 无条件跳转回 loop
exit:
    # 继续执行...
5. 间接寻址 (Indirect Addressing)

原理:操作数地址存放在寄存器中。

格式:操作码  rd, offset(rs1)
示例:JALR x0, 0(x5)    # PC = x5 + 0 (跳转表、函数指针调用)
     JALR ra, 0(ra)    # 函数返回

特点

  • 可以跳转到任意32位地址
  • 常用于函数返回、虚函数调用、switch语句实现
  • JALR 将偏移量与寄存器值相加

典型应用

# 函数指针调用
# 假设函数地址在 x10
jalr ra, 0(x10)       # 调用函数指针指向的函数

# 返回指令(jr 是 jalr 的伪指令)
jr ra                 # 等价于 jalr x0, 0(ra)

# Switch 跳转表实现
la   x5, jump_table   # 加载跳转表基址
slli x6, x6, 2        # 索引 * 4(字对齐)
add  x5, x5, x6       # 计算目标地址
lw   x5, 0(x5)        # 加载目标地址
jalr x0, 0(x5)        # 跳转到目标

寻址方式对比总结

寻址方式有效地址计算操作数位置典型指令主要用途
寄存器寻址寄存器ADD, SUB算术逻辑运算
立即数寻址指令中ADDI, LUI常数操作
Base寻址EA = 基址 + 偏移内存LW, SW数组、结构体访问
PC相对寻址EA = PC + 偏移内存(代码段)BEQ, JAL分支、跳转
间接寻址EA = 寄存器值内存JALR函数返回、函数指针
指令的寻址范围

32位地址空间中的寻址能力

指令类型寻址范围说明
I-type (Load)±2KB相对于基址寄存器的偏移
S-type (Store)±2KB相对于基址寄存器的偏移
B-type (Branch)±4KBPC相对,13位有符号偏移
J-type (Jump)±1MBPC相对,21位有符号偏移
JALR全地址空间32位寄存器值 + 12位偏移

大范围访问策略

# 访问远距离数据(超出 ±2KB 范围)
lui  x5, 0x12345      # 加载高位地址
addi x5, x5, 0x678    # 加上低位偏移
lw   x6, 0(x5)        # 加载数据

# 远距离跳转(超出 ±1MB)
lui  x5, %hi(target)  # 加载目标地址高位
addi x5, x5, %lo(target)  # 加上低位
jalr ra, 0(x5)        # 跳转到目标

浮点数

IEEE 754浮点数规格

浮点数格式

IEEE 754 标准定义了浮点数的存储格式,由三部分组成:

字段位数(单精度)位数(双精度)说明
符号位 (S)110为正,1为负
指数位 (E)811移码表示,需减去偏置值
尾数位 (M)2352小数部分

单精度(32位)浮点数格式

31 | 30      23 | 22                    0
 S |   指数E   |        尾数M
 1位   8位           23位

双精度(64位)浮点数格式

63 | 62          52 | 51                                        0
 S |     指数E      |                  尾数M
 1位      11位                52位

真值计算公式Value=(1)S×1.M×2EbiasValue = (-1)^S \times 1.M \times 2^{E - bias}

  • 单精度偏置值bias = 127
  • 双精度偏置值bias = 1023
规格化浮点数

规格化条件:指数位不全为0且不全为1。

隐含最高位:规格化浮点数默认尾数的最高位为1(即 1.M),该位不存储,称为隐含位隐藏位。因此单精度实际有效位数为24位(1位隐含 + 23位存储)。

非规格化数:当指数位全为0时,表示非规格化数: Value=(1)S×0.M×21biasValue = (-1)^S \times 0.M \times 2^{1 - bias}

非规格化数用于表示非常接近0的数值,以及 ±0(尾数也为0时)。

特殊值的指数编码

指数位尾数位含义
全0全0±0
全0非0非规格化数
全1全0±∞(无穷大)
全1非0NaN(非数)
浮点数的数据段范围

单精度(32位)范围

类型最小正值最大正值说明
规格化数1.0×21261.18×10381.0 \times 2^{-126} \approx 1.18 \times 10^{-38}(2223)×21273.40×1038(2 - 2^{-23}) \times 2^{127} \approx 3.40 \times 10^{38}正常表示范围
非规格化数223×21261.40×10452^{-23} \times 2^{-126} \approx 1.40 \times 10^{-45}(1223)×2126(1 - 2^{-23}) \times 2^{-126}接近0的子范围

双精度(64位)范围

类型最小正值最大正值
规格化数210222.23×103082^{-1022} \approx 2.23 \times 10^{-308}(2252)×210231.80×10308(2 - 2^{-52}) \times 2^{1023} \approx 1.80 \times 10^{308}

典型数值的编码示例

数值符号位指数位(含偏置)尾数位
+1.0001111111 (127)全0
-1.0101111111 (127)全0
+0.00全0全0
+∞0全1全0
NaN0全1非0

浮点数加法

浮点数加法需要经过多个步骤,确保结果正确且规格化:

步骤1:对阶(Alignment)

  • 比较两个操作数的指数
  • 将指数较小的数的尾数右移,使其指数与较大的对齐
  • 右移时低位丢弃,可能需要舍入

步骤2:尾数相加

  • 将对齐后的尾数进行加法运算
  • 包含隐含位参与运算(实际为 1.M

步骤3:规格化(Normalization)

  • 若结果溢出(尾数 ≥ 2),则右规:尾数右移1位,指数加1
  • 若结果最高位为0,则左规:尾数左移,指数减1,直到最高位为1

步骤4:舍入(Rounding)

  • 根据舍入模式处理超出精度的位
  • 常见模式:向偶数舍入(默认)、向零舍入、向正无穷舍入、向负无穷舍入

步骤5:判断溢出

  • 指数超出表示范围则为溢出
  • 指数上溢:结果太大,产生 ±∞
  • 指数下溢:结果太小,产生0或非规格化数

示例:计算 1.0 + 0.5

1.0  = 1.000...0 × 2^0
0.5  = 1.000...0 × 2^-1

对阶:0.5 的尾数右移1位
     0.100...0 × 2^0

相加:1.000...0 + 0.100...0 = 1.100...0 × 2^0

结果:1.5

浮点数乘除法

浮点数乘法

(M1×2E1)×(M2×2E2)=(M1×M2)×2E1+E2(M_1 \times 2^{E_1}) \times (M_2 \times 2^{E_2}) = (M_1 \times M_2) \times 2^{E_1 + E_2}

运算步骤:

  1. 指数相加Eresult=E1+E2biasE_{result} = E_1 + E_2 - bias(减去一个偏置值避免重复计算)
  2. 尾数相乘Mresult=M1×M2M_{result} = M_1 \times M_2
  3. 规格化:调整尾数和指数
  4. 舍入:处理超出精度的位
  5. 确定符号:同号得正,异号得负

浮点数除法

M1×2E1M2×2E2=M1M2×2E1E2\frac{M_1 \times 2^{E_1}}{M_2 \times 2^{E_2}} = \frac{M_1}{M_2} \times 2^{E_1 - E_2}

运算步骤:

  1. 指数相减Eresult=E1E2+biasE_{result} = E_1 - E_2 + bias
  2. 尾数相除Mresult=M1÷M2M_{result} = M_1 \div M_2
  3. 规格化与舍入
  4. 确定符号

特殊处理

  • 除数为0 → 结果为 ±∞NaN
  • 被除数为 ±∞ 且除数为 ±∞NaN
  • 0 / 0NaN

乘法器实现:现代 CPU 使用 Booth 算法或阵列乘法器加速尾数乘法,指数运算通过加法器完成。

数据通路设计 (Data Path)

数据通路是 CPU 中执行数据操作的路径,包含以下关键组件:

基本组件

1. 程序计数器 (PC)

  • 存储下一条要执行指令的地址
  • 每周期自动 +4(字节寻址,32位指令)
  • 分支/跳转时加载新地址

2. 指令存储器 (Instruction Memory)

  • 根据 PC 地址读取指令
  • 只读,通常在取指阶段访问

3. 寄存器堆 (Register File)

  • 32个通用寄存器(x0-x31)
  • 2个读端口、1个写端口
  • x0 硬连线为0

4. 算术逻辑单元 (ALU)

  • 执行算术运算(加、减)
  • 执行逻辑运算(与、或、异或)
  • 执行比较运算(SLT)
  • 计算内存地址

5. 数据存储器 (Data Memory)

  • 存储程序数据
  • 读写操作在访存阶段进行

6. 控制单元 (Control Unit)

  • 解析指令 opcode
  • 生成控制信号

单周期数据通路

单周期设计中,每条指令在一个时钟周期内完成:

指令执行阶段:
┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│  取指    │ -> │  译码   │ -> │  执行   │ -> │  访存   │ -> │  写回   │
│ (Fetch) │    │ (Decode)│    │(Execute)│    │(Memory) │    │(WriteBack)│
└─────────┘    └─────────┘    └─────────┘    └─────────┘    └─────────┘
   PC+4          读寄存器       ALU运算        读写内存        写寄存器
   读指令        立即数扩展     分支判断

关键路径:取指 -> 寄存器读 -> ALU -> 内存访问 -> 寄存器写

缺点:时钟周期由最长指令(Load)决定,效率低下

多周期数据通路

将指令执行分为多个时钟周期,资源共享:

周期操作活跃组件
1取指PC, Inst Memory
2译码/读寄存器Register File
3执行/地址计算ALU
4访存/分支完成Data Memory
5写回Register File

优点

  • 功能部件复用(节省硬件)
  • 不同指令执行周期数不同
  • 更短的时钟周期

流水线 (Pipeline)

流水线通过重叠执行多条指令来提高吞吐量。

经典五级流水线

时间 ->
指令1: [IF][ID][EX][MEM][WB]
指令2:     [IF][ID][EX][MEM][WB]
指令3:         [IF][ID][EX][MEM][WB]
指令4:             [IF][ID][EX][MEM][WB]
阶段名称功能
IF取指 (Instruction Fetch)从内存读取指令,PC+4
ID译码 (Instruction Decode)解析指令,读取寄存器
EX执行 (Execute)ALU运算,计算地址
MEM访存 (Memory Access)读写数据内存
WB写回 (Write Back)结果写回寄存器

流水线冒险 (Hazards)

1. 结构冒险 (Structural Hazard)

  • 原因:硬件资源冲突
  • 示例:单端口内存同时需要取指和访存
  • 解决
    • 硬件:增加端口(哈佛结构)
    • 软件:插入气泡(Bubble)

2. 数据冒险 (Data Hazard)

  • 原因:指令间数据依赖
add x1, x2, x3   # I1
sub x4, x1, x5   # I2 需要 I1 的结果
and x6, x1, x7   # I3 需要 I1 的结果
or  x8, x1, x9   # I4 需要 I1 的结果

类型

  • RAW(写后读):I2 依赖 I1 的结果
  • WAR(读后写):通常不会在流水线中发生
  • WAW(写后写):不会在五级流水线中发生

解决方法

  • 转发/旁路 (Forwarding/Bypassing)

    EX阶段结果直接传递给下一条指令的EX阶段
    [IF][ID][EX]-->[MEM][WB]
         [IF][ID]-->[EX][MEM][WB]
                 ^^
                 转发路径
  • 加载-使用冒险 (Load-Use Hazard)

    lw  x1, 0(x2)    # I1
    add x3, x1, x4   # I2 需要 I1 的加载结果(无法转发)

    解决:插入1个气泡,或使用编译器调度

3. 控制冒险 (Control Hazard)

  • 原因:分支指令改变PC,但下一条指令已在流水线中
beq x1, x2, label  # 分支指令
add x3, x4, x5     # 无论是否分支,此指令已进入流水线

解决方法

  • 停顿 (Stall):等待分支结果(性能损失大)
  • 预测不跳转 (Predict Not Taken):假设分支不成立
  • 延迟分支 (Delayed Branch):编译器调度安全指令填充延迟槽
  • 分支预测 (Branch Prediction):硬件预测分支方向

分支预测 (Branch Prediction)

分支预测用于减少控制冒险带来的性能损失。

静态预测

策略描述准确率
总是不跳转假设分支总是不成立~50%
总是跳转假设分支总是成立~60%
向后跳转预测为真循环中常用(loop)~70%
编译器提示指令中附加预测提示可变

动态预测

1. 一位分支预测器

状态机:
     0 (Not Taken)
     ^      |
     |      v
     1 (Taken)

预测与实际结果一致:保持状态
预测与实际结果不一致:翻转状态

缺点:循环边界预测错误两次(进入和退出)

2. 两位分支预测器(饱和计数器)

状态机:
00: 强不跳转 (Strongly Not Taken)
01: 弱不跳转 (Weakly Not Taken)
10: 弱跳转   (Weakly Taken)
11: 强跳转   (Strongly Taken)

预测错误时移动一位,正确时继续强化

优点:对偶尔不成立的循环表现更好

3. 分支历史表 (BHT - Branch History Table)

PC低位索引 -> BHT -> 2位饱和计数器

BHT结构:
┌─────────────────┬──────────────────┐
│  分支指令PC低位  │  2位饱和计数器    │
├─────────────────┼──────────────────┤
│     ...         │      ...         │
└─────────────────┴──────────────────┘

4. 两级自适应预测器 (PAg/PAp/GAp)

  • 全局历史寄存器 (GHR):记录最近N次分支结果
  • 模式历史表 (PHT):根据历史模式选择预测器
GHR: 1010 (最近4次分支:跳、不跳、跳、不跳)

     v
PHT索引 -> 选择对应的2位计数器

分支目标缓冲 (BTB - Branch Target Buffer)

缓存分支指令的目标地址,避免计算延迟:

BTB结构:
┌──────────────┬──────────────┬──────────────┐
│ 分支指令PC    │ 目标地址      │ 预测信息      │
├──────────────┼──────────────┼──────────────┤
│  0x1000      │  0x2000      │  11 (强跳转)  │
│  0x1050      │  0x1800      │  01 (弱不跳)  │
└──────────────┴──────────────┴──────────────┘

Cache (高速缓存)

Cache 利用程序访问的局部性原理,在 CPU 和主存之间提供快速数据访问。

局部性原理

时间局部性:最近访问的数据很可能再次被访问

循环中的计数器变量

空间局部性:访问某个地址后,其邻近地址很可能被访问

顺序访问数组元素

Cache 基本结构

┌─────────────────────────────────────────┐
│              Cache 结构                  │
├─────────────┬─────────────┬─────────────┤
│    Index    │    Tag      │    Data     │
│  (索引位)   │  (标记位)   │  (数据块)   │
└─────────────┴─────────────┴─────────────┘

地址划分

32位物理地址:
┌─────────────────┬─────────────┬─────────────┐
│      Tag        │    Index    │   Offset    │
│    (标记位)      │   (索引位)   │  (块内偏移)  │
└─────────────────┴─────────────┴─────────────┘

直接映射 Cache (Direct Mapped)

每个内存块只能映射到唯一的 Cache 行:

Cache 行号 = (内存块地址) mod (Cache 行数)

示例:4行 Cache,块大小16字节
地址0x0000 -> Cache 行0
地址0x0010 -> Cache 行1
地址0x0020 -> Cache 行2
地址0x0030 -> Cache 行3
地址0x0040 -> Cache 行0 (冲突!)

优点:实现简单,查找快 缺点:容易产生冲突失效(Conflict Miss)

全相联 Cache (Fully Associative)

内存块可以映射到任意 Cache 行:

查找:比较所有行的 Tag

优点:无冲突失效 缺点:硬件复杂度高(需要并行比较器),不适合大容量

组相联 Cache (Set Associative)

Cache 分成若干组,每组包含多行:

2路组相联示例:
┌─────────────────────────────────────┐
│  组0  │  行0-0  │  行0-1  │         │
│  组1  │  行1-0  │  行1-1  │         │
│  组2  │  行2-0  │  行2-1  │         │
│  组3  │  行3-0  │  行3-1  │         │
└─────────────────────────────────────┘

组号 = (内存块地址) mod (组数)
组内任意行可存放该块

优点:平衡了冲突率和硬件复杂度

Cache 失效类型

类型原因解决策略
强制失效 (Compulsory)首次访问该块预取 (Prefetching)
容量失效 (Capacity)Cache 容量不足增大 Cache
冲突失效 (Conflict)映射冲突增加相联度

替换策略

策略描述特点
LRU (Least Recently Used)替换最久未使用的行实现复杂,命中率高
FIFO (First In First Out)替换最早进入的行实现简单
Random随机选择实现最简单
LFU (Least Frequently Used)替换访问次数最少的需要计数器

写策略

写命中 (Write Hit)

策略操作特点
写直达 (Write Through)同时写 Cache 和内存数据一致,速度慢
写回 (Write Back)只写 Cache,替换时写内存速度快,需要脏位

写不命中 (Write Miss)

策略操作
写分配 (Write Allocate)加载块到 Cache,再写入
非写分配 (No Write Allocate)直接写内存,不加载到 Cache

常见组合

  • 写直达 + 非写分配
  • 写回 + 写分配

多级 Cache

CPU -> L1 Cache -> L2 Cache -> L3 Cache -> 主存
      (32-64KB)   (256KB-1MB)  (4-64MB)
      1-3周期     8-15周期     20-50周期    200+周期
级别特点设计目标
L1分指令和数据 (Harvard)最小延迟
L2统一缓存平衡延迟和命中率
L3多核共享高命中率

包含策略

  • Inclusive:L1 内容一定在 L2 中
  • Exclusive:L1、L2 内容互不重复
  • Non-inclusive:介于两者之间

BUS (总线)

总线是连接 CPU、内存、I/O 设备的共享通信通路。

总线类型

总线功能示例
数据总线传输数据64位宽
地址总线传输地址32/64位宽
控制总线传输控制信号读写信号、中断等

总线仲裁

集中式仲裁

  • 链式查询:优先级固定,结构简单
  • 计数器定时查询:优先级可编程
  • 独立请求:每个设备独立请求,响应快

分布式仲裁

  • 设备自行竞争总线使用权

总线事务

典型读事务:
1. 主设备发送地址和读命令
2. 从设备准备数据
3. 从设备发送数据
4. 主设备接收数据

典型写事务:
1. 主设备发送地址和写命令
2. 主设备发送数据
3. 从设备接收并存储数据

计算机的性能指标

IC与Clock

指令数 (IC - Instruction Count):程序执行的总指令条数

时钟周期 (Clock Cycle):CPU 操作的最小时间单位

时钟频率 (Clock Rate)

f=1Tf = \frac{1}{T}

其中 TT 为时钟周期,ff 为时钟频率(单位:Hz)

CPI

CPI (Cycles Per Instruction):每条指令的平均时钟周期数

CPI=总时钟周期数指令数=i(CPIi×指令比例i)\text{CPI} = \frac{\text{总时钟周期数}}{\text{指令数}} = \sum_{i}(\text{CPI}_i \times \text{指令比例}_i)

不同指令的 CPI

指令类型典型 CPI说明
ALU 指令1单周期完成
Load2-5需要访存
Store1-2无需写回寄存器
分支1-3取决于预测成功率
乘法/除法3-32复杂运算

CPU Time

CPU 执行时间

CPU Time=指令数×CPI×时钟周期\text{CPU Time} = \text{指令数} \times \text{CPI} \times \text{时钟周期}

或:

CPU Time=指令数×CPI时钟频率\text{CPU Time} = \frac{\text{指令数} \times \text{CPI}}{\text{时钟频率}}

性能优化方向

方向方法影响
减少指令数更好的算法/编译器软件优化
降低 CPI流水线、超标量、更好的预测硬件架构
提高时钟频率更好的工艺、更短的流水线物理实现

Amdahl 定律

加速比=1(1f)+fs\text{加速比} = \frac{1}{(1 - f) + \frac{f}{s}}

其中:

  • ff:可优化部分在原执行时间中的比例
  • ss:该部分的加速倍数

关键洞察:优化常见情况(高频部分)收益更大

Comments