这篇文章把我在 Advanced Architecture (UCSD CS240A) 里学到的核心机制,统一放到一条主线上:架构设计的第一性原理之一是“并行性”。
1. What is Parallelism?
- ILP(Instruction-Level Parallelism):同一线程内,让更多独立指令并发执行(流水线、超标量、OoO)。
- MLP(Memory-Level Parallelism):同一线程内,让多个 cache miss 并发在路上(non-blocking cache、MSHR、prefetch)。
- TLP(Thread-Level Parallelism):多个线程/核并发(SMT、多核)。
这三类并行里,最“容易被浪费”的资源是:
(1)前端取不到指令、(2)后端被依赖/长延迟卡住、(3)访存把一切拖慢。
2. Pipeline
经典 5 级流水:IF → ID → EX → MEM → WB
直觉上它是“时间并行”:同一时刻不同指令在不同 stage。
2.1 Hazard
- 数据相关:RAW / WAR / WAW
- 结构冲突:端口/执行单元不够
- 控制相关:分支把取指路径切断
最关键的观察是:流水线把 CPI 往 1 拉,但 hazard 会把bubble塞回去。
所以后续的 OoO / predictor / cache 其实都在对付“bubble”。
3. Branch
分支的本质问题:它让“将来执行哪条路径”不确定,前端只能干等。
所以现代 CPU 几乎默认投机执行:先猜、先跑,错了再回滚。
3.1 Branch Predictor
- 2-bit 饱和计数器:稳定但能修正短期波动
- gshare:全局历史(ghistory) xor PC
- 2-level local history: 根据local pattern (pc -> local pattern寄存器组) 对应的计数器预测
- tournament:用 chooser 在不同预测器之间动态选择
- TAGE:现代CPU常用的
3.2 BTB / RAS
- BTB(Branch Target Buffer):给出目标地址(taken 时直接改 PC),并常用于快速识别分支
- RAS(Return Address Stack):专门优化 return(比一般 predictor 更准)
4. Out of Order
如果说流水线是“stage 并行”,超标量是“同周期多发射(issue)”,那 OoO 的核心是:
执行可以乱序,提交必须顺序。
这样既能把独立指令提前跑,又能保证程序语义干净(处理exception、可回滚投机)。
4.1 rename → schedule → execute → commit
flowchart TD
A[Fetch / Decode] --> B[Rename<br/>Map Table + Free List]
B --> C[Dispatch<br/>IQ/RS + ROB alloc]
C --> D[Issue<br/>Ready select]
D --> E[Execute<br/>FUs / LSU]
E --> F[Writeback<br/>bypass + wakeup]
F --> G[Commit in order<br/>ROB retire]
G --> H[Architectural state updated]
OoO 的“并行能力”理解成由两个东西决定:
- 窗口有多大(ROB/IQ/LSQ 容量、前端供给能力)
- 窗口里能否把假相关去掉(寄存器重命名)
4.2 Register Renaming
- RAW 是真依赖(数据必须等)
- WAR/WAW 是名字冲突(“写谁”的问题),可以靠重命名消掉
典型实现(MIPS R10K):
- Map Table:architectural reg → physical reg
- Free List:可用物理寄存器池
- ROB:按程序顺序记录投机结果,用于顺序提交与回滚
4.3 ROB
ROB记录目前已经完成的指令,按照顺序提交,如果前面的指令还在运行,则等待。
5. Cache + Memory System
单线程性能经常不是被算力限制,而是被访存延迟与带宽限制。
Cache 的主旨是:利用局部性让“慢内存看起来更快”;同时用并发 miss 把等待摊薄。
5.1 Cache
Offset选 block 内字节Index选 setTag做命中比较
Physical Address (example)
+-------------------+-----------+--------+
| Tag | Index | Offset |
+-------------------+-----------+--------+
调参数(block size / associativity / sets),本质是在做取舍:
- block 大:更吃空间局部性,但可能增加冲突/带宽压力
- associativity 大:冲突少,但 hit 可能更慢、更耗能
5.2 Non-blocking Cache
阻塞 cache 的问题是“一次 miss,全体停工”。
Non-blocking cache 通过 MSHR 记录未完成 miss,使得:
- Hit-under-miss:miss 期间仍可服务 hit
- Miss-under-miss:允许多个 outstanding miss
这就是把“等内存”的时间变成“并行的在路上”。
5.3 Prefetch
预取的本质是猜“未来可能要用”,提前拉进来,减少未来 miss 的可见延迟。
但它也会抢带宽、污染 cache。这里有多种hardware和software的方式来预测。比如miss了这一行就把这一行和下一行一起拉进去。
6. MOESI
当从单核走向多核,共享内存下每个核都有自己的 cache。
MOESI 是 MESI 的扩展:多了一个 O(Owned) 状态,用来减少回写压力、提升共享脏数据的效率。
6.1 5 Stage for MOESI
- M (Modified):只在本 cache,已修改,内存是旧的,本 cache 负责提供最新数据
- O (Owned):数据可能被多个 cache 共享,但本 cache 是“owner”,内存仍是旧的,owner 负责对外提供数据(脏共享)
- E (Exclusive):只在本 cache,未修改,内存一致;若写可直接转 M(无需总线广播)
- S (Shared):多个 cache 共享,未修改,内存一致;写需要使其他副本失效
- I (Invalid):无效
6.2 Why we introduce O?
没有 O 的 MESI 下,如果一份数据被修改后又要被其他核读:
- 常见路径:M → (被迫)写回内存 → 其他核再从内存拿
这会把“共享”变成大量回写。
有 O 后:
- 修改者可以把数据以 O 的形式分享出去:不必立刻写回内存
- 其他核拿到 S,owner 仍负责提供最新数据
这在共享读多、写少的场景能显著减少内存流量。
7. TLB
OS设计的虚拟内存(virtual memory),带来一个新开销:每次访存都要把 VA → PA。
TLB 是page的cache。
7.1 VIPT
在 VIPT (virtual index, physical tag) L1 cache 中:
- 用 VA 的 page offset 部分作为
Index+Offset去读 cache set(因为 page offset 翻译前后不变) - 同时用 TLB 把 VA 翻成 PA,得到 Physical Tag
- 最后用 Physical Tag 和读出的 tag array 比较,确认命中
这样子就可以两边并行了。
7.2 Example I
假设 page size = 4KB,则 page offset = 12 bits(低 12 位不变):
Virtual Address
+---------------------+----------------+
| VPN | page offset |
+---------------------+----------------+
| |
| +--> Offset (within cache block)
+----------> Index (select cache set)
TLB 做的是 VPN → PPN:
TLB: VPN --> PPN
Physical Address 变成:
Physical Address
+---------------------+----------------+
| PPN | page offset |
+---------------------+----------------+
| Tag | Index+Offset | (in VIPT L1)
关键约束(VIPT 能并行的条件)
L1 cache 的 Index bits 必须完全落在 page offset 里。
否则不同虚拟页可能映射到同一个物理页的同一行却落到不同 set(alias/synonym)
7.3 Example II
- page size = 4KB → offset = 12 bits
- L1 line size = 64B → block offset = 6 bits
- 那么可用于 index 的 page-offset bits 还有:12 – 6 = 6 bits
- index bits = 6 → sets = 64
- 若 8-way:L1 size = sets × ways × line = 64 × 8 × 64B = 32KB
8. SMT: Simultaneous Multithreading
本课程教授的杰作
单线程会因为分支错预测、长延迟 load、资源冲突而产生大量bubble。
SMT 的思路是:同周期从多个线程取指/发射,让一个线程的bubble被另一个线程的就绪指令填掉。
8.1 Strategies
- Coarse-grain:大 miss 才切换,切换成本高(可能要 flush)
- Fine-grain:每周期轮换线程,单线程延迟变大但吞吐稳
- SMT:多线程 + superscalar,追求吞吐最大化
8.2 Resources
SMT 的核心矛盾是:多个线程争抢同一套前端/后端资源。
如果取指策略不好,很容易出现一种情况:某个线程因为 cache miss/长延迟指令卡住,但前端还在疯狂给它喂指令,结果这些指令堆在队列里占着 ROB/IQ/LSQ,把“能跑的线程”也挤死。
I-count 的思路非常直接:
优先从 “in-flight 指令数更少” 的线程取指。
in-flight(或称占用度)可以用该线程当前占用的 ROB 项数、IQ 项数等来近似衡量。
Why?
- 少喂“已经塞满/可能在等内存”的线程:
如果一个线程 in-flight 很多,往往说明它已经占了大量窗口资源,但并没有及时退休(常见原因:长延迟 load、分支错预测恢复、资源瓶颈)。 - 多喂“更可能立刻产生 useful work” 的线程:
in-flight 少的线程,意味着它没占多少资源,给它更多取指机会,能更快把执行单元填满、提高整体 IPC/吞吐。
Example
- Thread A:刚触发一个 LLC miss,ROB 里堆了一堆依赖它的指令(in-flight 很高,但大概率短期无法退休)
- Thread B:工作集命中 L1/L2,很多指令能快速执行并退休(in-flight 低,但“喂一点就能干活”)
I-count 会倾向多取 Thread B,避免让 Thread A 继续膨胀占资源,从而把 vertical waste(等待) 和 horizontal waste(槽位闲置) 都压下去。
9. Energy
加宽发射、加深窗口、堆更复杂的预测器/缓存结构,几乎都会把功耗和能耗推上去,而现代 CPU 往往首先被 功耗墙(power wall)/温度/供电约束。
9.1 Math Format
功耗分为两类动态功耗和静态功耗.
- 动态功耗大致满足:
$$
P_{dyn} \propto C \cdot V^2 \cdot A \cdot f
$$
其中 (C) 是等效电容,(V) 电压,(A) 活动因子(有多少电路在翻转),(f) 频率。
直觉:降电压收益极大(平方项),而提高频率往往需要提高电压,导致能耗暴涨。
不过随着制程的进步,静态功耗逐渐变大,小的晶体管也有漏电问题。
9.2 Save Energy in Parallelism
从并行的视角,很多省电机制其实是在问:哪些并行是“错的/没用的”?
- DVFS(动态电压频率调节)
负载低、或瓶颈在内存时,提高频率意义有限(前端/后端更多在等),这时降频降压常常是最划算的。 - Clock gating / Power gating
- clock gating:不让某些模块翻转(降低活动因子 (A))
- power gating:直接断电(减少静态泄漏 + 动态),但唤醒有延迟/状态恢复代价
- “推测是耗电的”(非常重要的架构直觉)
分支预测错了,做了大量无效工作:取指、译码、rename、进队列、执行、写回……全变成了“发热”。
所以很多设计会用类似“confidence/节流”的策略:预测不稳时降低前端攻击性,少取一点,减少错的并行。
9.3 Summary for Energy
- 更大的 OoO window:更能隐藏延迟,但 wakeup/select、CAM/比较、bypass 网络会显著增加功耗
- 更激进的 prefetch:可能降延迟,但也可能占带宽、污染 cache、带来大量“无效搬运”
- SMT:吞吐更高,但竞争更激烈;对某些 workload,额外线程会把 cache/TLB 压力推大,反而能耗更差
10. Security: Leaking from Parallelism
当我们为了并行而引入:
- 投机执行(分支预测 + OoO)
- 多级 cache / TLB / prefetch
- 共享资源(SMT、多核共享 LLC)
就会出现一个长期副作用:程序的“微架构痕迹”可能暴露本不该暴露的信息。
10.1 Side Channel Attack
Prime+Probe(No need for shared page)
- Prime:攻击者先把某些 cache sets 填满(建立“占位”)
- Victim runs:受害者访问内存,可能把攻击者占位 line 挤出去
- Probe:攻击者再访问自己那批地址测时间 → 推断哪些 set 被挤了
优点:适用面广(不需要共享页)。缺点:噪声相对更大。
Flush+Reload(Need for shared page/line)
- Flush:把共享 line 从 cache 清掉(例如 clflush)
- Victim runs:受害者若访问该 line,会把它带回 cache
- Reload:攻击者 reload 测时间 → 命中则说明 victim 访问过
优点:信号强、分辨率高。限制:通常需要共享内存页(例如共享库页)以及一些缓存包含性/系统条件。
10.2 Spectre: Predicting World
Spectre V1
1) 训练预测器:让 CPU “习惯”某个分支方向(例如 if (x < bound) 走真)
2) 触发越界条件:真实条件为假,但 CPU 在投机阶段仍沿着“训练出来的方向”执行了越界访问
3) 用 cache 痕迹外泄:把 secret 编码到 cache(例如访问 array2[secret*4096]),之后攻击者用测时读出 secret
Spectre V2
1) 训练BTB:让 CPU “习惯”某个分支方向
2) 触发越界条件:跳转这个方向到hack代码.