本门课程的主要内容
- 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中,调用过程通常涉及以下步骤:
- 将函数参数传递到指定的寄存器(如a0-a7)。
- 使用jal指令跳转到函数的入口地址,并将返回地址(一般是pc+4)存储到ra寄存器中。在jal命令中,我们只需要指定函数的入口地址,编译器会自动计算返回地址并存储到ra寄存器中。
- 在函数内部执行所需的操作,可能会使用s0-s11寄存器来保存需要保留的值。
- 使用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. 算术运算指令
| 指令 | 格式 | 功能 | 说明 |
|---|---|---|---|
ADD | R | rd = rs1 + rs2 | 加法 |
SUB | R | rd = rs1 - rs2 | 减法 |
ADDI | I | rd = rs1 + imm | 加立即数 |
LUI | U | rd = imm << 12 | 加载高位立即数 |
AUIPC | U | rd = PC + (imm << 12) | 加PC到高位立即数 |
2. 逻辑运算指令
| 指令 | 格式 | 功能 | 说明 |
|---|---|---|---|
AND/OR/XOR | R | 位与/或/异或 | 逻辑运算 |
ANDI/ORI/XORI | I | 与/或/异或立即数 | 立即数逻辑运算 |
SLL/SRL/SRA | R | 逻辑/算术移位 | 移位操作 |
3. 比较指令
| 指令 | 格式 | 功能 | 说明 |
|---|---|---|---|
SLT/SLTU | R | rd = (rs1 < rs2) ? 1 : 0 | 小于置位(有/无符号) |
SLTI/SLTIU | I | rd = (rs1 < imm) ? 1 : 0 | 小于立即数置位 |
4. 分支指令
| 指令 | 格式 | 功能 | 说明 |
|---|---|---|---|
BEQ/BNE | B | 等于/不等于分支 | 条件跳转 |
BLT/BLTU | B | 小于分支(有/无符号) | 条件跳转 |
BGE/BGEU | B | 大于等于分支(有/无符号) | 条件跳转 |
JAL | J | rd = PC+4; PC += imm | 跳转并链接 |
JALR | I | rd = PC+4; PC = rs1 + imm | 寄存器跳转并链接 |
5. 加载存储指令
| 指令 | 格式 | 功能 | 说明 |
|---|---|---|---|
LB/LH/LW/LD | I | rd = M[rs1+imm] | 加载字节/半字/字/双字 |
LBU/LHU/LWU | I | 无符号加载 | 零扩展加载 |
SB/SH/SW/SD | S | M[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
地址计算:
特点:
- 支持访问结构体、数组元素、栈帧中的变量
- 偏移量为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
地址计算:
特点:
- 偏移量是字(2字节)对齐的,因此指令编码中省略最低位(总是0)
- B-type:13位有符号偏移(实际表示范围 ±4KB)
- J-type:21位有符号偏移(实际表示范围 ±1MB)
- 支持位置无关代码(PIC),代码可以在内存中任意位置执行
跳转范围:
| 指令类型 | 偏移位数 | 实际范围 | 用途 |
|---|---|---|---|
| B-type | 13位 | PC ± 4KB | 条件分支(循环、if语句) |
| J-type | 21位 | 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) | ±4KB | PC相对,13位有符号偏移 |
| J-type (Jump) | ±1MB | PC相对,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) | 1 | 1 | 0为正,1为负 |
| 指数位 (E) | 8 | 11 | 移码表示,需减去偏置值 |
| 尾数位 (M) | 23 | 52 | 小数部分 |
单精度(32位)浮点数格式:
31 | 30 23 | 22 0
S | 指数E | 尾数M
1位 8位 23位
双精度(64位)浮点数格式:
63 | 62 52 | 51 0
S | 指数E | 尾数M
1位 11位 52位
真值计算公式:
- 单精度偏置值:
bias = 127 - 双精度偏置值:
bias = 1023
规格化浮点数
规格化条件:指数位不全为0且不全为1。
隐含最高位:规格化浮点数默认尾数的最高位为1(即 1.M),该位不存储,称为隐含位或隐藏位。因此单精度实际有效位数为24位(1位隐含 + 23位存储)。
非规格化数:当指数位全为0时,表示非规格化数:
非规格化数用于表示非常接近0的数值,以及 ±0(尾数也为0时)。
特殊值的指数编码:
| 指数位 | 尾数位 | 含义 |
|---|---|---|
| 全0 | 全0 | ±0 |
| 全0 | 非0 | 非规格化数 |
| 全1 | 全0 | ±∞(无穷大) |
| 全1 | 非0 | NaN(非数) |
浮点数的数据段范围
单精度(32位)范围:
| 类型 | 最小正值 | 最大正值 | 说明 |
|---|---|---|---|
| 规格化数 | 正常表示范围 | ||
| 非规格化数 | 接近0的子范围 |
双精度(64位)范围:
| 类型 | 最小正值 | 最大正值 |
|---|---|---|
| 规格化数 |
典型数值的编码示例:
| 数值 | 符号位 | 指数位(含偏置) | 尾数位 |
|---|---|---|---|
+1.0 | 0 | 01111111 (127) | 全0 |
-1.0 | 1 | 01111111 (127) | 全0 |
+0.0 | 0 | 全0 | 全0 |
+∞ | 0 | 全1 | 全0 |
NaN | 0 | 全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
浮点数乘除法
浮点数乘法:
运算步骤:
- 指数相加:(减去一个偏置值避免重复计算)
- 尾数相乘:
- 规格化:调整尾数和指数
- 舍入:处理超出精度的位
- 确定符号:同号得正,异号得负
浮点数除法:
运算步骤:
- 指数相减:
- 尾数相除:
- 规格化与舍入
- 确定符号
特殊处理:
- 除数为0 → 结果为
±∞或NaN - 被除数为
±∞且除数为±∞→NaN 0 / 0→NaN
乘法器实现:现代 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):
其中 为时钟周期, 为时钟频率(单位:Hz)
CPI
CPI (Cycles Per Instruction):每条指令的平均时钟周期数
不同指令的 CPI:
| 指令类型 | 典型 CPI | 说明 |
|---|---|---|
| ALU 指令 | 1 | 单周期完成 |
| Load | 2-5 | 需要访存 |
| Store | 1-2 | 无需写回寄存器 |
| 分支 | 1-3 | 取决于预测成功率 |
| 乘法/除法 | 3-32 | 复杂运算 |
CPU Time
CPU 执行时间:
或:
性能优化方向:
| 方向 | 方法 | 影响 |
|---|---|---|
| 减少指令数 | 更好的算法/编译器 | 软件优化 |
| 降低 CPI | 流水线、超标量、更好的预测 | 硬件架构 |
| 提高时钟频率 | 更好的工艺、更短的流水线 | 物理实现 |
Amdahl 定律:
其中:
- :可优化部分在原执行时间中的比例
- :该部分的加速倍数
关键洞察:优化常见情况(高频部分)收益更大
Comments