Skip to content

필사 모드: 计算机体系结构完全指南:从 ISA 到 GPU 并行架构

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言

计算机体系结构(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 缓存(指令缓存/数据缓存分离)。

类别冯·诺依曼哈佛
内存统一分离
带宽有限较高
复杂度较低较高
应用场景通用 CPUDSP、微控制器

计算机抽象层次

计算机系统由多个抽象层次构成:

应用程序(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]
 755357
I-型(立即数/加载):
[imm[11:0]|rs1|funct3|rd|opcode]
  125357
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 - B2 的补码)
AND:  结果 = A AND B
OR:   结果 = A OR B
XOR:  结果 = A XOR B
SLT:  结果 = (A < B) ? 1 : 0Set 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)

  1. 按顺序取指/译码指令
  2. 从已就绪的指令开始执行(Tomasulo 算法)
  3. 结果按顺序写回(使用 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):

[PML49位) | PDPT9位) | PD9位) | PT9位) | 偏移(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 的设计哲学

项目CPUGPU
核心数量数十个数千到数万个
核心复杂度非常复杂(OoO、分支预测)简单
缓存大小大且复杂小且简单
设计目标最小化单线程延迟最大化吞吐量(Throughput)
用途通用串行运算大规模并行运算

SIMT 执行模型

SIMT(Single Instruction, Multiple Thread)是 GPU 的核心执行模型。

线程束(Warp):32 个线程为一组(NVIDIA32 个线程同时执行同一条指令
→ 每个线程作用于不同的数据

线程束调度器:发生延迟(如内存访问)时立即切换到另一个线程束
→ 通过在数千个线程束之间快速切换来隐藏延迟(Latency Hiding)

NVIDIA GPU 层次结构

GPU
├── GPC(图形处理集群,Graphics Processing Cluster)x 8
│   └── SM(流多处理器,Streaming Multiprocessor)x 7-12
│       ├── CUDA Core x 128FP32 运算)
│       ├── 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:每周期 1FP32 MAC
Tensor Core(第 4 代):16x16x16 矩阵乘 = 每周期 4096FP16 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 堆叠,以实现超高带宽。

HBM3E2024):9.6 Gbps/针脚,8 层堆叠 = 1.2 TB/s(每颗封装)
GDDR6XRTX 4090):21 Gbps/针脚,384= 1 TB/s
DDR5-640051.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 未命中处理

  1. TLB 中不存在对应的虚拟页号(VPN)
  2. 由硬件(x86)或软件(RISC-V、MIPS)执行页表遍历
  3. 按顺序查找多级页表(PML4 → PDPT → PD → PT)
  4. 获得最终的物理帧号(PFN)
  5. 在 TLB 中添加新条目(如需替换已有条目,则采用 LRU)
  6. 重新尝试原本的内存访问
  • 整个过程耗时数百个周期 → 因此维持 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 倍以内。这正是为何消除串行瓶颈才是并行优化核心的原因。


参考资料

현재 단락 (1/490)

计算机体系结构(Computer Architecture)是理解硬件与软件边界的核心学科。对电子/计算机工程专业的学生而言,这是必须深入理解的领域,而对高性能软件开发者、系统程序员、芯片设计者来说,...

작성 글자: 0원문 글자: 14,564작성 단락: 0/490