- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言
- 1. 计算机体系结构概览
- 2. 指令集架构(ISA)
- 3. ALU 与数据通路
- 4. 流水线(Pipelining)
- 5. 存储器层次结构
- 6. 虚拟内存
- 7. 输入输出系统
- 8. 并行架构
- 9. GPU 架构
- 10. 最新架构趋势
- 11. 性能优化实战
- 12. 小测验
- 参考资料
引言
计算机体系结构(Computer Architecture)是理解硬件与软件边界的核心学科。对电子/计算机工程专业的学生而言,这是必须深入理解的领域,而对高性能软件开发者、系统程序员、芯片设计者来说,同样是必备知识。
本指南以 Patterson & Hennessy 的《计算机组成与设计》(Computer Organization and Design)与《计算机体系结构:量化研究方法》(Computer Architecture: A Quantitative Approach)为基础,从 ISA 设计一直系统地讲到现代 GPU 并行架构。
1. 计算机体系结构概览
冯·诺依曼架构(Von Neumann Architecture)
作为现代计算机基础的冯·诺依曼架构,由 John von Neumann 于 1945 年提出。其核心特征是将程序和数据存储在同一内存空间。
组成要素:
- CPU(中央处理器,Central Processing Unit):ALU + 控制单元 + 寄存器
- 内存:存储程序代码 + 数据
- 输入输出设备:键盘、显示器、磁盘等
- 总线:传输数据/地址/控制信号
冯·诺依曼瓶颈(Von Neumann Bottleneck):CPU 与内存之间的总线带宽限制了性能。现代缓存层次结构正是为了缓解这一问题而设计的。
哈佛架构(Harvard Architecture)
哈佛架构将指令内存与数据内存分离,从而允许同时访问。它被用于 DSP、微控制器(AVR、PIC),以及现代 CPU 的 L1 缓存(指令缓存/数据缓存分离)。
| 类别 | 冯·诺依曼 | 哈佛 |
|---|---|---|
| 内存 | 统一 | 分离 |
| 带宽 | 有限 | 较高 |
| 复杂度 | 较低 | 较高 |
| 应用场景 | 通用 CPU | DSP、微控制器 |
计算机抽象层次
计算机系统由多个抽象层次构成:
应用程序(Application)
↓
操作系统(Operating System)
↓
ISA(指令集架构,Instruction Set Architecture) ← 硬件/软件边界
↓
微架构(Microarchitecture)
↓
逻辑门(Logic Gates)
↓
晶体管(Transistors)
ISA 是硬件与软件之间的契约(contract)。只要实现的是同一 ISA,无论采用何种微架构,同一份软件都能运行。
性能度量指标
CPU 执行时间公式:
CPU Time = Instruction Count × CPI × Clock Cycle Time
= Instruction Count × CPI / Clock Rate
- CPI(Cycles Per Instruction,每条指令的时钟周期数):执行 1 条指令所需的平均时钟周期数
- Clock Rate(时钟频率):单位为 Hz,即每秒的时钟周期数(例:3.5 GHz)
- MIPS(Millions of Instructions Per Second):每秒执行的指令数(以百万为单位)
- FLOPS(Floating Point Operations Per Second):浮点运算性能
Amdahl 定律(性能提升的极限):
Speedup = 1 / ((1 - f) + f/s)
其中 f 是可并行化的比例,s 是并行化部分的加速倍数。如果全部工作中只有 40% 可以并行化,那么无论使用多少核心,最大加速比的上限都是 1.67 倍。
2. 指令集架构(ISA)
RISC 与 CISC
RISC(精简指令集计算机,Reduced Instruction Set Computer):
- 简单、定长指令
- 以寄存器为中心的运算(Load/Store 架构)
- 有利于硬件流水线化
- 代表:ARM、RISC-V、MIPS、PowerPC
CISC(复杂指令集计算机,Complex Instruction Set Computer):
- 复杂、变长指令
- 可直接对内存进行运算
- 代码密度高(用更少的指令完成复杂工作)
- 代表:x86-64、VAX
现代 x86-64 处理器在内部会将指令转换为 RISC 风格的微操作(micro-ops)来执行,因此二者的实际边界已经变得模糊。
指令格式(以 RISC-V 为例)
RISC-V 使用 6 种指令格式:
R-型(寄存器运算):
[funct7|rs2|rs1|funct3|rd|opcode]
7位 5位 5位 3位 5位 7位
I-型(立即数/加载):
[imm[11:0]|rs1|funct3|rd|opcode]
12位 5位 3位 5位 7位
S-型(存储):
[imm[11:5]|rs2|rs1|funct3|imm[4:0]|opcode]
B-型(分支):
[imm[12|10:5]|rs2|rs1|funct3|imm[4:1|11]|opcode]
U-型(高位立即数):
[imm[31:12]|rd|opcode]
J-型(JAL):
[imm[20|10:1|11|19:12]|rd|opcode]
RISC-V ISA 示例代码
RISC-V 是由 UC Berkeley 开发的开源 ISA,在教育和产业领域都在快速发展。
# RISC-V 汇编示例
# 基本算术运算
add a0, a0, a1 # a0 = a0 + a1
sub t0, t1, t2 # t0 = t1 - t2
addi t0, t0, 10 # t0 = t0 + 10(立即数)
mul a0, a1, a2 # a0 = a1 * a2
# 内存访问(Load/Store)
lw t0, 0(a0) # t0 = Memory[a0 + 0] (字加载)
lh t1, 2(a0) # t1 = Memory[a0 + 2] (半字加载)
lb t2, 1(a0) # t2 = Memory[a0 + 1] (字节加载)
sw t0, 4(a0) # Memory[a0 + 4] = t0 (字存储)
# 逻辑运算
and t0, t1, t2 # t0 = t1 & t2
or t0, t1, t2 # t0 = t1 | t2
xor t0, t1, t2 # t0 = t1 ^ t2
sll t0, t1, t2 # t0 = t1 << t2 (逻辑左移)
srl t0, t1, t2 # t0 = t1 >> t2 (逻辑右移)
# 分支与跳转
beq t0, t1, label # if t0 == t1, jump to label
bne t0, t1, label # if t0 != t1, jump to label
blt t0, t1, label # if t0 < t1, jump to label
bge t0, t1, label # if t0 >= t1, jump to label
jal ra, func # ra = PC+4; jump to func
jalr ra, 0(t0) # ra = PC+4; jump to t0+0
# RISC-V 寄存器约定
# x0 (zero): 恒为 0
# x1 (ra): 返回地址(Return Address)
# x2 (sp): 栈指针(Stack Pointer)
# x10-x17 (a0-a7): 函数参数/返回值
# x5-x7, x28-x31 (t0-t6): 临时寄存器
# x8-x9, x18-x27 (s0-s11): 保存寄存器
将 C 函数转换为 RISC-V 汇编
// C 代码
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
# RISC-V 汇编(递归阶乘)
factorial:
addi sp, sp, -16 # 分配栈帧
sw ra, 12(sp) # 保存返回地址
sw a0, 8(sp) # 保存 n
addi t0, zero, 1
bgt a0, t0, recurse # n > 1 则递归
addi a0, zero, 1 # return 1
j done
recurse:
addi a0, a0, -1 # n - 1
jal ra, factorial # 递归调用
lw t0, 8(sp) # 恢复 n
mul a0, t0, a0 # n * factorial(n-1)
done:
lw ra, 12(sp) # 恢复返回地址
addi sp, sp, 16 # 释放栈帧
jalr zero, 0(ra) # 返回
3. ALU 与数据通路
ALU 设计
ALU(算术逻辑单元,Arithmetic Logic Unit)是 CPU 的核心运算部件。基本运算:
ADD: 结果 = A + B
SUB: 结果 = A + (~B + 1) = A - B (2 的补码)
AND: 结果 = A AND B
OR: 结果 = A OR B
XOR: 结果 = A XOR B
SLT: 结果 = (A < B) ? 1 : 0 (Set Less Than)
SLL: 结果 = A << B (移位)
行波进位加法器(Ripple Carry Adder):
FA0: S0 = A0 XOR B0 XOR Cin; C1 = (A0 AND B0) OR ...
FA1: S1 = A1 XOR B1 XOR C1; C2 = ...
...
FAn: Sn = An XOR Bn XOR Cn; Cout = ...
N 位行波进位加法器的延迟为 O(N)。64 位加法会产生 64 级门延迟。
先行进位加法器(Carry Lookahead Adder,CLA):
定义生成(Generate)和传播(Propagate):
Gi = Ai AND Bi (进位生成)
Pi = Ai XOR Bi (进位传播)
Ci+1 = Gi OR (Pi AND Ci)
以 4 位为一组并行计算进位,可将延迟降到 O(log N)。
单周期数据通路
在单周期(Single-cycle)实现中,每条指令都在一个时钟周期内执行完成。
指令流程(R-型 ADD):
1. IF: PC → 指令内存 → 取指令
2. ID: 从寄存器堆读取 rs1、rs2 + 生成控制信号
3. EX: ALU 计算 rs1 + rs2
4. MEM: (R-型指令不访问内存)
5. WB: 将 ALU 结果写入 rd
单周期的问题:最慢的指令(例如内存访问)决定了时钟周期长度 → 快指令也不得不迁就慢时钟。
4. 流水线(Pipelining)
流水线技术就像同时使用洗衣机、烘干机、熨斗一样,把多条指令的不同阶段重叠执行。
5 级流水线
阶段 缩写 作用
------+-----+----------------------------------------
取指 IF 从内存中按 PC 取出指令
译码 ID 指令译码、读寄存器、生成控制信号
执行 EX 执行 ALU 运算
访存 MEM 读/写数据内存
写回 WB 将结果写入寄存器堆
时序图:
周期: 1 2 3 4 5 6 7 8
指令1: IF ID EX MEM WB
指令2: IF ID EX MEM WB
指令3: IF ID EX MEM WB
指令4: IF ID EX MEM WB
理想情况下,5 级流水线相比单周期能提供最多 5 倍的吞吐量提升。
流水线冒险(Pipeline Hazards)
1. 结构冒险(Structural Hazard)
当两条指令同时需要使用同一硬件资源时发生。
- 解决方法:资源分离(I-Cache 与 D-Cache 分离)、增加寄存器端口
2. 数据冒险(Data Hazard)
当前一条指令的结果尚未就绪,而下一条指令就需要用到该值时发生。
add t0, t1, t2 # 写 t0(WB:第 5 周期)
sub t3, t0, t4 # 读 t0(ID:第 3 周期)→ RAW 冒险!
种类:
- RAW(Read After Write):最常见。前一条指令还没写入,就被读取。
- WAR(Write After Read):发生在乱序执行(OoO)中。
- WAW(Write After Write):两条指令写同一个寄存器时。
解决方法:
- 前递(Forwarding/Bypassing):将 EX/MEM 阶段的结果直接传给下一个 EX 阶段作为输入。
- 停顿(Stall/Bubble):暂停流水线并插入 NOP(No Operation)。会造成性能下降。
- 代码重排:编译器在中间插入互不依赖的指令。
# 加载-使用冒险(1 个周期的停顿不可避免)
lw t0, 0(a0) # 从内存加载
add t1, t0, t2 # 立即使用 t0 → MEM→EX 前递不可行
# 解决方法:编译器插入独立指令
lw t0, 0(a0)
add t3, t4, t5 # 插入独立指令
add t1, t0, t2 # 此时 t0 已就绪
3. 控制冒险(Control Hazard)
由分支(branch)指令引起,导致下一条要执行的指令不确定时发生。
beq t0, t1, label # 是否分支在 EX 阶段才决定
# 与此同时 IF、ID 阶段取出的指令怎么办?
解决方法:
- 冲刷(Flush):丢弃错误取出的指令(2-3 周期的代价)
- 分支预测(Branch Prediction):
- 静态预测:总是不跳转(Not-Taken)或总是跳转(Taken)
- 动态预测:1 位/2 位预测器、BTB(Branch Target Buffer)
- 现代 CPU 的预测准确率在 95% 以上
- 延迟分支(Delayed Branch):在分支指令之后的槽位总是放置一条有用的指令(MIPS)
超标量流水线
现代 CPU 会并行运行多条流水线:
Intel Core: 6 路乱序执行(Out-of-Order Execution)
AMD Zen 4: 4 路译码 + OoO 执行
ARM Cortex-X4: 5 路译码
乱序执行(Out-of-Order Execution):
- 按顺序取指/译码指令
- 从已就绪的指令开始执行(Tomasulo 算法)
- 结果按顺序写回(使用 Reorder Buffer)
5. 存储器层次结构
存储器层次
寄存器(Register)
容量:~1KB | 速度:1 周期 | 成本:极高
L1 缓存(片上)
容量:32-64KB | 速度:4-5 周期 | 成本:高
L2 缓存(片上)
容量:256KB-1MB | 速度:12-15 周期 | 成本:中等
L3 缓存(片上,共享)
容量:8-64MB | 速度:30-40 周期 | 成本:低
DRAM(主存)
容量:8-256GB | 速度:200-300 周期 | 成本:极低
SSD/NVMe
容量:1-4TB | 速度:10,000+ 周期 | 成本:非常低
局部性原理(Principle of Locality)
- 时间局部性(Temporal Locality):最近访问过的数据很可能很快再次被访问。(循环变量、计数器)
- 空间局部性(Spatial Locality):所访问数据附近的数据也很可能很快被访问。(数组顺序访问)
// 空间局部性优化示例
// 不好的例子:按列优先访问(频繁缓存未命中)
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
sum += A[i][j]; // A[0][0], A[1][0], A[2][0]...(跨行跳跃)
// 好的例子:按行优先访问(缓存友好)
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
sum += A[i][j]; // A[0][0], A[0][1], A[0][2]...(连续内存)
缓存结构
直接映射(Direct-mapped)缓存:内存中的每个块只能存放在缓存中唯一的一个位置。
- 优点:实现简单、访问快
- 缺点:可能发生冲突未命中(Conflict Miss)
N 路组相联(N-way Set-associative)缓存:内存块可以存放在 N 个槽位中的任意一个。
- 实际 CPU 中最常用(L1:4 路,L2:8 路,L3:16 路)
全相联(Fully-associative)缓存:内存块可以存放在任意槽位。
- 优点:没有冲突未命中
- 缺点:查找开销大(用于 TLB)
缓存地址分解(32 位地址,4KB 缓存,64B 块,4 路):
[标签(20 位) | 索引(6 位) | 偏移(6 位)]
缓存替换策略
- LRU(Least Recently Used,最近最少使用):替换最久未被使用的块。性能最好,实现复杂。
- FIFO(First In First Out,先进先出):替换最先进入的块。实现简单。
- 随机(Random):随机替换。硬件实现简单,性能与 LRU 相近。
写策略
- 写直达(Write-through):同时更新缓存和内存。保证一致性,但消耗带宽。
- 写回(Write-back):只更新缓存,替换时才写回内存(使用 Dirty bit)。性能好,但一致性处理复杂。
6. 虚拟内存
概述
虚拟内存为每个进程提供独立的地址空间,从而实现保护、隔离,以及内存的超量分配(overcommit)。
虚拟地址(VA):进程使用的地址(0x0000 ~ 0xFFFFFFFF)
物理地址(PA):实际 DRAM 的地址
页(Page): 虚拟/物理内存的固定大小块(通常为 4KB)
页表转换
虚拟地址 [VPN | Page Offset]
↓
查找页表
↓
物理地址 [PFN | Page Offset]
64 位系统使用 4 级页表(x86-64):
[PML4(9位) | PDPT(9位) | PD(9位) | PT(9位) | 偏移(12位)]
TLB(Translation Lookaside Buffer,转址旁路缓存)
查找页表需要额外的内存访问。TLB 是缓存最近转换结果的全相联缓存。
TLB 命中: 虚拟地址 → 查找 TLB → 物理地址 (1-2 周期)
TLB 未命中:虚拟地址 → 页表遍历 → 物理地址 (100+ 周期)
TLB 大小:通常为 64-1024 项。命中率需维持在 99% 以上。
页面置换算法
当内存已满时,需要决定将哪个页换出到磁盘。
- 最优(Optimal):替换未来最长时间不会被使用的页(理论最优,实际不可实现)
- LRU:替换最久未被使用的页(性能好,实现代价高)
- Clock(FIFO + 二次机会):实际操作系统中常用的 LRU 近似算法
7. 输入输出系统
I/O 控制方式
轮询(Polling): CPU 定期检查设备状态。
- 优点:实现简单,延迟低
- 缺点:浪费 CPU 时间(忙等待,Busy-wait)
中断(Interrupt): 设备就绪后向 CPU 发送信号。
- 优点:CPU 可以执行其他工作
- 缺点:中断处理带来开销
DMA(Direct Memory Access,直接内存访问): DMA 控制器无需 CPU 介入即可直接在内存与设备之间传输数据。
- 大批量数据传输所必需(磁盘、网络、GPU)
- CPU 只需处理传输的开始/完成
总线架构
PCIe 5.0: x16 插槽 = 128 GB/s 双向
NVMe(PCIe):最高 7 GB/s 顺序读取(Gen4)
USB 4.0: 最高 40 Gbps
DDR5-6400: 最高 51.2 GB/s(每通道)
8. 并行架构
Flynn 分类法
| 分类 | 指令流 | 数据流 | 示例 |
|---|---|---|---|
| SISD | 单一 | 单一 | 单核 CPU |
| SIMD | 单一 | 多重 | GPU、AVX 向量运算 |
| MISD | 多重 | 单一 | 容错系统 |
| MIMD | 多重 | 多重 | 多核 CPU、集群 |
多核与缓存一致性
当多个核心的缓存中持有同一份数据时,必须维护一致性。
MESI 协议(Modified、Exclusive、Shared、Invalid):
Modified(M):只有该核心持有已修改的最新副本
Exclusive(E):只有该核心持有,且与内存一致
Shared(S): 多个核心持有只读副本
Invalid(I): 无效(其他核心已修改)
状态转换:
- 核心 A 对处于 S 状态的数据进行写入 → A 转为 M,其他核心转为 I(失效)
- 核心 B 读取处于 I 状态的数据 → 通过总线窥探(bus snooping)从 A 获取数据
OpenMP 并行编程
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
// 并行数组求和
int main() {
int n = 1000000;
long long sum = 0;
int *arr = malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
arr[i] = i + 1;
// 使用 reduction 实现安全的并行求和
#pragma omp parallel for reduction(+:sum) schedule(static)
for (int i = 0; i < n; i++) {
sum += arr[i];
}
printf("Sum = %lld\n", sum); // 500000500000
// 并行 for + 任务分配
#pragma omp parallel
{
int tid = omp_get_thread_num();
int nthreads = omp_get_num_threads();
printf("Thread %d of %d\n", tid, nthreads);
}
free(arr);
return 0;
}
// 矩阵乘法并行化(缓存友好 + OpenMP)
void matmul(float *A, float *B, float *C, int N) {
#pragma omp parallel for collapse(2) schedule(dynamic, 64)
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {
sum += A[i*N + k] * B[k*N + j];
}
C[i*N + j] = sum;
}
}
}
NUMA 架构
NUMA(Non-Uniform Memory Access,非统一内存访问)是每个 CPU 插槽都拥有本地内存的架构。
插槽 0(核心 0-15) ←→ 本地 DRAM(64GB) [~80ns]
↕ QPI/Infinity Fabric
插槽 1(核心 16-31) ←→ 本地 DRAM(64GB) [~80ns]
从插槽 0 访问插槽 1 的内存:~160ns(延迟 2 倍)
使用 numactl 可以将进程绑定到特定的 NUMA 节点。
9. GPU 架构
GPU 与 CPU 的设计哲学
| 项目 | CPU | GPU |
|---|---|---|
| 核心数量 | 数十个 | 数千到数万个 |
| 核心复杂度 | 非常复杂(OoO、分支预测) | 简单 |
| 缓存大小 | 大且复杂 | 小且简单 |
| 设计目标 | 最小化单线程延迟 | 最大化吞吐量(Throughput) |
| 用途 | 通用串行运算 | 大规模并行运算 |
SIMT 执行模型
SIMT(Single Instruction, Multiple Thread)是 GPU 的核心执行模型。
线程束(Warp):32 个线程为一组(NVIDIA)
→ 32 个线程同时执行同一条指令
→ 每个线程作用于不同的数据
线程束调度器:发生延迟(如内存访问)时立即切换到另一个线程束
→ 通过在数千个线程束之间快速切换来隐藏延迟(Latency Hiding)
NVIDIA GPU 层次结构
GPU
├── GPC(图形处理集群,Graphics Processing Cluster)x 8
│ └── SM(流多处理器,Streaming Multiprocessor)x 7-12
│ ├── CUDA Core x 128 (FP32 运算)
│ ├── Tensor Core x 4 (矩阵运算,AI)
│ ├── RT Core x 1 (光线追踪)
│ ├── Warp Scheduler x 4
│ ├── 寄存器堆(256KB)
│ └── 共享内存 / L1 缓存(128-256KB)
└── L2 缓存(共享,数十 MB)
NVIDIA H100(Hopper,2022):
- 132 个 SM,16,896 个 CUDA Core
- 528 个 Tensor Core(第 4 代,支持 FP8)
- 80GB HBM3 内存,3.35 TB/s 带宽
- 4 PetaFLOPS FP8 Tensor Core 性能
CUDA 编程基础
#include <cuda_runtime.h>
#include <stdio.h>
// GPU 内核:向量加法
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
// 计算线程的全局索引
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = 1 << 20; // 1M 元素
size_t size = n * sizeof(float);
// 主机(CPU)内存
float *h_a = (float*)malloc(size);
float *h_b = (float*)malloc(size);
float *h_c = (float*)malloc(size);
// 设备(GPU)内存
float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, size);
cudaMalloc(&d_b, size);
cudaMalloc(&d_c, size);
// 主机 → 设备 拷贝
cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
// 内核执行:每个块 256 个线程
int threadsPerBlock = 256;
int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);
// 设备 → 主机 拷贝
cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);
cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
free(h_a); free(h_b); free(h_c);
return 0;
}
GPU 内存优化
// 利用共享内存优化矩阵乘法
#define TILE_SIZE 16
__global__ void matmulShared(float *A, float *B, float *C, int N) {
__shared__ float tileA[TILE_SIZE][TILE_SIZE];
__shared__ float tileB[TILE_SIZE][TILE_SIZE];
int row = blockIdx.y * TILE_SIZE + threadIdx.y;
int col = blockIdx.x * TILE_SIZE + threadIdx.x;
float sum = 0.0f;
for (int t = 0; t < N / TILE_SIZE; t++) {
// 将 tile 加载到共享内存
tileA[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
tileB[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
__syncthreads(); // 等待所有线程加载完成
for (int k = 0; k < TILE_SIZE; k++)
sum += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
__syncthreads();
}
if (row < N && col < N)
C[row * N + col] = sum;
}
Tensor Core 与 AI 加速
Tensor Core 在硬件层面加速矩阵乘加(MMA:Matrix Multiply-Accumulate)运算。
普通 CUDA Core:每周期 1 个 FP32 MAC
Tensor Core(第 4 代):16x16x16 矩阵乘 = 每周期 4096 个 FP16 MAC
NVIDIA cuBLAS、cuDNN、TensorRT 会自动利用 Tensor Core。
10. 最新架构趋势
芯粒(Chiplet)设计
不再使用单一大型单片裸片(monolithic die),而是通过中介层(interposer)连接多个更小的芯粒。
- AMD EPYC(Genoa):12 个 CCD(Core Complex Die,5nm)+ 1 个 IOD(I/O Die,6nm)
- Intel Meteor Lake:CPU + GPU + SoC 分离为独立 tile
- 优点:提升良率、优化制程、降低成本
- 技术:TSMC CoWoS、Intel EMIB、UCIe 标准
HBM(High Bandwidth Memory,高带宽内存)
将 DRAM 进行 3D 堆叠,以实现超高带宽。
HBM3E(2024):9.6 Gbps/针脚,8 层堆叠 = 1.2 TB/s(每颗封装)
GDDR6X(RTX 4090):21 Gbps/针脚,384 位 = 1 TB/s
DDR5-6400:51.2 GB/s(每通道,CPU 场景)
NPU/TPU:AI 专用加速器
- Google TPU v5p:459 TFLOPS BF16,Mesh 互联
- Apple Neural Engine:iPhone 15 Pro,35 TOPS INT8
- Qualcomm Hexagon:智能手机 NPU,75 TOPS
- Intel Gaudi 3:1835 TFLOPS BF16
RISC-V 的崛起
作为开源 ISA,正从低功耗嵌入式一路扩展到数据中心服务器:
- SiFive、StarFive、阿里巴巴平头哥(T-Head)
- RISC-V International:3,000+ 家会员企业
- Linux 5.19 官方支持,Android 移植已完成
11. 性能优化实战
缓存友好型编程
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#define N 4096
float A[N][N], B[N][N], C[N][N];
// 低效:按列优先访问(缓存未命中很多)
void matmul_naive() {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; k++)
C[i][j] += A[i][k] * B[k][j]; // B[k][j] 的访问不连续
}
// 高效:分块(Tiling),实现缓存复用
#define BLOCK 64
void matmul_tiled() {
for (int ii = 0; ii < N; ii += BLOCK)
for (int jj = 0; jj < N; jj += BLOCK)
for (int kk = 0; kk < N; kk += BLOCK)
for (int i = ii; i < ii+BLOCK && i < N; i++)
for (int j = jj; j < jj+BLOCK && j < N; j++)
for (int k = kk; k < kk+BLOCK && k < N; k++)
C[i][j] += A[i][k] * B[k][j];
}
SIMD 向量化(AVX2)
#include <immintrin.h> // AVX2
// 用 AVX2 同时对 8 个 float 做加法
void vector_add_avx2(float *a, float *b, float *c, int n) {
int i;
for (i = 0; i <= n - 8; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]); // 加载 8 个 float
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_add_ps(va, vb); // 8 个同时相加
_mm256_storeu_ps(&c[i], vc); // 存储 8 个 float
}
// 处理剩余元素
for (; i < n; i++)
c[i] = a[i] + b[i];
}
12. 小测验
Q1. 流水线中数据冒险(Data Hazard)产生的原因及解决方法是什么?
答案:当前一条指令的写入(Write)还未完成,而下一条指令就试图读取该寄存器时(RAW:Read After Write)会发生数据冒险。
解决方法:
- 前递(Forwarding):将 EX/MEM 阶段的结果直接传给下一条指令的 EX 输入,从而不经过停顿即可解决。
- 停顿(Stall):插入 NOP(气泡)使流水线暂停。会造成性能下降。
- 代码重排:编译器在中间插入互不依赖的指令,无延迟地处理。
- 对于加载-使用冒险,1 个周期的停顿是不可避免的(无法将 MEM 阶段的结果传给 EX 输入)。
Q2. 直接映射缓存与 4 路组相联缓存的区别是什么?
答案:直接映射缓存中,内存的每个块只能映射到缓存里唯一的一个槽位;而 4 路组相联缓存中,同一索引可以映射到 4 个槽位中的任意一个。
核心差异:
- 直接映射:可能发生冲突未命中(Conflict Miss)。实现简单,成本低。
- 4 路组相联:冲突未命中减少。需要 LRU 等替换策略。成本增加。
- 实际 CPU 的 L1 缓存通常采用 4 路或 8 路,全相联(Fully-associative)仅用于 TLB。
Q3. 请说明 TLB(Translation Lookaside Buffer)的作用,以及 TLB 未命中时的处理过程。
答案:TLB 是缓存虚拟地址到物理地址最近转换结果的全相联缓存,用于减少访问内存中页表所带来的开销。
TLB 未命中处理:
- TLB 中不存在对应的虚拟页号(VPN)
- 由硬件(x86)或软件(RISC-V、MIPS)执行页表遍历
- 按顺序查找多级页表(PML4 → PDPT → PD → PT)
- 获得最终的物理帧号(PFN)
- 在 TLB 中添加新条目(如需替换已有条目,则采用 LRU)
- 重新尝试原本的内存访问
- 整个过程耗时数百个周期 → 因此维持 TLB 命中率(99% 以上)至关重要。
Q4. GPU 中的线程束(Warp)发散(Divergence)是什么,会对性能产生怎样的影响?
答案:当线程束内的 32 个线程走向不同的分支(if-else)时,就会发生线程束发散。
运作方式:
- 在 SIMT 模型中,线程束内的所有线程必须执行同一条指令。
- 执行 if 分支时:只有走 if 的线程被激活,走 else 的线程被屏蔽(禁用)
- 执行 else 分支时:只有走 else 的线程被激活,走 if 的线程被屏蔽
- 两条路径被串行执行,最坏情况下耗时为 2 倍
解决方法:
- 对齐数据,使同一线程束内的线程走向相同的分支
- 用数学运算代替分支(条件选择)
- 在 CUDA 着色器(shader)中尽量减少分支
Q5. 利用 Amdahl 定律,计算当整段代码的 80% 被并行化时,理论上的最大加速比。
答案:假设使用无限多的处理器,最大加速比为 5 倍。
计算过程:
- 并行化比例 f = 0.8(80%)
- 串行比例 = 1 - 0.8 = 0.2(20%)
- 令并行化加速倍数 s 趋于无穷大:f/s → 0
- Speedup = 1 / ((1 - f) + f/s) = 1 / (0.2 + 0) = 1 / 0.2 = 5 倍
含义:无论增加多少核心,串行执行部分(20%)都会把整体加速比限制在 5 倍以内。这正是为何消除串行瓶颈才是并行优化核心的原因。
参考资料
- Patterson, D. & Hennessy, J. (2020). Computer Organization and Design: RISC-V Edition (2nd ed.). Morgan Kaufmann.
- Hennessy, J. & Patterson, D. (2019). Computer Architecture: A Quantitative Approach (6th ed.). Morgan Kaufmann.
- RISC-V International. (2024). The RISC-V Instruction Set Manual. https://riscv.org/technical/specifications/
- NVIDIA. (2023). CUDA C++ Programming Guide. https://docs.nvidia.com/cuda/cuda-c-programming-guide/
- ARM Ltd. (2024). ARM Architecture Reference Manual. https://developer.arm.com/documentation/
- Intel. (2024). Intel 64 and IA-32 Architectures Software Developer's Manual.