交互式学习手册 · COMPUTER ARCHITECTURE PART 2
从存储墙到流水线冒险,
把 CPU 的两大提速手段吃透
这是根据 EE6156 Part 2 讲义(Lesson 7–12)整理的自学手册。前半部分讲存储系统:为什么需要缓存、缓存怎么映射和替换、写操作怎么处理、虚拟内存怎么翻译地址;后半部分讲流水线:DLX 指令集、五级流水线里每一拍发生了什么,以及结构、数据、控制三类冒险怎么让 CPI 离开 1。每课都配了可以动手的实验和自测题。
晚上 8:45–9:15,上课讲室,10 道选择题。考第 7–9 周(存储层次、缓存映射 I/II、写策略),虚拟内存不考。带笔、计算器、带照片的证件;9:00 后不能进场。老师划重点 →
上午 9:00–11:00,4 道大题:Q1–Q2 Part 1,Q3 缓存,Q4 流水线(虚拟内存、L2 公式不考)。约 50% 是例题/Tutorial 同类题,30% 换情境应用,20% 较难。
老师划重点:考什么、不考什么
第 1–9 周根据第 1–9 周的课堂录音整理。老师在课上说过的考试安排、范围、易错点和"不考"的内容都在这里。和讲义有冲突时,以老师课上说的为准;手册里对应的小节已经按老师的口径改过。
- 就在上课的讲室。6:30–8:30 正常上课,休息 15 分钟后开考,约 30 分钟。
- 10 道选择题,老师说"不难":有概念题,也有算地址位、算有效 CPI、做映射的题。
- 范围:第 7、8、9 周讲的内容(存储层次与 AMAT/CPI、三种缓存映射、3C 缺失、块大小、写策略)。虚拟内存不考。
- 带:笔(用钢笔/水笔作答)、计算器、带照片的证件。手机放包里,不能带白纸;试卷背面空白可以打草稿。
- 9:00 以后到的不能进场,算缺考;进场的人 9:00 之前不能离开。座位号会提前公布,记住自己的座位号。
- NTULearn 上有 mock-up quiz,老师在删掉虚拟内存的题后重新放出来,一定要做一遍。
- 2 小时,4 道大题:Q1、Q2 是 Part 1,Q3、Q4 是 Part 2(Cheng)。
- Part 2:Q3 考缓存(映射、AMAT、CPI,范围同 Quiz 2),Q4 考流水线(第 10–12 周)。
- 难度分布:约 50% 和 Tutorial/例题同类,30% 换情境应用,20% 较难。
- 手算缓存轨迹题在考试里会短很多(大约 4 次访问),不会像课堂练习那样 9–12 次。
- MIPS 指令表考试会提供,不用背格式,要会用。
- Quiz 1(第 7 周)和电路作业(第 7 周周五截止)已经结束。
- 参与分 = 每周课上扫码签到。
- 第 13 周是备用/复习课。如果时间不够,老师可能删减流水线的部分内容;删掉的内容会做成录像,不考。
明确不考(老师原话"quiz 和 exam 都不考")
- 虚拟内存与 TLB(9.7 节、Tutorial 9.7):第 9 周老师说没时间讲,只录视频,quiz 和 final 都不考。手册里的 9.7 节和动画留着,有兴趣再看。
- 多级缓存(L2)的公式:老师"保证"quiz 和 exam 都不考,只需要知道"加 L2 能降低平均缺失代价"这个概念(9.6 节)。
- 多核缓存一致性(MESI):超出本课范围,只提了一下。
- 老师因时间不够跳过、改成录像的内容,都不考。
Part 2(Cheng)反复强调的点
- AMAT = HR × HT + MR × MT,其中 MT = HT + MP(先查缓存,没命中再去主存)。讲义上写成 "HT + HP" 是笔误。
- 另一种写法 AMAT = HT + MR × MP 结果一样。老师说选一个公式用到底,别混着用。
- 实际 CPI = 理想 CPI + 存储停顿;停顿 = 每条指令访存次数 × 缺失率 × 缺失代价,指令和数据分开算再相加。
- 每条指令访存次数 = 1(取指)+ load/store 占比。题目说"1.2 次取指"就用 1.2(见练习 T7.6+)。
- 缺失代价给的是 ns 时,先用时钟频率换成周期数(T8.3:50 ns ÷ 2 ns = 25 周期,CPI = 2 + 0.75 + 0.5 = 3.25)。
- 地址位 = log₂(主存字节数);Offset = log₂(块字节数);Index = log₂(组数);Tag = 其余。
- 组数 = 行数 ÷ 路数。直接映射:组数 = 行数;全相联:只有 1 组,Index = 0 位。
- 注意 K/M/G:1 KB = 2¹⁰ = 1024 字节,不是 1000;"256 KB" 和 "256 B" 差了 10 位。块大小给的是"字"时先乘 4 换成字节。
- 老师喜欢的算法:块号 B = ⌊地址 / 块大小⌋,Index = B mod 组数,Tag = ⌊B / 组数⌋;或者直接把地址写成二进制切开。
- 比较器:个数 = 每次要查的行数(直接映射 1 个,全相联 = 行数,k 路 = k 个),每个比较器的位数 = Tag 位数,XOR 门数 = 两者相乘。T8.2 两路是 2 × 15 = 30(老师口误说成 32)。
- 还会考"反推":给 Tag 位数和块数,倒推块大小和容量;Index 算出来是 0 就说明是全相联。
- 每一步:先算块(地址 ÷ 块大小取整 → 这一块的起止地址),再看 Index 选组,组内比 Tag。
- 命中:什么都不换,只把这一行的 TSLR 清零、其他行加一。缺失:有空行先填空行,没空行踢 TSLR 最大(最久没用)的。
- 第一次访问某个块 = compulsory,即使同时把别人踢出去也算 compulsory(老师答疑时确认,compulsory 优先)。
- 全相联里的重复缺失 = capacity(全相联没有冲突缺失);直接映射/组相联里的重复缺失 = conflict。
- Ex 8.1 的答案:8 次缺失、4 次命中、命中率 33.3%(讲义上的 3.3% 是笔误)。
- 写直达(write-through):写缓存同时写主存,简单、无脏位,但访存流量大。写回(write-back):只写缓存并置脏位,被踢出时才写回主存,性能高,现代缓存多用写回。
- 写分配(write allocate,先把块搬进缓存再写,常配写回)vs 不写分配(直接写主存)。写缓冲(write buffer)让 CPU 不用等写完。
- 块太大:缺失率先降后升(块数变少,冲突/容量缺失增加),缺失代价变大;常用 64–128 字节。
- 加大容量比加相联度效果更明显;相联度高了以后再加收益很小。
- CPU 变快、内存不变:缺失代价按周期算会变大(时钟翻倍,100 → 200 周期,CPI 5.44 → 8.88),停顿占比越来越高。
- 直接映射:硬件简单但不灵活;全相联:冲突最少但比较器多;组相联是折中。
老师给出的答案和手册原来不一样的地方(已改)
| 题目 | 老师课上的答案 | 说明 |
|---|---|---|
| Exercise 7.2 | 5 次访存,50 ms vs 15 ms,3.3 倍 | 每轮只算一次 a[i];原来按 10 次算出 5 倍 |
| Tutorial 7.4 | 80 ms vs 22 + 6 = 28 ms,2.86 倍 | 老师用缓存 2 ms、主存 20 ms(学生版题目没给时间) |
| Tutorial 7.6 | AMAT 15 ns vs 50 ns → 88 分钟,多 28 分钟 | MT = 10 + 50 = 60 ns;原来用 100 ns 得 19.6 分钟 |
| T7.6+(新增) | 停顿 1.2 + 1.2 = 2.4,CPI 3.9 | 老师解答里多出来的一题,已加进习题小程序 |
| Exercise 8.1 | 最后的 241 是 compulsory | 老师课上改正了讲义;手册一直按这个口径 |
Part 1 的重点回顾
- Lecture 1–6 全部可能考,有理论题也有计算题;重心是 Lecture 3(CPU 模型与电路)和 Lecture 4(MIPS ISA)。
- "讲义上有的都可能考",别只看计算。复杂度分析(大 O)不会深考。
- 老师的原话:理解原理,别背答案;Part 1 搞懂了 30 分钟能做完。
- 传输门(T-gate)的两个控制端必须永远相反:一个接信号,另一个接同一个信号经过非门的结果。不要用两个独立输入。
- PMOS(栅极带圆圈)负责上拉,NMOS 负责下拉;画 D 触发器、用 T-gate 搭多路选择器是常考题。
- 扇出(fan-out)超限时用两个非门做缓冲,刷新信号。本课只用上升沿触发。
- 先认类型(R / I / J),再找 rs、rt、rd 的位置;I 型的目的寄存器是 rt。
- 立即数要符号扩展(逻辑运算是零扩展);分支偏移 × 4,跳转地址 ÷ 4;PC + 4。
- 二进制转十六进制按 4 位一组;补码用计算器的进制转换功能核对。
- 图灵机:小端序的 +1 / −1 例子,状态转移表,怎么把 TM 编码成二进制串。
- RAM 模型:运算前必须先 load 到寄存器;函数调用要压栈保存返回地址。PM 模型:指针只能存储和判等。
- 加速比(Amdahl 定律)、GB 和 GiB 的区别、磁盘延迟 = 寻道 + 旋转 + 传输 + 控制器/排队。
Part 1 的详细复习在首页的芯片蓝图、Tutorial 精讲、模拟卷和速查表里。
体系结构基础与存储层次
Quiz 2 范围Part 2 的第一课回答一个问题:为什么 CPU 越来越快,程序却没有同比例变快?答案是 CPU 大部分时间在等内存。这一课先说明冯·诺依曼结构为什么会有"瓶颈",再引出存储层次和局部性原理,最后给出整个 Part 2 都要用的两组公式:平均访存时间(AMAT)和带存储停顿的实际 CPI。
7.1 冯·诺依曼结构与它的瓶颈
冯·诺依曼结构的核心是存储程序(stored-program):指令和数据放在同一个主存里。它由这几部分组成:
- CPU:控制单元(CU)负责取指和控制指令执行;ALU 负责算术和逻辑运算。
- 主存:同时存程序和数据,按字节编址(每个地址存 1 个字节)。
- 系统总线:在 CPU、内存、I/O 之间传数据、地址和控制信号。
- I/O 设备和辅存(硬盘、SSD,非易失)。
问题出在"共享"上:指令和数据走同一个存储器、同一条通路。CPU 处理数据的速度远快于数据在总线上搬运的速度,于是 CPU 经常停下来等,这就是冯·诺依曼瓶颈。
| 容量增长 | 速度(延迟)增长 | |
|---|---|---|
| 逻辑电路 | 每 1.5 年 ×4 | 每 3 年 ×4 |
| DRAM | 每 3 年 ×4 | 每 10 年 ×2 |
| 磁盘 | 每 3 年 ×4 | 每 10 年 ×2 |
讲义里的 DRAM 数据:1980 年 64 Kb、250 ns,到 1995 年 64 Mb、120 ns。容量涨了 1000 倍,周期时间只缩短一半。CPU 与 DRAM 之间越拉越大的差距被称为存储墙(memory wall)。
怎么缓解瓶颈(讲义列了五条)
- 缓存与存储层次:把常用数据放在离 CPU 近的地方。这是本课和接下来两课的主线。
- 哈佛或混合结构:指令和数据走分开的通路,带宽翻倍。
- 并行与专用化:多核、GPU、向量机、AI 加速器。
- 存内/近存计算(PIM):把处理器和 RAM 做在同一芯片上,减少数据搬运。
- 更快的互连与存储:例如 3D 堆叠的高带宽内存 HBM。
7.2 存储层次与局部性原理
硬件上有个绕不开的事实:大的存储器慢,快的存储器小。存储层次的思路是把几种存储器叠起来,让程序"看起来"拥有一块又大、又快、又便宜的内存。
- 任何时刻,数据只在相邻两层之间复制。靠近 CPU 的叫上层(小、快、贵),远的叫下层(大、慢、便宜)。
- 层与层之间搬运的基本单位叫块(block);块放进缓存后占据的位置叫缓存行(cache line)。本课程里 block 和 line 混用。
层次结构之所以有效,靠的是局部性原理:程序在任一时刻只访问地址空间里很小的一部分("90/10 规则":10% 的代码占 90% 的执行时间)。局部性分两种:
| 类型 | 含义 | 例子 | 缓存怎么利用它 |
|---|---|---|---|
| 时间局部性 | 刚访问过的数据很快会再被访问 | 循环变量、查找表、循环体里的指令 | 访问过的数据留在缓存里;满了就淘汰最久没用的 |
| 空间局部性 | 访问某地址后,邻近地址很快会被访问 | 顺序遍历数组:100, 101, 102, 103 | 缺失时一次取回一整块连续地址 |
序列 100, 101, 102, 103, 100, 101, 102, 200, 200 同时有两种局部性:100→103 依次递增是空间局部性;100 又出现、200 连续两次是时间局部性。Tutorial 9.1(d) 就是这样考的。
主存和缓存到底是什么、在哪里
以一台常见的游戏本为例(Intel Core Ultra 7 处理器、16 GB 内存、2 TB SSD):
| 层次 | 是什么 | 物理位置 | 这台电脑上 |
|---|---|---|---|
| 寄存器 | CPU 正在算的那几个数 | CPU 每个核内部 | 每个核几十个 |
| 缓存(cache) | SRAM,很小很快 | CPU 芯片里面,紧挨着各个核 | L1 每核 32–48 KB;L2 几 MB;L3 30 MB,所有核共用 |
| 主存(main memory) | DRAM,平时说的"内存"、RAM | CPU 外面:主板上的内存条或内存芯片,经内存总线连到 CPU | 16 GB |
| 辅存 | SSD / 硬盘 | 主板上的 M.2 插槽等 | 2 TB SSD |
┌──────────── CPU 芯片(一小块硅片) ────────────┐
│ 核 0: 寄存器 + L1 核 1: 寄存器 + L1 … │
│ L2(每个核或每组核一个) │
│ L3(所有核共用) ← 这些都是"缓存" │
└───────────────────┬────────────────────────────┘
│ 内存总线(芯片外面的导线)
┌─────────┴─────────┐
│ 内存条 DRAM │ ← 这就是"主存"
└─────────┬─────────┘
│ PCIe / NVMe
┌─────────┴─────────┐
│ SSD / 硬盘 │ ← 辅存,关机后数据还在
└───────────────────┘- 主存放"原件":正在运行的程序和它的全部数据。打开程序时,系统先把它从 SSD 读进主存;断电就清空。
- 缓存放"常用的复印件":只是主存里最近用过的一小部分的副本(上例 L3 只有 30 MB,约为主存的 1/500)。CPU 读某个地址时先查 L1,没有再查 L2、L3,都没有才去主存;从主存取回时把整块复制进缓存,下次再用就快了。
- 速度差得很远:L1 约 1 ns(几个周期);主存约 50–100 ns(几百个周期);SSD 约 50–150 μs,比主存再慢上千倍。
寄存器 = 手里正拿着的那张纸;缓存 = 书桌,只放得下几本书但伸手就拿到;主存 = 房间里的书架,书多得多但要起身去拿;SSD = 楼下的图书馆,什么都有,跑一趟最慢。命中就是书在桌上;缺失就是要去书架拿,多花的时间是缺失代价。缓存里是副本,在副本上改了值,原件就旧了,这就是 9.4 节写策略要解决的问题。
注意:日常说的"手机内存 256 GB"其实是存储空间,相当于这里的辅存;课程里的主存(内存)专指 RAM。考试题默认只有"一级缓存 + 主存"两层,不用管 L2、L3。
补充:显存(GPU 内存)在哪一层
显存在层次图上和主存是同一层:它本身就是 DRAM,只是不属于 CPU 这座金字塔,而是 GPU 自己那座金字塔里"主存"的位置。
| 层次 | CPU 这一侧 | GPU 这一侧 |
|---|---|---|
| 最快、最小 | 寄存器 | 寄存器(每个 SM 有几万个) |
| 片上缓存 | L1 / L2 / L3(SRAM) | 共享内存(shared memory)、L1 / L2(SRAM) |
| 片外 DRAM | 主存:DDR4 / DDR5 | 显存:GDDR6 / GDDR7,数据中心卡用 HBM |
| 更下面 | SSD / 硬盘 | 经 PCIe 访问 CPU 的主存,再往下是硬盘 |
- 技术上它就是 DRAM。它和主存一样处在"片外、大、比 SRAM 慢"那一层,区别是为带宽设计:数据位宽更宽、频率更高。例如一块 8 GB GDDR7 的笔记本显卡,带宽有几百 GB/s;CPU 的双通道 DDR5 约 80–100 GB/s。单次访问的延迟并不比主存短,GPU 靠几千个线程同时访存把延迟藏起来。
- 两座金字塔之间隔着 PCIe。CPU 不能像用自己的主存那样透明地用显存,数据要显式拷过去(例如
cudaMemcpy)。PCIe 只有几十 GB/s,远低于显存带宽。这条通路就是 7.1 节冯·诺依曼瓶颈在 CPU 和 GPU 之间的翻版,所以跑深度学习时要尽量让数据一直留在显存里。 - 集成显卡没有独立显存。核显(以及苹果 M 系列的统一内存)的"显存"就是从主存里划出来的一块,和 CPU 共用同一层 DRAM。
- 和讲义的联系。7.1 节"怎么缓解瓶颈"里的"GPU 等并行加速器"和"3D 堆叠的 HBM"指的就是这一套:HBM 是贴在 GPU 旁边堆叠起来的显存。
课程默认只有"缓存 + 主存"两级,显存不在考试范围里,算题时不要把它算进去。
7.3 命中、缺失与 AMAT(课程约定)
本课程默认只有两级:缓存和主存。访问缓存找到了叫命中(hit),没找到就要去主存取块,叫缺失(miss),CPU 为此多等的周期叫存储停顿(memory stall)。几个时间量要分清:
讲义例子:缓存 1 周期,主存 50 周期,命中率 80%。命中时间 1,缺失代价 50,缺失时间 51,缺失率 0.2,所以 AMAT = 0.8×1 + 0.2×51 = 11 周期。
缺失代价(penalty)只是"多出来"的那部分时间;缺失时间(miss time)= 查缓存的时间 + 缺失代价。题目说"缓存比主存快 10 倍"时,按课程约定一次缺失要花 C + M = 1 + 10 = 11 个单位,而不是 10。Tutorial 7.1 用这个约定算出加速比正好是 5:不用缓存每次 10,用缓存 AMAT = 0.9×1 + 0.1×11 = 2。
加速比(speedup)是什么
加速比衡量一项改进让同一件事快了多少倍:
- 大于 1 表示变快了:加速比 4 就是快了 4 倍,时间变成原来的 1/4;小于 1 表示反而变慢了。
- 前后必须是同一个任务(同一个程序、同一批数据),否则比较没有意义。
| 场景 | 公式 | 课程里的例子 |
|---|---|---|
| 加缓存 | 无缓存每次访问时间 / AMAT | T7.1:10 → AMAT 2,加速比 5 |
| 流水线(指令很多) | 单周期时间 / 流水线时钟周期 | 10.4 节:950 ps / 250 ps = 3.8 |
| 流水线(n 个任务) | 顺序总时间 / 流水总时间 | 10.3 节洗衣房:360 / 210 ≈ 1.71 |
| 时钟相同,只比 CPI | CPI旧 / CPI新 | 12.1 节:5 / 1.25 = 4 |
| 时钟也变了(最通用) | (CPI旧 × T旧) / (CPI新 × T新) | T11.8:(4.6 × 0.8) / (1.2 × 1.0) ≈ 3.07 |
最后一行是通式:每条指令的平均时间 = CPI × 时钟周期,CPU 时间之比就是这两项乘积之比,前面几行都是它的特殊情况。
- 分子分母别放反:慢的放上面、快的放下面,结果才大于 1。
- "快 3 倍"和"快 300%"不是一回事:加速比 3 是时间变成 1/3;"快了 35%"对应加速比 1.35。T7.3 问"慢多少",2.7 / 2.0 = 1.35 倍,即多花 35% 的时间。
- 流水线加速比有上限:各级完全平衡、指令无限多、没有停顿时,最大加速比 = 级数;不均衡、填充排空、冒险停顿都会让它更小。
- 只看 AMAT 会出错:T9.2 里 2 路组相联的 AMAT 更小,但时钟周期变长,算 CPU 时间之比反而是直接映射更快。比较要比"整个程序的总时间"。
- Part 1 的 Amdahl 定律也是加速比:只有比例 f 的部分被加速 s 倍时,整体加速比 = 1 / ((1 − f) + f / s)。T7.1 若按"缺失只花主存时间"的口径算,就是这个公式,得 5.26;按课程口径是 5。
7.4 存储停顿与实际 CPI
理想 CPI 是"所有取指和取数都命中缓存、没有流水线冒险、没有分支代价、不等 I/O"时每条指令平均需要的周期数,代表处理器理论上最好的表现。现实中缺失会带来停顿:
先分清:取指 vs 取数
取指(instruction fetch):把指令本身从存储器读出来。CPU 按 PC 里的地址读出那 32 位指令编码,放进指令寄存器 IR,然后 PC 加 4。取数(data access):指令执行时,去存储器里读或写它要处理的数据。
| 取指 | 取数 | |
|---|---|---|
| 读的是什么 | 指令本身(操作码、寄存器号、立即数) | 程序的数据(数组元素、变量) |
| 地址从哪来 | PC | 指令算出来的有效地址,如 R2 + 8 |
| 哪些指令有 | 每一条都有,课程默认恰好 1 次 | 只有 load / store(DLX 是 load-store 架构) |
| 读还是写 | 只读 | load 读,store 写 |
| 流水线哪一级 | IF(第 1 级) | MEM(第 4 级) |
| 走哪个缓存 | 指令缓存 I-cache | 数据缓存 D-cache |
例:LW R1, 8(R2) 存在地址 1000,R2 = 500。取指:读 Mem[1000],得到这条指令的编码;取数:读 Mem[500 + 8] = Mem[508],把值放进 R1。这一条指令访问了两次存储器。ADD R1, R2, R3 只有取指(操作数都在寄存器里);SW R3, 8(R2) 是取指加一次写数据。
- 访存次数 = 1(取指)+ load/store 比例(取数)。
- 缺失率要分别乘:指令停顿 = 1 × I-缺失率 × 代价;数据停顿 = load/store 比例 × D-缺失率 × 代价。
- 结构冒险:指令和数据共用一个单端口存储器时,一条指令在 IF 取指、另一条在 MEM 取数,同一周期抢同一个存储器(Lesson 12)。分开的指令/数据缓存(哈佛结构的思路)可以解决。
"load/store 占 40%"是什么意思
这叫指令构成(instruction mix):程序运行时执行的所有指令里,有 40% 是 load 或 store(两者合起来算,比如 25% load + 15% store),其余 60% 是算术逻辑、分支这类不访问数据存储器的指令。
- 数的是执行次数,不是代码行数:循环里的一条 LW 跑 100 遍就算 100 条,题目给的比例都是运行时的统计。
- 40% 只用在数据访问上:取指那一项每条指令都有,前面乘 1,不要再乘 40%。
- 给比例时先换成百分比:Exercise 7.1 给的是 4 : 3 : 1,load/store 占 3 / 8 = 37.5%。
情形 1:取指和取数的缺失率、缺失代价相同
课程默认每条指令都要取指一次(I = 1),只有 load/store 才访问数据存储器。讲义例子:理想 CPI 2.0,load/store 占 40%,缺失率都是 2%,代价 25 周期 → 访存次数 1.4,停顿 1.4×0.02×25 = 0.7,实际 CPI 2.7,比全命中慢 2.7/2.0 = 1.35 倍。如果根本没有缓存,每次访存都要 25 周期:停顿 1.4×25 = 35,CPI = 37。
情形 2:指令缓存和数据缓存的参数不同
讲义例子:理想 CPI 1.1,指令构成 50% 算术、30% load/store、20% 控制。数据缺失率 10%,代价 50。只算数据缺失:0.3×0.1×50 = 1.5,CPI = 2.6,有 1.5/2.6 = 58% 的时间在等内存。再加上 1% 的指令缺失(0.01×50 = 0.5),CPI = 3.1,停顿占 2.0/3.1 = 64.5%。
- 写出每条指令的取指次数(1)和取数次数(load/store 比例)。
- 分别算指令停顿和数据停顿,相加得每条指令停顿。
- 实际 CPI = 理想 CPI + 停顿;停顿占比 = 停顿 / 实际 CPI;变慢倍数 = 实际 CPI / 理想 CPI。
- 如果代价以纳秒给出,先换成周期:代价(周期) = 主存时间 ÷ 时钟周期(500 MHz → 2 ns)。
A processor has an ideal CPI of 1.2. The instruction mix ratio is 4 : 3 : 1 for arithmetic/logic, load/store and control instructions. The instruction-cache miss rate is 2%, the data-cache miss rate is 15%, and the miss penalty for both is 40 cycles. Compute (1) the total average miss penalty per instruction, (2) the actual CPI, (3) the percentage of execution time spent on memory stalls.
理想 CPI 1.2,算术逻辑 : load/store : 控制 = 4 : 3 : 1。指令缓存缺失率 2%,数据缓存缺失率 15%,两者缺失代价都是 40 周期。求 (1) 每条指令的平均缺失代价总和;(2) 实际 CPI;(3) 存储停顿占执行时间的百分比。
解答 · Solution
Convert the ratio into a fraction first. With a 15% data miss rate, data stalls dominate.
比例要先换成百分比。数据缺失率高达 15%,所以数据停顿占了大头。
Ideal CPI = 1.5, instruction count IC = 2 × 106, 30% of instructions are loads/stores, the miss rate for both instructions and data is 4%, and the miss penalty is 20 cycles. Each instruction needs one instruction fetch and only loads/stores access data memory. Compute (1) memory references per instruction, (2) memory stall cycles per instruction, (3) total memory stall cycles, (4) actual CPI, (5) total CPU execution cycles.
理想 CPI 1.5,指令数 IC = 2 × 106,load/store 占 30%,指令和数据缺失率都是 4%,缺失代价 20 周期。每条指令取指一次,只有 load/store 访问数据存储器。求 (1) 每条指令访存次数;(2) 每条指令停顿;(3) 总停顿周期;(4) 实际 CPI;(5) CPU 总周期数。
解答 · Solution
Ideal CPI = 1.5. Instruction mix: 50% arithmetic/logic, 30% load/store, 20% branch/control. I-cache miss rate 1% with a 30-cycle penalty; D-cache miss rate 4% (of data accesses) with a 50-cycle penalty. Compute the I-cache stall cycles per instruction, the D-cache stall cycles per instruction, the total memory stall cycles per instruction, the actual CPI and the percentage of execution time spent on memory stalls.
理想 CPI 1.5;50% 算术逻辑、30% load/store、20% 分支控制。指令缓存缺失率 1%,代价 30 周期;数据缓存缺失率 4%(按数据访问计),代价 50 周期。求指令停顿、数据停顿、总停顿、实际 CPI 和停顿占比。
解答 · Solution
改动参数,看实际 CPI 由哪几部分组成。可以点下方的预设,直接套用讲义例题和 Tutorial 的数据。
7.5 循环访存计时题
讲义用一个循环说明缓存为什么有效:循环 10 次访问数组 a[i],缓存访问 1 ms,RAM 访问 10 ms(夸张的数字,只为好算)。约定如下:
- 每次引用
a[i]算一次访存;sum、i都在寄存器里,不产生访存。 - 整个数组在同一个块里:第一次访问缺失,把块从 RAM 搬进缓存;之后全部命中。
这类题最容易错在"一共访问了几次 a[i]"。老师课上做 Exercise 7.2 时数的是 5 次(i = 0…4,每轮访问一个元素):无缓存 5 × 10 = 50 ms,有缓存 (1 + 10) + 4 × 1 = 15 ms,提升 3.3 倍。也就是说 if (a[i] > 0) 和后面的 sum ±= a[i] 读的是同一个元素,只算一次。Tutorial 7.4 的 while (sum < 15 && i < 8) 条件只用寄存器,要先模拟到 sum 不再小于 15 时停下,数清实际执行了几轮(4 轮)。
老师的口径:第一次缺失的时间 = 缓存时间 + 主存时间(MT = HT + MP),之后每次只花缓存时间。
Consider sum = 0; i = 0; while (i < 5) { if (a[i] > 0) sum += a[i]; else sum -= a[i]; i++; } with a = [4, −2, 7, −5, 3]. Assume each array element a[i] used in an iteration costs one memory access, sum and i are in registers, cache access = 1 ms, main-memory access = 10 ms, and the first array access misses while all later ones hit. Determine (1) the final sum, (2) the number of memory accesses, (3) the total access time without a cache, (4) with a cache, (5) the performance improvement factor.
程序:sum = 0; i = 0; while (i < 5) { if (a[i] > 0) sum += a[i]; else sum -= a[i]; i++; },a = [4, −2, 7, −5, 3]。假设每轮用到的数组元素 a[i] 算一次访存,sum、i 在寄存器里;缓存 1 ms,主存 10 ms;第一次访问数组缺失,之后全部命中。求 (1) 最终 sum;(2) 访存次数;(3) 无缓存总时间;(4) 有缓存总时间;(5) 性能提升倍数。
解答 · Solution
The lecturer's in-class answer (week 7) is 5 accesses, 50 ms, 15 ms, 3.3×: one access per array element. Miss time = cache 1 ms + memory 10 ms = 11 ms (MT = HT + MP). Counting the if test and the add/subtract separately (2 per iteration) would give 10 accesses, 100 ms, 20 ms and 5×; in the exam use the lecturer's count and state the assumption.
老师课上(第 7 周)给的答案就是 5 次、50 ms、15 ms、3.3 倍:每轮按"访问一个数组元素"算一次。缺失时间 = 缓存 1 ms + 主存 10 ms = 11 ms(MT = HT + MP)。如果把 if 判断和加减各算一次(每轮 2 次),会得到 10 次、100 ms、20 ms、5 倍;考试按老师的数法写 5 次,必要时注明假设。
自测 · Self-Check
冯·诺依曼瓶颈的根本原因是什么?为什么哈佛结构能缓解它?
"缺失代价"和"缺失时间"差在哪里?缓存 2 周期、主存 80 周期时分别是多少?
为什么题目说"每条指令的访存次数 = 1 + load/store 比例"?
理想 CPI 2.0、停顿 0.7 时,机器比"全部命中"慢多少?停顿占执行时间的多少?
为什么缓存一次搬"一整块"而不是只搬 CPU 要的那个字节?
缓存映射 I:三种组织方式
Quiz 2 范围存储层次的设计目标有两个:用最便宜的技术提供大容量,同时借助局部性让平均访问速度接近最快的那一层。这一课进入缓存内部:一个内存块进了缓存该放在哪一行?怎么判断命中?满了踢谁?讲义用同一个 64 字节内存、4 行缓存的小例子,把全相联、直接映射、组相联三种方式依次走了一遍。本课的手算表格是 Quiz 2 的重点题型。
8.1 性能指标与 AMAT 公式
| 缩写 | 名称 | 含义 |
|---|---|---|
| HT | 命中时间 Hit Time | 在某一层命中时完成访问的时间。每次访问都先查缓存,所以每次都要付 HT |
| MP | 缺失代价 Miss Penalty | 在命中时间之外,从下一层取数据并交给 CPU 的额外时间 |
| MT | 缺失时间 Miss Time | 缺失时总时间,MT = HT + MP |
| HR / MR | 命中率 / 缺失率 | HR + MR = 1 |
把 MT = HT + MP 代进 Lesson 7 的 AMAT 定义,就得到最常用的形式:
两个式子结果相同,Tutorial 8.6 就要求你用两种写法各算一遍来验证。讲义例子:HT = 1,MP = 100,HR = 97%,AMAT = 1 + 0.03×100 = 4 周期。缺失率只有 3%,但缺失代价太大,平均访问时间就从 1 涨到了 4。
| 技术 | 典型用途 | 访问时间 | 每比特成本 |
|---|---|---|---|
| SRAM | 处理器缓存 | ~0.5–2 ns | 最高 |
| DRAM | 主存 | ~10–20 ns | 中等 |
| SSD / 硬盘 | 辅存 | SSD ~50–150 μs | 最低 |
8.2 缓存设计四问与三种映射
- 怎么判断命中?比较标签(tag):地址里的标签和缓存行里存的标签相同(且有效位为 1)就是命中。
- 新块放在哪?映射方式:全相联、直接映射、组相联。
- 候选位置都满了踢谁?替换策略:LRU(最近最少使用)、FIFO、随机。
- 块里的哪个字节?用地址最低的偏移位(offset)选。
讲义的统一例子:主存 64 字节(地址 0–63,需要 6 位地址),块大小 4 字节,所以主存有 16 个块;缓存 16 字节,即 4 行。下面点切换,看同一个内存块在三种方式下能去哪:
8.3 全相联 + LRU:最灵活的放法
全相联(fully associative):任何内存块都可以放进任何一行。地址只分两段:
偏移位数 = log₂(块大小) = log₂4 = 2;标签位数 = 地址位数 − 偏移位数 = 6 − 2 = 4(也等于 log₂(主存块数) = log₂16)。每行除了数据,还要存标签和替换用的 LRU 信息。讲义用 TSLR(time since last referenced,距上次访问的次数)记录 LRU:每访问一次,所有有效行的 TSLR 加 1,被访问的行清零;要替换时踢 TSLR 最大的那行。
判断 HIT 还是 MISS:每一步问三个问题
判断命中只看一件事:这个地址所在的"块",此刻是不是在缓存里。和这个地址本身以前有没有被访问过无关。
- 它属于哪个块?块大小 4 字节,块从 4 的倍数开始:块起点 = 地址 ÷ 4 取整再 × 4。例如 24 → 24–27,20 → 20–23,04 → 4–7,45 → 44–47。
- 这个块现在在缓存里吗?全相联时,4 行里任何一行有它都算。
- 在 → HIT,记下它刚被用过。不在 → MISS,把整块搬进来:有空行放空行;4 行都满了,踢掉最久没被用过的那一块(LRU)。
讲义例题逐步走:序列 24, 20, 04, 12, 20, 44, 04, 24, 44
表中括号里记的是这一块最后一次被用到是第几步,要踢的时候踢括号里数字最小的。
| 步 | 地址 → 块 | 块此刻在缓存里吗? | 结果 | 访问后缓存里的块 |
|---|---|---|---|---|
| 1 | 24 → 24–27 | 缓存是空的,不在 | MISS | 24–27(1) |
| 2 | 20 → 20–23 | 不在 | MISS | 24–27(1)、20–23(2) |
| 3 | 04 → 4–7 | 不在 | MISS | 24–27(1)、20–23(2)、4–7(3) |
| 4 | 12 → 12–15 | 不在 | MISS | 24–27(1)、20–23(2)、4–7(3)、12–15(4) ← 满了 |
| 5 | 20 → 20–23 | 在(第 2 步搬进来的) | HIT | 20–23 更新为 (5) |
| 6 | 44 → 44–47 | 不在,而且满了 | MISS | 踢掉数字最小的 24–27(1),换成 44–47(6) |
| 7 | 04 → 4–7 | 在 | HIT | 4–7 更新为 (7) |
| 8 | 24 → 24–27 | 不在了(第 6 步被踢掉) | MISS | 此时 12–15(4) 最久没用,踢掉它,换成 24–27(8) |
| 9 | 44 → 44–47 | 在(第 6 步搬进来的) | HIT | 共 3 次命中、6 次缺失 |
讲义里的 TSLR 记的是"距上次使用过了几次访问",数字最大的就是最久没用的;上表记"最后使用在第几步"、取最小的。两种记法说的是同一件事,方向相反而已。
讲义把第 6 步(44 第一次被访问,同时引起替换)标成 capacity miss,理由是缓存装不下序列需要的所有块。但按讲义自己第 24 页的定义("块第一次被访问"就是 compulsory),以及教科书的标准 3C 模型,第一次访问一律算 compulsory。下面的模拟器按标准定义分类。考试遇到"首次访问且发生替换"时写 compulsory,可以注明"cache full, LRU block replaced"。老师课上已确认:讲 Ex 8.1 时把最后的 241 改成了 compulsory,第 9 周答疑时又说"第一次访问就是 compulsory,即使同时要踢掉别的块,compulsory 优先"。
- 以前访问过 ≠ 一定命中。第 8 步的 24 第 1 步就访问过,但第 6 步被踢出去了,所以还是 MISS。
- 没访问过 ≠ 一定缺失。命中按"块"算。Exercise 8.1 先访问 08,块 8–11 整块进了缓存;之后访问 09,虽然 09 是第一次出现,但它也在 8–11 里,所以是 HIT。这就是空间局部性。
"在不在缓存里"到底怎么看
手算时,看你自己记下的缓存内容。缓存里有什么完全由前面的步骤决定,所以手算时每一步都要把当前缓存里的块记下来(上表最右一栏)。判断第 n 步时,只看第 n − 1 步那一行:要找的块在里面就是 HIT。例如第 8 步访问 24(块 24–27),第 7 步后缓存里是 44–47、20–23、4–7、12–15,没有 24–27,所以 MISS。
硬件上,比较标签(tag)。硬件不存"24–27"这种范围,而是在每一行存一个标签,再加一个有效位(这一行有没有装东西):
- 把地址拆开:本例地址 6 位,最低 2 位是偏移;全相联没有索引,高 4 位全是标签,标签其实就是块号。
- 拿地址的标签和缓存里存的标签逐个比较。
- 某一行标签相同且有效位为 1 → HIT;一个都不同 → MISS。空行有效位是 0,永远不会命中。
| 步 | 地址 | 二进制(标签 | 偏移) | 当时缓存里的标签 | 比较结果 |
|---|---|---|---|---|
| 5 | 20 | 0101 | 00 | 0110、0101、0001、0011 | 与第 2 行相同 → HIT |
| 6 | 44 | 1011 | 00 | 0110、0101、0001、0011 | 都不同 → MISS,踢掉 0110 |
| 8 | 24 | 0110 | 00 | 1011、0101、0001、0011 | 都不同 → MISS(0110 第 6 步已被踢) |
这也解释了 09 为什么在 08 之后命中:08 = 0010|00,09 = 0010|01,标签都是 0010,只是偏移不同。偏移只决定从块里取哪个字节,不影响命中判断。
| 组织 | 和哪些行比较标签 | 比较器 |
|---|---|---|
| 全相联 | 所有行(4 行同时比) | 4 个,硬件最贵 |
| 直接映射 | 先用索引位直接选出唯一一行,只和这一行比。例如 04 = 00|01|00 只看第 01 行,它存的标签 01(块 20–23)≠ 00 → MISS | 1 个 |
| 组相联 | 先用索引选出一组,再和组内几行比 | k 个(k 路) |
缓存模拟器每一格里显示的就是存的标签("tag 0101"),下方表格的"二进制拆分"一栏给出地址的标签,对照着看就能看清每一步在比什么。
Fully associative cache: main memory 256 bytes, 4 cache lines, 4-byte blocks, LRU replacement, initially empty. Reference sequence: 08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241. (1) Number of bits for the address, offset and tag. (2) For each reference, update the cache and show the tag and time since last referenced. (3) Hit or miss; for a miss, compulsory or capacity. (4) Total hits, misses and hit rate.
全相联缓存:主存 256 字节,4 行,块 4 字节,LRU 替换,初始为空。访问序列:08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241。(1) 地址、偏移、标签各几位;(2) 每次访问后的缓存内容(标签与距上次访问次数 TSLR);(3) 命中还是缺失,缺失是强制还是容量;(4) 总命中、总缺失和命中率。
解答 · Solution
| # | Addr 地址 | Tag Index Offset | Block 块 | Set/Line 组/行 | Tag | Result 结果 | Evicted 被替换 | Cache after 访问后缓存 (TSLR) |
|---|---|---|---|---|---|---|---|---|
| 1 | 8 | 000010 00 | 8–11 | – | 000010 | MISS compulsory | 8–11(0) | |
| 2 | 124 | 011111 00 | 124–127 | – | 011111 | MISS compulsory | 8–11(1), 124–127(0) | |
| 3 | 77 | 010011 01 | 76–79 | – | 010011 | MISS compulsory | 8–11(2), 124–127(1), 76–79(0) | |
| 4 | 45 | 001011 01 | 44–47 | – | 001011 | MISS compulsory | 8–11(3), 124–127(2), 76–79(1), 44–47(0) | |
| 5 | 9 | 000010 01 | 8–11 | – | 000010 | HIT | 8–11(0), 124–127(3), 76–79(2), 44–47(1) | |
| 6 | 126 | 011111 10 | 124–127 | – | 011111 | HIT | 8–11(1), 124–127(0), 76–79(3), 44–47(2) | |
| 7 | 44 | 001011 00 | 44–47 | – | 001011 | HIT | 8–11(2), 124–127(1), 76–79(4), 44–47(0) | |
| 8 | 211 | 110100 11 | 208–211 | – | 110100 | MISS compulsory | 76–79 | 8–11(3), 124–127(2), 208–211(0), 44–47(1) |
| 9 | 8 | 000010 00 | 8–11 | – | 000010 | HIT | 8–11(0), 124–127(3), 208–211(1), 44–47(2) | |
| 10 | 212 | 110101 00 | 212–215 | – | 110101 | MISS compulsory | 124–127 | 8–11(1), 212–215(0), 208–211(2), 44–47(3) |
| 11 | 77 | 010011 01 | 76–79 | – | 010011 | MISS capacity | 44–47 | 8–11(2), 212–215(1), 208–211(3), 76–79(0) |
| 12 | 241 | 111100 01 | 240–243 | – | 111100 | MISS compulsory | 208–211 | 8–11(3), 212–215(2), 240–243(0), 76–79(1) |
(1) Address = log₂256 = 8 bits, offset = log₂4 = 2 bits, no index in a fully associative cache, tag = 8 − 2 = 6 bits.
(4) 4 hits (09, 126, 44, 08) and 8 misses (7 compulsory, 1 capacity: the second access to 77). Hit rate = 4/12 = 33.3%. 09 shares block 8–11 with 08, 126 shares block 124–127 with 124, and 44 shares block 44–47 with 45: these hits come from spatial locality.
(1) 地址 log₂256 = 8 位,偏移 log₂4 = 2 位,全相联没有索引,标签 = 8 − 2 = 6 位。
(4) 命中 4 次(09、126、44、08),缺失 8 次(强制 7、容量 1:最后一次访问 77),命中率 4/12 = 33.3%。09 与 08 同在块 8–11,126 与 124 同在块 124–127,44 与 45 同在块 44–47,这些命中来自空间局部性。
8.4 直接映射:每个块只有一个家
全相联每次都要比较所有行,大缓存里还要维护 LRU,硬件代价高。直接映射(direct-mapped)让每个内存块只能放在固定的一行:用地址中间几位做索引(index)直接选行。
查找三步:用 index 选中一行 → 比较这一行存的 tag 和地址的 tag → 相同则命中,从行里按 offset 取字节;不同则缺失,从主存取块覆盖这一行。
同一个序列在直接映射下:24(行 10)、20(行 01)、04(行 01,踢掉 20)、12(行 11)、20(行 01,踢掉 04)、44(行 11,踢掉 12)、04(行 01,踢掉 20)、24 命中、44 命中,只有 2 次命中。20 和 04 都映射到行 01,互相把对方踢出去,这就是冲突缺失。
每个块只有一个位置可去,发生冲突时那一行原来的块直接被替换,不用 LRU、FIFO。这也是它比全相联便宜、快的原因之一。
第 3 步表格里 24 那一行的标签写成了 00,应为 01(24 = 01 10 00₂)。另外第 3 步把 04 的缺失标成 conflict,但 04 是第一次被访问,同一页第 6 步的 44 情况完全相同却标成 compulsory。按定义两者都是 compulsory。
直接映射和全相联到底差在哪
两者看起来像:都要存标签、比标签,讲义例题结果也差不多(全相联 3 次命中,直接映射 2 次)。根本区别只有一个:一个块能放在哪里。
| 直接映射 | 全相联 | |
|---|---|---|
| 块能放哪 | 只能放一个固定的行:行号 = 块号 mod 行数 | 任何一行都行 |
| 地址拆分 | Tag | Index | Offset | Tag | Offset(没有 Index) |
| 查找时比较几次 | 用 Index 直接选出那一行,只比 1 次 | 和所有行都比一遍 |
| 满了踢谁 | 不用选,那一行原来的块直接被替换 | 要选,按 LRU 踢最久没用的 |
| 冲突缺失 | 有:别的行空着也不能用 | 没有 |
| 硬件 | 1 个比较器,便宜、快 | 每行一个比较器 + LRU 记录,贵、慢 |
打个比方:直接映射像按学号尾号分配的储物柜,你的东西只能放 3 号柜,3 号柜被人占了就得把他的拿走,哪怕旁边柜子全空着;全相联像随便用哪个空柜子,空间利用最好,但找东西时要把所有柜子都看一遍。
一个差别很大的例子:主存 64 字节、块 4 字节、缓存 4 行,访问 0, 16, 0, 16, 0, 16。地址 0 是块 0,地址 16 是块 4,直接映射下行号都是 0 mod 4 = 4 mod 4 = 0,两块抢同一行。
| 访问 | 直接映射(都只能去第 0 行) | 全相联(随便放) |
|---|---|---|
| 0 | MISS 第 0 行 = 块 0 | MISS 放第 1 行 |
| 16 | MISS 踢掉块 0,第 0 行 = 块 4 | MISS 放第 2 行,块 0 还在 |
| 0 | MISS 踢掉块 4 | HIT |
| 16 | MISS 踢掉块 0 | HIT |
| 0 | MISS | HIT |
| 16 | MISS | HIT |
| 合计 | 0 次命中 | 4 次命中 |
直接映射另外 3 行从头到尾空着,两个块却在第 0 行来回互踢(冲突缺失,见 8.6 节)。两者真正结果一样的情况:访问的块刚好落在不同的行、彼此不冲突时;以及缓存只有 1 行时。两者都是组相联的特例:直接映射是每组 1 行(1 路),全相联是所有行一组。可以在下面的缓存模拟器里亲手试:主存 64、行数 4、块大小 4,序列填 0, 16, 0, 16, 0, 16,切换"直接映射"和"全相联"各运行一次对比。
Same memory and sequence as Exercise 8.1, but a direct-mapped cache with 4 lines and 4-byte blocks (no replacement policy needed). (1) Number of bits for the address, offset, index and tag. (2) Update the cache for each reference. (3) Hit or miss; compulsory or conflict. (4) Total hits, misses and hit rate.
Sequence: 08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241
主存和序列同 Exercise 8.1,改为直接映射缓存,4 行,块 4 字节(不需要替换策略)。(1) 地址、偏移、索引、标签各几位;(2) 每次访问后的缓存内容;(3) 命中还是缺失,缺失是强制还是冲突;(4) 总命中、总缺失和命中率。
序列:08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241
解答 · Solution
| # | Addr 地址 | Tag Index Offset | Block 块 | Set/Line 组/行 | Tag | Result 结果 | Evicted 被替换 | Cache after 访问后缓存 |
|---|---|---|---|---|---|---|---|---|
| 1 | 8 | 0000 10 00 | 8–11 | 2 | 0000 | MISS compulsory | L2: 8–11 | |
| 2 | 124 | 0111 11 00 | 124–127 | 3 | 0111 | MISS compulsory | L2: 8–11 L3: 124–127 | |
| 3 | 77 | 0100 11 01 | 76–79 | 3 | 0100 | MISS compulsory | 124–127 | L2: 8–11 L3: 76–79 |
| 4 | 45 | 0010 11 01 | 44–47 | 3 | 0010 | MISS compulsory | 76–79 | L2: 8–11 L3: 44–47 |
| 5 | 9 | 0000 10 01 | 8–11 | 2 | 0000 | HIT | L2: 8–11 L3: 44–47 | |
| 6 | 126 | 0111 11 10 | 124–127 | 3 | 0111 | MISS conflict | 44–47 | L2: 8–11 L3: 124–127 |
| 7 | 44 | 0010 11 00 | 44–47 | 3 | 0010 | MISS conflict | 124–127 | L2: 8–11 L3: 44–47 |
| 8 | 211 | 1101 00 11 | 208–211 | 0 | 1101 | MISS compulsory | L0: 208–211 L2: 8–11 L3: 44–47 | |
| 9 | 8 | 0000 10 00 | 8–11 | 2 | 0000 | HIT | L0: 208–211 L2: 8–11 L3: 44–47 | |
| 10 | 212 | 1101 01 00 | 212–215 | 1 | 1101 | MISS compulsory | L0: 208–211 L1: 212–215 L2: 8–11 L3: 44–47 | |
| 11 | 77 | 0100 11 01 | 76–79 | 3 | 0100 | MISS conflict * | 44–47 | L0: 208–211 L1: 212–215 L2: 8–11 L3: 76–79 |
| 12 | 241 | 1111 00 01 | 240–243 | 0 | 1111 | MISS compulsory | 208–211 | L0: 240–243 L1: 212–215 L2: 8–11 L3: 76–79 |
(1) Address 8 bits, offset 2 bits, index log₂4 = 2 bits, tag 8 − 2 − 2 = 4 bits. Line = block number mod 4.
(4) 2 hits (09, 08) and 10 misses (7 compulsory, 3 conflict). Hit rate = 2/12 = 16.7%. Blocks 31 (124–127), 19 (76–79) and 11 (44–47) all map to line 3 and keep evicting each other. The last access to 77 (marked *) would also miss in a fully associative cache of the same size (it is a capacity miss in Ex 8.1), so strict 3C calls it capacity; this exercise only asks compulsory vs conflict, so it is recorded as conflict.
(1) 地址 8 位,偏移 2 位,索引 log₂4 = 2 位,标签 8 − 2 − 2 = 4 位。行号 = 块号 mod 4。
(4) 命中 2 次(09、08),缺失 10 次(强制 7、冲突 3),命中率 2/12 = 16.7%。块 31(124–127)、19(76–79)、11(44–47)全都映射到行 3,反复互相踢出。标 * 的最后一次 77:同样大小的全相联缓存也会缺失(Ex 8.1 里它是容量缺失),严格按 3C 应记 capacity;本题只要求区分强制与冲突,记 conflict。
8.5 组相联:两者的折中
k 路组相联(k-way set-associative)把缓存行分成若干组,每组 k 行。索引选中一组,块可以放在组内任意一行,组内满了按 LRU 替换。
讲义例题:4 行的 2 路组相联
把 4 行缓存两两分成一组:组 0 两行、组 1 两行。规则只有两条:每个块只能进一个固定的组(像直接映射);进组之后组里两行随便放,组满了踢组里最久没用的那块(像全相联)。所以说它是两者的折中。
| 字段 | 位数 | 怎么来的 | 作用 |
|---|---|---|---|
| Tag | 3 位(第 5–3 位) | 6 − 1 − 2 | 认出是哪个块 |
| Index | 1 位(第 2 位) | log₂(组数 2) | 去哪个组 |
| Offset | 2 位(第 1–0 位) | log₂(块大小 4) | 块里第几个字节 |
换个算法也一样:块号 = 地址 ÷ 4,组号 = 块号 mod 2,标签 = 块号 ÷ 2。所以有个好用的结论:块号是偶数进组 0,是奇数进组 1。Index 恰好就是块号的最低位。
| 地址 | Tag | Index | Offset | 块 | 块号 | 组 |
|---|---|---|---|---|
| 24 | 011 | 0 | 00 | 24–27 | 6(偶) | 组 0 |
| 20 | 010 | 1 | 00 | 20–23 | 5(奇) | 组 1 |
| 04 | 000 | 1 | 00 | 4–7 | 1(奇) | 组 1 |
| 12 | 001 | 1 | 00 | 12–15 | 3(奇) | 组 1 |
| 44 | 101 | 1 | 00 | 44–47 | 11(奇) | 组 1 |
20、04、12、44 四个块全挤进组 1,而组 1 只有 2 行;只有 24 进组 0。逐步走时,每一步先算组号,只在那个组里找。括号里是"最后一次被用到是第几步",要踢时踢数字小的:
| 步 | 地址 → 块 | 去哪组 | 那组里有它吗? | 结果 | 访问后组 0 | 访问后组 1 |
|---|---|---|---|---|---|---|
| 1 | 24 → 24–27 | 组 0 | 空 | MISS | 24–27(1) | — |
| 2 | 20 → 20–23 | 组 1 | 空 | MISS | 24–27(1) | 20–23(2) |
| 3 | 04 → 4–7 | 组 1 | 没有,还有一行空 | MISS | 24–27(1) | 20–23(2)、4–7(3) |
| 4 | 12 → 12–15 | 组 1 | 没有,组 1 满了 | MISS,踢 20–23 | 24–27(1) | 12–15(4)、4–7(3) |
| 5 | 20 → 20–23 | 组 1 | 没有(第 4 步被踢) | MISS,踢 4–7 | 24–27(1) | 12–15(4)、20–23(5) |
| 6 | 44 → 44–47 | 组 1 | 没有 | MISS,踢 12–15 | 24–27(1) | 44–47(6)、20–23(5) |
| 7 | 04 → 4–7 | 组 1 | 没有(第 5 步被踢) | MISS,踢 20–23 | 24–27(1) | 44–47(6)、4–7(7) |
| 8 | 24 → 24–27 | 组 0 | 有 | HIT | 24–27(8) | 不变 |
| 9 | 44 → 44–47 | 组 1 | 有 | HIT | 不变 | 44–47(9)、4–7(7) |
结果 2 次命中、7 次缺失:5 次是第一次访问(强制缺失),第 5、7 步是冲突缺失。讲义第 64 页把第 4 步(12 第一次被访问)标成 conflict,按定义应算 compulsory,口径问题同 8.3 节。
组 0 从头到尾只用了一行,另一行一直空着;组 1 那边四个块抢两个座位,不停互相踢。它们不能去组 0 坐,因为组号由地址决定,没得选。这正是冲突缺失:缓存其实还有空位,只是映射规则不让用(8.6 节详细讲)。同一序列下全相联命中 3 次(第 7 步的 04 还在缓存里),2 路和直接映射都只有 2 次。这个序列恰好不利于 2 路;一般情况下提高相联度能减少冲突缺失。
硬件上怎么判断命中:用 Index 位选出一组(例如 20 的 Index = 1,选组 1);把地址的 Tag 和组内两行存的 Tag 同时比较,需要 2 个比较器(直接映射 1 个,全相联 4 个);有一行相同且有效位为 1 就命中。缺失时每组用 1 个 LRU 位记住哪一路最近用过,决定踢谁。
一般来说,每组放几块?
要分清两件事:每组最多同时装几块是硬件定死的;主存里哪些块会被分到这一组由地址决定。
- 每组最多同时装 k 块,固定不变。k 路组相联每组有 k 行。本例 4 行 2 路:组数 = 4 ÷ 2 = 2,组 0、组 1 各 2 行,各自最多同时装 2 块。组与组之间不能互相借位。
- 主存的块平均分给各组,而且交替排列。组号 = 块号 mod 组数。主存 64 字节共 16 块,偶数块归组 0,奇数块归组 1,每组"负责" 8 个块,但同一时刻只有 2 个座位。
- 某次运行里各组实际装了几块,看访问序列。本例 1 块去组 0、4 块去组 1,所以组 1 挤得不停踢;如果顺序访问 0, 4, 8, 12, 16…,块号奇偶交替,两组会用得差不多平均。
| 组 | 归它的块号 | 对应地址 |
|---|---|---|
| 组 0 | 0, 2, 4, 6, 8, 10, 12, 14(偶数) | 0–3、8–11、16–19、24–27、32–35、40–43、48–51、56–59 |
| 组 1 | 1, 3, 5, 7, 9, 11, 13, 15(奇数) | 4–7、12–15、20–23、28–31、36–39、44–47、52–55、60–63 |
| 参数 | 组数 = 行数 ÷ k | 每组行数 = k | 每组负责的主存块数 |
|---|---|---|---|
| 讲义:64 B 主存,4 行,2 路,4 B 块 | 2 | 2 | 16 ÷ 2 = 8 |
| Tutorial 8.8:256 B 主存,8 行,2 路,4 B 块 | 4 | 2 | 64 ÷ 4 = 16 |
| Exercise 8.3:256 B 主存,4 行,2 路,4 B 块 | 2 | 2 | 64 ÷ 2 = 32 |
Tutorial 8.8 有 4 组,组号是块号除以 4 的余数,不再是看奇偶。那题的块号 5、9、13、17 除以 4 都余 1,全进了组 1,和本例是同一种"挤在一组"的情况。
| 方式 | 组数 × 每组行数 | 地址划分 | 块能去哪 |
|---|---|---|---|
| 全相联 | 1 × 4 | Tag 4 | Offset 2 | 任意一行 |
| 2 路组相联 | 2 × 2 | Tag 3 | Index 1 | Offset 2 | 指定组里的任意一行 |
| 直接映射 | 4 × 1 | Tag 2 | Index 2 | Offset 2 | 唯一一行 |
Same memory and sequence again, with a 2-way set-associative cache of 4 lines, 4-byte blocks and LRU replacement. (1) Bits for the address, offset, index and tag. (2) Update the cache (tag and time since last referenced). (3) Hit or miss; compulsory or conflict. (4) Total hits, misses and hit rate.
Sequence: 08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241
主存和序列不变,改为 2 路组相联,4 行,块 4 字节,LRU。(1) 地址、偏移、索引、标签各几位;(2) 每次访问后的缓存内容(标签与 TSLR);(3) 命中还是缺失,强制还是冲突;(4) 总命中、总缺失和命中率。
序列:08, 124, 77, 45, 09, 126, 44, 211, 08, 212, 77, 241
解答 · Solution
| # | Addr 地址 | Tag Index Offset | Block 块 | Set/Line 组/行 | Tag | Result 结果 | Evicted 被替换 | Cache after 访问后缓存 (TSLR) |
|---|---|---|---|---|---|---|---|---|
| 1 | 8 | 00001 0 00 | 8–11 | 0 | 00001 | MISS compulsory | S0: 8–11(0) | |
| 2 | 124 | 01111 1 00 | 124–127 | 1 | 01111 | MISS compulsory | S0: 8–11(1) S1: 124–127(0) | |
| 3 | 77 | 01001 1 01 | 76–79 | 1 | 01001 | MISS compulsory | S0: 8–11(2) S1: 124–127(1), 76–79(0) | |
| 4 | 45 | 00101 1 01 | 44–47 | 1 | 00101 | MISS compulsory | 124–127 | S0: 8–11(3) S1: 44–47(0), 76–79(1) |
| 5 | 9 | 00001 0 01 | 8–11 | 0 | 00001 | HIT | S0: 8–11(0) S1: 44–47(1), 76–79(2) | |
| 6 | 126 | 01111 1 10 | 124–127 | 1 | 01111 | MISS conflict | 76–79 | S0: 8–11(1) S1: 44–47(2), 124–127(0) |
| 7 | 44 | 00101 1 00 | 44–47 | 1 | 00101 | HIT | S0: 8–11(2) S1: 44–47(0), 124–127(1) | |
| 8 | 211 | 11010 0 11 | 208–211 | 0 | 11010 | MISS compulsory | S0: 8–11(3), 208–211(0) S1: 44–47(1), 124–127(2) | |
| 9 | 8 | 00001 0 00 | 8–11 | 0 | 00001 | HIT | S0: 8–11(0), 208–211(1) S1: 44–47(2), 124–127(3) | |
| 10 | 212 | 11010 1 00 | 212–215 | 1 | 11010 | MISS compulsory | 124–127 | S0: 8–11(1), 208–211(2) S1: 44–47(3), 212–215(0) |
| 11 | 77 | 01001 1 01 | 76–79 | 1 | 01001 | MISS conflict * | 44–47 | S0: 8–11(2), 208–211(3) S1: 76–79(0), 212–215(1) |
| 12 | 241 | 11110 0 01 | 240–243 | 0 | 11110 | MISS compulsory | 208–211 | S0: 8–11(3), 240–243(0) S1: 76–79(1), 212–215(2) |
(1) 4 lines ÷ 2 ways = 2 sets: address 8 bits, offset 2 bits, index 1 bit, tag 8 − 1 − 2 = 5 bits. Set = block number mod 2.
(4) 3 hits (09, 44, 08) and 9 misses (7 compulsory, 2 conflict). Hit rate = 3/12 = 25%. Comparing the three organizations: fully associative 33.3% > 2-way 25% > direct-mapped 16.7%; higher associativity removes conflict misses.
(1) 4 行 ÷ 2 路 = 2 组:地址 8 位,偏移 2 位,索引 1 位,标签 8 − 1 − 2 = 5 位。组号 = 块号 mod 2。
(4) 命中 3 次(09、44、08),缺失 9 次(强制 7、冲突 2),命中率 3/12 = 25%。三种组织对比:全相联 33.3% > 2 路 25% > 直接映射 16.7%,相联度越高,冲突缺失越少。
8.6 3C 缺失分类
| 类型 | 原因 | 典型场景 | 怎么减少 |
|---|---|---|---|
| 强制缺失 Compulsory / Cold | 块第一次被访问,从没进过缓存 | 程序刚开始运行 | 预取;增大块(效果有限) |
| 冲突缺失 Conflict | 多个块争同一组/同一行,缓存其实还有空位 | 直接映射里 A、B 反复互踢 | 提高相联度(2 路、4 路) |
| 容量缺失 Capacity | 工作集比缓存大,全相联也装不下 | 1 MB 缓存反复访问 10 MB 数据 | 增大缓存;加一级缓存 |
冲突缺失到底是什么意思
缓存其实装得下,但映射规则规定这个块只能去某一行(或某一组),偏偏那里被别的块占了,只好把它踢出去;后来再访问被踢掉的块,就缺失了。一句话:不是缓存太小,是座位分配规则太死。
停车场 4 个车位,规定单号车只能停 A 区(2 个位),双号车只能停 B 区(2 个位)。连续来了 4 辆单号车:A 区两个位被反复争夺,后来的把先停的赶走;B 区一直空着一个位,但单号车不准停过去。被赶走的车再回来又找不到位,这就是冲突。如果改成"随便停"(全相联),4 辆车都停得下,就不会互相赶。
8.5 节的 2 路组相联例子里,第 5、7 步就是冲突缺失:第 4 步 12 进组 1 时把 20–23 踢掉,第 5 步再访问 20 就 MISS;第 5 步又把 4–7 踢掉,第 7 步访问 04 也 MISS。这两次缺失时组 0 都还空着一行,总容量是够的。
判断方法,按顺序问两步:
- 这个块是第一次被访问吗?是 → 强制缺失,不管它有没有引起替换。
- 不是第一次的话,换成同样行数的全相联缓存(也用 LRU)跑同一个序列,这一步会命中吗?会命中 → 冲突缺失(全相联装得下,是映射规则害的);也缺失 → 容量缺失(缓存本身太小,怎么放都装不下)。
验证:全相联在第 4 步后装着 24–27、20–23、4–7、12–15,第 5 步访问 20 会命中,所以 2 路这里的缺失是冲突缺失。反过来,全相联自己第 8 步访问 24 缺失,它没有映射限制,只能怪 4 行装不下,所以是容量缺失。标准 3C 模型里每次缺失只归入一类。冲突缺失只出现在直接映射和组相联里,全相联没有冲突缺失;这也是提高相联度能降低缺失率的原因。
讲义的直接映射/组相联练习(Exercise 8.2、8.3,Tutorial 8.8)只让你在 compulsory 和 conflict 之间选:凡是"以前访问过、后来被同一行/同一组的别的块踢掉"的缺失,都写 conflict。缓存模拟器里标 * 的少数几次,严格按上面第 2 步算是容量缺失,答题时按题目给的两个选项写即可。
| 问题 | 冲突缺失 | 容量缺失 |
|---|---|---|
| 换成全相联有用吗? | 有用 | 没用 |
| 加大缓存有用吗? | 有时有用 | 有用 |
| 根本原因 | 映射碰撞 | 缓存太小 |
8.7 比较器的硬件代价
判断命中靠等值比较器:把存储的标签和地址标签逐位异或(XOR),全部为 0 且有效位为 1 才算命中。一个 T 位比较器需要 T 个 XOR("位比较")。
| 组织方式 | 比较器个数 | 每个比较器宽度 | 同时进行的位比较总数 |
|---|---|---|---|
| 直接映射 | 1 | T | T × 1 |
| k 路组相联 | k | T | T × k |
| 全相联(C 行) | C | T | T × C |
讲义例子:4 位标签、8 行。直接映射 1 个比较器、4 次位比较;全相联 8 个比较器、32 次位比较。注意同一个缓存换组织方式时 T 也会变(全相联没有索引位,标签最长),Tutorial 8.7 则直接给定了 T = 6。
- 直接映射:硬件简单、面积小、访问快(索引直接定位,数据可以和标签比较并行读出);但冲突缺失多。
- 全相联:没有冲突缺失,命中率通常最高;但比较器多、功耗大、面积大,访问时间可能更长。
- 同样的芯片面积:全相联可能命中率更高,直接映射可以做得更大。组相联是两者的折中。
选好组织方式和参数,输入访问序列,然后一步一步执行。每一步都会显示地址怎么拆分、落在哪一组、是否命中、属于哪类缺失,以及缓存当前的内容。预设里有讲义例题、Exercise 8.1–8.3 和 Tutorial 8.8 / 9.1 / 9.6。
| # | 地址 | 二进制拆分 | 块 | 组 | Tag | 结果 | 替换 |
|---|
分类规则:第一次访问的块记 compulsory;全相联里的其余缺失记 capacity;直接映射/组相联里的其余缺失记 conflict。标 * 的表示同样大小的全相联 LRU 缓存也会缺失,按严格 3C 应记 capacity(讲义的直接映射/组相联练习只要求区分 compulsory 和 conflict)。TSLR 每访问一次全体有效行加 1,被访问的行清零。
自测 · Self-Check
主存 256 字节、块 4 字节、缓存 8 行。全相联、直接映射、2 路组相联的地址分别怎么划分?
直接映射里,地址 37 应该放进哪一行?标签是多少?(主存 256 B、块 4 B、8 行)
为什么直接映射不需要 LRU?
A、B 两个块在直接映射缓存里反复互踢。这是什么缺失?换成全相联还会发生吗?
6 位标签、16 行缓存。直接映射和全相联各需要多少个比较器、多少次位比较?
缓存 II:块大小、写策略与虚拟内存
Quiz 2 范围(9.7 不考)Lesson 8 解决了"块放哪、怎么找"。这一课补上剩下的设计问题:缓存和块该做多大、相联度该取几、写操作怎么保证主存拿到正确的值、缺失代价怎么随 CPU 变快而放大、多级缓存怎么算。最后把同一套思路搬到主存和磁盘之间,得到虚拟内存和 TLB。
9.1 减少缺失:容量与相联度
- 强制缺失:第一次访问,躲不掉;程序跑几百万条指令时,这部分可以忽略。
- 冲突缺失:办法 1 是加大缓存(每行被更少的地址共享),办法 2 是提高相联度(每组多几行)。
- 容量缺失:缓存装不下程序用到的所有块(例如缓存 64 KB、程序 64 MB),只能加大缓存。
讲义的统计图给了两条经验:缺失率随缓存变大和相联度变高而下降;4 路组相联的缺失率约等于两倍大的直接映射缓存。从直接映射到 2 路收益最大,在很多情况下缺失率能降 20% 以上;再往 4 路、8 路走,收益越来越小,而硬件复杂度和访问时间却在增加。
9.2 块大小的权衡
| 大块的好处 | 大块的坏处 |
|---|---|
| 更好地利用空间局部性,减少强制缺失 | 缓存容量固定时块数变少,冲突/容量缺失增加(还会削弱时间局部性) |
| 同样容量下需要的标签更少 | 每次缺失要传更多字节,缺失代价变大 |
| 块太大时,取回来的很多字节根本用不上,浪费空间和带宽 |
讲义演示:2 组、8 字节块的直接映射缓存,访问 24 时整个 24–31 块进入缓存,随后访问 28 直接命中,尽管 28 从没被访问过。这就是块大小带来的空间局部性收益。Tutorial 9.6 用一个只有一块的缓存把这个效果量化:块 8 B 时缺失率 50%,16 B 时 37.5%,32 B 时 25%。
9.3 地址位划分通用方法
缓存由三个参数决定:组数、相联度(每组块数)、块大小。
块数怎么算:容量 ÷ 块大小
块数就是缓存一共能放多少个块,也就是缓存有多少行。关键是先把两个数换成同一单位(字节),再写成 2 的幂。以 Exercise 9.1(32 KB 缓存、256 B 块)为例:
- 把 32 KB 换成字节:1 KB = 1024 = 2¹⁰,32 = 2⁵,所以 32 KB = 2⁵ × 2¹⁰ = 2¹⁵ = 32768 字节。
- 块大小写成 2 的幂:256 = 2⁸。
- 2 的幂相除就是指数相减:2¹⁵ ÷ 2⁸ = 2⁷ = 128 块。直观地说,32768 字节的数据区切成每份 256 字节,正好 128 份,每份一行、放一个块。
后面几步都从这个 128 出发:Offset = log₂256 = 8(一块 256 字节,编号 0–255 要 8 位);组数 = 128 ÷ 相联度(直接映射 128、4 路 32、全相联 1);Index = 7、5、0;Tag = 32 − Index − 8 = 17、19、24。
| 写法 | 2 的幂 | 字节数 |
|---|---|---|
| 1 KB | 2¹⁰ | 1024 |
| 32 KB | 2¹⁵ | 32768 |
| 64 KB | 2¹⁶ | 65536 |
| 256 KB | 2¹⁸ | 262144 |
| 1 MB | 2²⁰ | 1048576 |
| 4 GB | 2³² | — |
有了这张表,"容量 ÷ 块大小"就是指数相减:64 KB ÷ 64 B = 2¹⁶⁻⁶ = 1024 块(T9.5);256 KB ÷ 64 B = 2¹⁸⁻⁶ = 4096 块(T8.5)。
- KB 是 1024,不是 1000:算成 32000 ÷ 256 就除不尽了。
- 块大小给的是"字"时先换成字节:T8.2 的"8-word blocks",1 字 = 4 字节,块是 32 B,不是 8 B。
- 这里算的是缓存里的块,不是主存里的块:主存块数 = 2³² ÷ 2⁸ = 2²⁴。这正好说明全相联的标签为什么是 24 位:没有索引,标签要能区分主存里所有 2²⁴ 个块。
Offset 选块内字节,Index 选组,Tag 确认组里存的是不是要找的块。讲义两个例子(32 位地址、64 KB、32 B 块):
| 组织 | 块数 | 组数 | Offset | Index | Tag | 比较器 |
|---|---|---|---|---|---|---|
| 直接映射 | 2¹¹ = 2048 | 2048 | 5 | 11 | 16 | 1 个 16 位 |
| 2 路组相联 | 2048 | 2¹⁰ = 1024 | 5 | 10 | 17 | 2 个 17 位 |
容量和块大小不变时,相联度每翻一倍,组数减半,Index 少 1 位,Tag 多 1 位;全相联时 Index = 0,Tag 最长。标签总存储 = 块数 × (Tag 位 + 有效位等附加位)。2 路组相联每组还需要 1 个 LRU 位(讲义图里没画)。
Tutorial 8.4(b)(c) 是反过来给 Tag 求别的量:已知 32 位地址、直接映射 32 块、Tag 21 位 → Index 5 位 → Offset = 32 − 21 − 5 = 6 位 → 块 64 B → 容量 2 KB。已知容量 256 B、Tag 28 位、块 16 B → Offset 4 → Index = 32 − 28 − 4 = 0 → 只有 1 组 → 16 块全在一组,即 16 路(全相联)。
A cache has a capacity of 32 KB and a block size of 256 bytes, with 32-bit addresses. Determine the number of tag, index and block-offset bits for (1) a direct-mapped cache, (2) a four-way set-associative cache, (3) a fully associative cache.
缓存容量 32 KB,块 256 字节,32 位地址。分别求 (1) 直接映射;(2) 4 路组相联;(3) 全相联时的标签、索引、块内偏移位数。
解答 · Solution
Pattern: with capacity and block size fixed, doubling the associativity removes one index bit and adds one tag bit.
规律:容量和块大小不变时,相联度每翻一倍,索引少 1 位、标签多 1 位。
默认数据来自 Tutorial 9.7:32 位虚拟地址、30 位物理地址、4 KB 页。页表可以直接编辑,每行写"VPN PPN 有效位"(十六进制)。
9.4 写策略:主存怎么拿到正确的值
写和读不一样:写不一定要让 CPU 等(可以用写缓冲),但写会修改缓存,于是缓存和主存可能暂时存着同一数据的不同版本;有些 I/O 设备(DMA)还会绕过缓存直接读主存。四种机制:
| 机制 | 做法 | 特点 |
|---|---|---|
| 写直达 Write-through | 每次写同时更新缓存和主存 | 主存永远最新;简单,但内存流量大 |
| 写回 Write-back / copy-back | 只写缓存,被改过的块打上脏位(dirty bit);块被替换时才写回主存 | 流量小、性能高;需要管理脏位,控制更复杂 |
| 写缓冲 | 把要写主存的数据先放进缓冲区,CPU 继续执行 | 常和写直达搭配;缓冲满了 CPU 仍要等 |
| 缓存一致性 | 多核下用 MESI 等协议保证各缓存与主存一致 | 超出本课程范围 |
为什么"写"会带来麻烦
读只是把数据复制一份到缓存,缓存和主存内容一样,不会出问题。写会改值:缓存里有块 100–103 = A B C D,主存也是 A B C D;CPU 把 X 写到 101,缓存那份变成 A X C D,主存那份改不改?不改的话,同一个地址在两处就是两个不同的值。主存的值在这几种时候要紧:
- 块被踢出缓存时:缓存里的新值若没写回主存,就永远丢了。
- I/O 设备(DMA)直接读主存时:它绕过缓存,会读到旧值。
- 多核时:别的核读到的也可能是旧值。
写策略要解决的就是"什么时候、怎么把新值交给主存"。
写命中:写直达 vs 写回,逐步对比
"写命中"指要写的块已经在缓存里。依次执行 Store X → 101、Store Y → 102、Store Z → 101,最后这个块被替换出缓存(Tutorial 9.4):
| 步骤 | 写直达:缓存 | 写直达:主存 | 写回:缓存 | 写回:主存 | 脏位 |
|---|---|---|---|---|---|
| 初始 | A B C D | A B C D | A B C D | A B C D | 0 |
| Store X → 101 | A X C D | A X C D | A X C D | A B C D | 1 |
| Store Y → 102 | A X Y D | A X Y D | A X Y D | A B C D | 1 |
| Store Z → 101 | A Z Y D | A Z Y D | A Z Y D | A B C D | 1 |
| 块被替换 | 直接丢掉 | 已是最新 | 脏位为 1,整块写回 | A Z Y D | — |
| 主存一共被写 | 3 次 | 1 次 | |||
- 写直达(write-through):每次写,缓存和主存同时更新。好处是主存永远最新、简单可靠,块被踢时直接丢掉;坏处是每次写都要访问慢速的主存,流量大。上例写了 3 次主存,第一次写的 X 后来还被 Z 覆盖,等于白写。
- 写回(write-back):写的时候只改缓存,等这个块被踢出缓存时才一次性写回主存。好处是同一块的多次写合并成一次,流量小、速度快(上例 X 根本没写到主存);坏处是主存有一段时间是旧的,控制更复杂,踢块时若要先写回会多花时间。
脏位(dirty bit)是什么
脏位是缓存每一行上额外的 1 个比特,标记"这一行被改过没有"。"脏"的意思是这份副本已经被改动,和主存里的原件不一样了。
| 脏位 | 意思 | 这一行被踢出时 |
|---|---|---|
| 0(clean) | 缓存里这份和主存那份一模一样 | 直接丢掉,什么都不用做 |
| 1(dirty) | 这一行在缓存里被写过,主存那份已经是旧的 | 必须先把整块写回主存,再丢掉 |
- 每一行(每个块)1 位,不是每个字节 1 位。块里哪怕只改了一个字节,整行就算脏,写回时整块一起写。
- 什么时候变:块从主存搬进缓存时 D = 0;对这一行写(store)时 D = 1;只读不改变 D;被踢出时看 D 决定要不要写回。
- 只有写回策略才需要脏位:写回在写时不改主存,必须记住哪些行的主存副本已经过期。只读过的块 D 一直是 0,踢出时不用白写一次主存,这也是写回省流量的地方。写直达每次都同时改主存,行永远是"干净"的,不需要脏位。
- T9.4(c) 问每次操作后的脏位:初始 0,三次写之后都是 1、1、1,直到块被写回、踢出。
写缓冲:让写直达不那么慢
写和读不一样:读时 CPU 必须拿到数据才能继续算;写时 CPU 不需要拿回任何东西。所以可以加一个小队列:CPU 把"地址 + 数据"往缓冲里一放就继续执行,缓冲在后台慢慢写进主存,好比把信投进邮筒就走,不用等邮递员送到。只有缓冲满了(写得太快,主存来不及消化),CPU 才要停下来等。写缓冲通常和写直达搭配。
缓存一致性是多核的问题:每个核有自己的缓存,核 1 改了 x,核 2 缓存里还是旧的 x,需要 MESI(Modified / Exclusive / Shared / Invalid)这类协议来通知和作废旧副本。讲义说超出本课程范围,知道名字即可。
写缺失时怎么办
写缺失:要写的地址所在的块不在缓存里。例如执行 Store X → 205,块 204–207 不在缓存里,有两种处理:
| 写分配 write allocate / fetch-on-write | 不写分配 no-write allocate / write-around | |
|---|---|---|
| 做法 | 先把块 204–207 从主存搬进缓存(和读缺失一样),再在缓存里写 X | 直接写主存的 205,缓存不动 |
| 想法 | 刚写过的地方很可能马上还要读写(局部性),搬进来后面就能命中 | 有些数据写完就不再用(例如初始化一个大数组),搬进缓存反而挤掉有用的块 |
| 之后再读 205 | 命中 | 缺失,要去主存取 |
| 通常搭配 | 写回:块进了缓存,之后的多次写都只改缓存,最后一起写回 | 写直达:反正每次写都要写主存,没必要再把块搬进缓存 |
| 组合 | 写命中 | 写缺失 | 特点 |
|---|---|---|---|
| 写回 + 写分配 | 只写缓存,脏位置 1 | 先搬块进缓存,再写缓存 | 性能高,现代缓存主流 |
| 写直达 + 不写分配 | 缓存和主存都写 | 只写主存 | 简单,主存永远最新,通常配写缓冲 |
数主存被写了几次:写直达 = 写的次数;写回 = 被踢出时脏块的个数。
写回的优点:同一块的多次写合并成一次写回,内存流量和带宽占用小;写操作以缓存速度完成,性能高、功耗低。写直达的优点:主存始终是最新的,一致性简单(DMA 读主存不会读到旧值);不需要脏位;替换块时不用先等写回。趋势是现代缓存多用写回。
缓存里已经有块 100–103 = A B C D,主存同样是 A B C D。依次执行:Store X → 101,Store Y → 102,Store Z → 101,最后这个块被替换出去。对比两种策略下缓存、主存和脏位的变化,以及一共写了几次主存。
9.5 缺失代价被 CPU 放大
改进缓存性能有三个方向:降低命中时间、降低缺失率、降低缺失代价。讲义用一个例子说明为什么 CPU 越快,缓存越重要。
| CPI_ideal | 总 CPI | 停顿占比 |
|---|---|---|
| 2 | 5.44 | 63% |
| 1 | 4.44 | 77% |
| 0.5 | 3.94 | 87% |
| 0.25 | 3.69 | 93% |
超标量处理器每拍可以完成多条指令,CPI_ideal 可以小于 1。另一种情况是时钟频率翻倍而内存不变:同样 50 ns 的主存访问现在要花两倍的周期数,MP 从 100 变成 200,停顿变成 4 + 2.88 = 6.88,CPI = 8.88。内存延迟本身没变,但 CPU 每次缺失等待的周期数翻倍。
比较两种缓存时不能只看 AMAT。2 路组相联的缺失率低、AMAT 也更低(2.90 ns vs 2.98 ns),但它让时钟周期变长 10%,而时钟周期影响的是每条指令的全部周期,不只是访存。算 CPU 时间:直接映射 IC × (2.0 × 2 + 1.3 × 0.014 × 70) = 5.274·IC ns;2 路 IC × (2.0 × 2.2 + 1.3 × 0.010 × 70) = 5.31·IC ns,反而是直接映射更快。
9.6 多级缓存 公式不考
第 9 周老师讲完两级缓存的例子后说,多级缓存的公式只是给大家参考,保证 quiz 和 final 都不考。只要知道"L1 缺失时先去 L2,比直接去主存代价小得多,所以加 L2 能降低停顿"就够了。下面的内容可以快速浏览。
只有一级缓存时,L1 缺失就得直接去主存,一次要等 100 个周期;L1 又不能做大,做大了就变慢。办法是在 L1 和主存之间再加一层更大一点、稍慢一点的 L2(芯片面积够时还有 L3):
- L1 缺失先找 L2:L2 有,只付 L2 的时间(比如 19 或 25 周期),不用去主存;L2 也没有,才去主存。
- L2 通常是统一缓存:指令和数据共用一个 L2;L1 则分成指令缓存和数据缓存两个。
下面所有公式都从一个办法推出来:数一数一共多少次访问、每次花多少时间,加起来再除以访问次数。
用 1000 次访问数一遍(Tutorial 9.3 的数据)
L1 命中 1 周期、缺失率 5%;L2 命中 19 周期、缺失率 2%;从主存取一块 50 周期。CPU 一共发出 N = 1000 次访问:
| 走到哪一层 | 次数 | 每次多花 | 合计 |
|---|---|---|---|
| 所有访问都先查 L1 | 1000 | 1 | 1000 |
| L1 缺失 → 去 L2 | 1000 × 5% = 50 | 19 | 950 |
| L2 也缺失 → 去主存 | 50 × 2% = 1 | 50 | 50 |
| 总时间 | 2000 周期 |
平均每次访问 = 2000 ÷ 1000 = 2 周期,这就是 AMAT。把这张表写成公式:
括号里正好是 L2 自己的 AMAT:L1 一旦缺失,平均还要再花 20 个周期才能拿到数据。所以"L1 的缺失代价就是 L2 的平均访问时间"。公式和一级缓存完全一样,只是 L1 的缺失代价不再是主存时间。这就是 Tutorial 9.3(a)(b):AMATL2 = 20,AMATL1 = 2。
局部缺失率 vs 全局缺失率
还是那 1000 次访问:L1 缺失 50 次,L2 缺失 1 次。
| 怎么算 | L1 | L2 | |
|---|---|---|---|
| 局部缺失率 | 本级缺失次数 ÷ 到达本级的访问次数 | 50 / 1000 = 5% | 1 / 50 = 2% |
| 全局缺失率 | 本级缺失次数 ÷ CPU 发出的总访问次数 | 50 / 1000 = 5% | 1 / 1000 = 0.1% |
- 对 L1 两者一样,因为所有访问都先到 L1;对 L2 差别很大,L2 只看得到 L1 漏下来的那 50 次。
- 全局 = 局部1 × 局部2 = 5% × 2% = 0.1%(讲义写作 n1/N × n2/n1 = n2/N):CPU 的所有访问里,最终有多少要跑到主存。
- L2 的局部缺失率常常看起来不低,因为它接到的都是 L1 搞不定的"难"访问,不代表 L2 没用。衡量 L2 的效果要看全局缺失率。
讲义的 CPI 例子,逐项拆开
接着 9.5 节:理想 CPI 2,load/store 36%,L1 指令缓存缺失率 2%、数据缓存 4%。L1 缺失后去 L2 要 25 周期;加了 L2 之后,只有 0.5% 的访问最终去主存(全局缺失率),去主存 100 周期。每次访存的平均停顿分两部分:
| 来源 | 去 L2 的停顿 | 去主存的停顿 |
|---|---|---|
| 取指(每条 1 次) | 1 × 2% × 25 = 0.5 | 1 × 0.5% × 100 = 0.5 |
| 取数(每条 0.36 次) | 0.36 × 4% × 25 = 0.36 | 0.36 × 0.5% × 100 = 0.18 |
| 小计 | 0.86 | 0.68 |
省在哪里:原来每次 L1 缺失都要等 100 周期;现在大部分 L1 缺失在 L2 就解决了,只付 25 周期,只有 0.5% 的访问还要付 100 周期。
| 题目给的 | 怎么用 |
|---|---|
| L2 的全局缺失率(讲义这个例子:0.5%) | 直接乘每条指令的访存次数和主存时间 |
| L2 的局部缺失率(Tutorial 9.3:2%) | 先乘 L1 缺失率,或用嵌套公式从 L2 往上逐级算 AMAT |
Tutorial 9.3(c):理想 CPI 2.0,25% 是 load/store,题目只讲数据缓存。每次数据访问的停顿 = MRL1 × MPL1 = 5% × 20 = 1 周期(也就是 AMATL1 − HTL1 = 2 − 1,命中时间已经算在理想 CPI 里了);每条指令停顿 = 0.25 × 1 = 0.25,CPI = 2.25。
| L1 | L2 | |
|---|---|---|
| 总容量 | 16–64 KB | 500–8000 KB |
| 块大小 | 32–64 B | 32–128 B |
| 缺失代价 | 10–25 周期 | 100–1000 周期 |
| 缺失率 | 2%–5% | 0.1%–2%(全局) |
读法:越往下越大(L2 比 L1 大十几到上百倍)、越慢、块越大;L1 的缺失代价是去 L2 的时间,L2 的缺失代价是去主存的时间;L2 一栏写的是全局缺失率,所以数字很小。
9.7 虚拟内存与 TLB 不考
第 9 周老师说时间不够,虚拟内存只录一个视频放到 NTULearn,quiz 和 final 都不考,mock quiz 里的虚拟内存题也会删掉。这一节和地址翻译动画留着,有兴趣再看;复习时间紧可以直接跳到 Lesson 10。
先抓住一句话:程序用的地址是"假的"(虚拟地址),每次访存前都要翻译成"真的"(物理地址)。TLB 是为了让这个翻译变快而加的小缓存。
为什么需要虚拟内存
电脑里同时跑着几十个程序,共用一块物理内存。如果每个程序直接用物理地址:程序得知道自己被放在内存哪里,两个程序都想用地址 1000 就打架;程序比内存大就没法运行;一个程序写错地址,可能改坏别的程序甚至操作系统。
虚拟内存是一层间接映射:每个程序都以为自己独占一整片从 0 开始的地址空间(32 位就是 4 GB),程序里写的都是虚拟地址,由硬件和操作系统偷偷翻译成物理地址。讲义列的三个问题就这样解决了:
| 问题 | 怎么解决 |
|---|---|
| 程序比物理内存大 | 暂时不用的部分放在磁盘上,用到时再调进内存,造成"内存无限大"的假象。主存相当于磁盘的缓存,靠的仍是局部性 |
| 空闲内存碎片化 | 每个程序各有一套翻译表,程序的各块可以分散放在主存的不同位置,零散的空闲内存也能用上 |
| 安全与共享 | 程序只能访问自己翻译表里有的地址,看不到别人的私有数据;需要共享时让两张表指向同一块物理内存 |
分页:按"页"来翻译
一个字节一个字节地翻译太费,所以把内存切成固定大小的页(page),常见是 4 KB。翻译只管"哪一页",页内第几个字节原样保留:
Tutorial 9.7:32 位虚拟地址、30 位物理地址、4 KB 页 → 偏移 log₂4096 = 12 位,VPN = 32 − 12 = 20 位,PPN = 30 − 12 = 18 位。
为什么要有 VPN 和 PPN 两个编号
一句话:VPN 是"程序以为的页号",PPN 是"这一页在内存条上实际的位置",页表负责把前者换成后者。
| VPN(虚拟页号) | PPN(物理页号) | |
|---|---|---|
| 是谁的编号 | 程序自己给自己的页编的号:0、1、2……连续排下去 | 物理内存里每个页框的编号 |
| 从哪来 | 程序发出的虚拟地址的高位 | 查页表(或 TLB)得到 |
| 用来做什么 | 当作"查询键":去页表找第 VPN 项(在 TLB 里当标签比较) | 和偏移拼成物理地址,真正拿去访问缓存和内存 |
| 每个程序都一样吗 | 每个程序都有自己的 VPN 0、1、2…… | 全机器只有一套,所有程序共用 |
为什么不能只用一个编号?因为"程序以为的位置"和"实际放的位置"本来就不一样,而且必须允许它们不一样:
| 程序 | 它的 VPN | 实际放在 PPN |
|---|---|---|
| 程序 A | 0 | 7 |
| 程序 A | 1 | 2 |
| 程序 A | 2 | 不在内存(在磁盘,有效位 0) |
| 程序 B | 0 | 5 |
| 程序 B | 1 | 3 |
- 程序不用管自己被放在哪:A、B 都从第 0 页开始写代码,操作系统放到哪都行,A 的连续几页在内存里可以东一块西一块(7、2)。
- 同样的 VPN 互不干扰:A 的第 0 页和 B 的第 0 页翻译到不同的 PPN(7 和 5),彼此看不到对方,这就是隔离和保护。
- 不是每一页都要在内存里:A 的第 2 页可以暂时在磁盘上,用到时才调进来。
如果程序直接用物理页号,这三点都做不到。打个比方:订酒店时确认单写"你的第 1 间房"(VPN),每个客人都从 1 开始编;前台的分配表(页表)写着"张三的第 1 间 = 512 号房",512 是真实房号(PPN);房间里的床位号就像页内偏移,不需要翻译。
虚拟地址位数、物理地址位数和页的关系
关键只有一点:虚拟页和物理页一样大,所以两边的偏移位数相同,虚拟地址和物理地址的位数差全部落在页号上。
| 由什么决定 | Tutorial 9.7 | 含义 | |
|---|---|---|---|
| 虚拟地址位数 | CPU 给程序用多宽的地址 | 32 位 | 每个程序"以为"自己有 2³² = 4 GB 地址空间 |
| 物理地址位数 | 机器实际能装多少内存 | 30 位 | 真正的物理内存最多 2³⁰ = 1 GB |
| 页大小 | 操作系统和硬件约定 | 4 KB | 虚拟页、物理页都是 4 KB |
物理页只有虚拟页的 1/4:一个程序的 4 GB 空间如果全用满,最多只有 1/4 能同时待在内存里,其余只能放在磁盘上,用到时再调进来。这就是有些页表项有效位为 0、会发生页缺失的根本原因:虚拟空间比物理内存大,装不下。这也说明了页表为什么在主存里:每个程序的页表有 2²⁰ 项,按每项 4 字节算就要 4 MB,芯片里放不下;每次都去主存查又太慢,所以要有 TLB。
做题步骤:① 偏移 = log₂(页大小),两边相同;② VPN = 虚拟地址位 − 偏移,PPN = 物理地址位 − 偏移;③ 翻译时把 VPN 换成 PPN、偏移照抄,结果自然是物理地址的位数。
页表与地址翻译
页表(page table)存放在主存里,每个程序一张,每一项记录一个虚拟页。真实的页表项还有脏位、引用位(用来近似 LRU)和访问权限(只读、可写等)。Tutorial 9.7 的页表:
| VPN | PPN | 有效位 | 说明 |
|---|---|---|---|
| 0x12345 | 0x1A234 | 1 | 这一页在内存里,物理页号 0x1A234 |
| 0x12346 | 0x0B567 | 1 | 在内存里 |
| 0xABCDE | 0x1F890 | 1 | 在内存里 |
| 0x22222 | — | 0 | 这一页不在内存里(在磁盘上或还没分配) |
翻译 0x12345678:
- 4 KB 页的偏移是 12 位,正好是最后 3 个十六进制位:偏移 = 678,VPN = 12345。
- 查页表:VPN 0x12345 → PPN 0x1A234,有效位 1。
- 拼起来:物理地址 = 0x1A234 拼接 678 = 0x1A234678。
同理 0x12346ABC → 0x0B567ABC,0xABCDEFFF → 0x1F890FFF。偏移为什么不变:虚拟页和物理页大小一样、都按页对齐,翻译只是把"第几页"换掉,字节在页里的位置不变。
页表里的有效位是什么
有效位回答一个问题:这一个虚拟页现在在不在物理内存里,也就是这一行写的 PPN 能不能用。
| 有效位 | 意思 | CPU 访问这一页时 |
|---|---|---|
| 1 | 这一页在内存里,PPN 那一栏就是它所在的物理页 | 正常翻译:PPN 拼接偏移得到物理地址 |
| 0 | 这一页不在内存里:被换到磁盘上了,或者还没分配 | PPN 没有意义、不能用 → 触发页缺失,交给操作系统 |
- 为什么需要它:PPN 字段里总会有某个二进制数,没有有效位的话,硬件分不清它是真的物理页号还是没用的旧值。有效位是给硬件看的开关:只有它是 1,才相信这一行的 PPN。
- 什么时候变:操作系统把一页从磁盘调进内存时,填好 PPN、有效位置 1;内存不够、把某一页换出到磁盘时,有效位置 0(这一页在磁盘的位置由操作系统另外记录)。
- Tutorial 9.7(c):访问 0x22222000,VPN 0x22222 的有效位是 0 → 硬件不用这一行的 PPN,触发页缺失 → 操作系统调页、填 PPN、置 1 → 重新执行指令。
| 位 | 在哪里 | 回答的问题 |
|---|---|---|
| 页表的有效位 | 页表每一项 | 这一页在不在物理内存里? |
| 缓存的有效位 | 缓存每一行 | 这一行有没有装着数据?(空行为 0,永远不会命中) |
| TLB 的有效位 | TLB 每一项 | 这一条翻译能不能用? |
| 脏位 | 缓存行、页表项 | 这一份被改过没有?被换出时要不要写回? |
页缺失(page fault)
访问 0x22222000 时,VPN = 0x22222,页表有效位是 0,这一页不在内存里,就是页缺失:
- 硬件发现有效位为 0,触发异常(精确中断),交给操作系统。
- 操作系统从磁盘把这一页读进某个空闲物理页(内存满了就先换出一页)。
- 填好页表:写上 PPN,有效位置 1。
- 回到那条指令重新执行,这次就能翻译成功。
磁盘太慢,一次页缺失要几毫秒,也就是几百万个周期。有效位的作用:标记"这一项的翻译能不能用",防止用到无效的 PPN。
和缓存对比:同一套思路,换了一层
| 缓存 | 虚拟内存 | |
|---|---|---|
| 上层 / 下层 | 缓存 / 主存 | 主存 / 磁盘 |
| 搬运单位 | 块(几十字节) | 页(KB 级) |
| 没找到叫 | 缓存缺失(几十到几百周期) | 页缺失(几百万周期) |
| 块/页能放哪 | 直接映射、组相联或全相联 | 全相联:任何空闲物理页都行,用页表记位置 |
| 替换谁 | LRU 等 | 近似 LRU(看引用位) |
| 写策略 | 写直达或写回 | 一律写回(脏位),因为每次写磁盘太慢 |
块放哪、怎么找、缺失时替换谁、写怎么处理,这四个问题对缓存、TLB、虚拟内存都适用,区别只在参数。缺失代价越大,就越值得用更聪明但更贵的办法,所以虚拟内存选全相联加写回。
TLB:翻译结果的"缓存"
问题:页表在主存里,CPU 每次读写内存都得先去主存读一次页表项才知道物理地址,访存次数直接翻倍。解决办法是 TLB(Translation Lookaside Buffer,快表):CPU 里一个很小、很快的缓存,专门存最近用过的几条翻译(VPN → PPN)。一般不超过 128–256 项,比数据缓存还快;和普通缓存一样可以是全相联、组相联或直接映射。每项含有效位、VPN(当作标签)、PPN、脏位、引用位、访问权限。
CPU 发出虚拟地址
│
├─ 查 TLB ── 命中 ──→ 直接得到物理地址 → 去缓存/主存取数据 (最常见,几乎不花时间)
│
└─ TLB 缺失 → 去主存读页表
├─ 有效位 = 1 → 把这条翻译装进 TLB,重试 (几十个周期)
└─ 有效位 = 0 → 页缺失,操作系统从磁盘调页 (几百万个周期)| 情况 | 原因 | 代价 | 谁处理 |
|---|---|---|---|
| TLB 命中 | 最近用过这一页 | 几乎为 0 | 硬件 |
| TLB 缺失,页在内存 | 页在内存,只是 TLB 没记 | 几十个周期 | 硬件或操作系统,从页表装入 TLB |
| 页缺失 | 页根本不在内存 | 几百万个周期 | 操作系统,从磁盘调页 |
- TLB 缺失不等于页缺失。TLB 缺失经常发生,大多数时候页其实就在内存里,比页缺失频繁得多。Tutorial 9.7(f):页缺失 5 ms、TLB 缺失 50 ns,前者是后者的 5×10⁻³ ÷ 5×10⁻⁸ = 100,000 倍。
- TLB 这么小也管用,靠的还是局部性。一页 4 KB 能装 1024 个 4 字节数据,顺序访问一个数组时连续上千次访问都落在同一页,第一次查到之后全部 TLB 命中。
- 软硬件分工(讲义):TLB 是硬件,TLB 访问时间算在缓存命中时间里(流水线可能为它多留一级);页缺失触发精确中断,由操作系统处理;硬件负责更新页表里的脏位和引用位。
你笔记里写的"第 3 本书第 120 页"是虚拟地址,只是你自己的编号,别人的"第 3 本书"是另一本。图书馆的总目录是页表,告诉你"你的第 3 本书"实际在"B 区 17 号架",目录很厚,放在服务台,要走过去查。贴在桌上的便利贴是 TLB,记着最近查过的几本书在哪个架子,先看便利贴,有就不用去服务台。书架上根本没这本书、要从仓库调过来,就是页缺失,最慢。"第 120 页"不需要翻译,就是页内偏移。
例题:一次走完 TLB 命中、TLB 缺失和页缺失
某系统虚拟地址 16 位,物理地址 15 位,页大小 4 KB。TLB 只有 2 项,全相联,LRU 替换;初始 TLB 里是 VPN 2 → PPN 0 和 VPN 5 → PPN 1,其中 VPN 5 最久未用。页表如下(其余 VPN 都无效):
| VPN | PPN | 有效位 |
|---|---|---|
| 0 | 4 | 1 |
| 1 | — | 0(在磁盘上) |
| 2 | 0 | 1 |
| 3 | 3 | 1 |
| 4 | — | 0 |
| 5 | 1 | 1 |
发生页缺失时,操作系统把那一页调入空闲的物理页 6,处理完后把这条翻译装进 TLB。依次访问虚拟地址:0x2ABC, 0x3010, 0x2FFF, 0x1234, 0x3FF0, 0x5008。
- 页内偏移、VPN、PPN 各几位?
- 对每次访问写出 VPN、偏移、TLB 命中还是缺失、是否页缺失、物理地址,以及访问后 TLB 里的内容。
- 查 TLB 每次 1 周期,TLB 缺失时读页表 50 周期,页缺失 1,000,000 周期。这 6 次访问的翻译总代价是多少周期?
- 思考:TLB 改成 4 项,结果有什么变化?
解答 · 逐步
(a) 偏移 = log₂4096 = 12 位;VPN = 16 − 12 = 4 位(共 16 个虚拟页);PPN = 15 − 12 = 3 位(物理页 0–7)。所以虚拟地址的第 1 个十六进制位就是 VPN,后 3 位是偏移;物理地址的第 1 个十六进制位就是 PPN。
| # | 虚拟地址 | VPN | 偏移 | TLB | 查页表 | 物理地址 | 访问后 TLB(左边是较久未用的) |
|---|---|---|---|---|---|---|---|
| 1 | 0x2ABC | 2 | ABC | 命中 | 不用查 | 0x0ABC | 5→1, 2→0 |
| 2 | 0x3010 | 3 | 010 | 缺失 | PPN 3,有效;装入,踢掉最久未用的 VPN 5 | 0x3010 | 2→0, 3→3 |
| 3 | 0x2FFF | 2 | FFF | 命中 | 不用查 | 0x0FFF | 3→3, 2→0 |
| 4 | 0x1234 | 1 | 234 | 缺失 | 有效位 0 → 页缺失;调入物理页 6,页表改为 1 → 6;装入 TLB,踢掉 VPN 3 | 0x6234 | 2→0, 1→6 |
| 5 | 0x3FF0 | 3 | FF0 | 缺失 | PPN 3,有效(第 4 步 VPN 3 被踢出 TLB,但页还在内存);踢掉 VPN 2 | 0x3FF0 | 1→6, 3→3 |
| 6 | 0x5008 | 5 | 008 | 缺失 | PPN 1,有效(VPN 5 第 2 步被踢出 TLB);踢掉 VPN 1 | 0x1008 | 3→3, 5→1 |
结果:TLB 命中 2 次(第 1、3 步),TLB 缺失 4 次;其中只有第 4 步是页缺失。第 5、6 步是 TLB 缺失但不是页缺失:页一直在内存里,只是翻译被挤出了 TLB,查一次页表就行。
一次页缺失占了 99.98% 的时间,这就是为什么页缺失必须非常少,也是虚拟内存要用全相联和写回、尽量减少页缺失的原因。
(d) TLB 改成 4 项:初始的 2、5 加上后来的 3、1 正好 4 项,不需要踢任何一项,第 5、6 步变成命中。TLB 命中 4 次、缺失 2 次(第 2、4 步),总代价 = 6 + 2 × 50 + 1,000,000 = 1,000,106 周期。TLB 太小会把还要用的翻译挤出去,和缓存的容量缺失是同一回事。
用上面例题的 6 次访问,一步步演示虚拟地址怎么变成物理地址:先拆出 VPN 和偏移,偏移直接抄过去;VPN 先查 TLB,命中就直接得到 PPN;缺失就去主存读页表;有效位为 0 再去磁盘调页。小方块是当前正在传递的东西,右下角累计翻译花掉的周期数。做完例题再看,可以逐步对答案。
| # | 虚拟地址 | VPN | 偏移 | TLB | 页缺失 | 物理地址 |
|---|
在 9.3 节的"地址拆分器"里切到"虚拟地址"标签,点"9.7 节例题"预设,也可以逐个检查这道题每个地址的拆分和翻译(那个工具只查页表,不模拟 TLB;0x1234 会显示页缺失,因为它用的是调页之前的页表)。
- 位数:偏移 = log₂(页大小),VPN = 虚拟地址位 − 偏移,PPN = 物理地址位 − 偏移。
- 翻译:拆出 VPN 查表换成 PPN,偏移照抄;4 KB 页就是后 3 个十六进制位不动。
- 有效位 = 0 → 页缺失,说清操作系统做的四步。
- 偏移为什么不变:页和页框大小相同、按页对齐。
- 虚拟内存的优点任选两个:运行比内存大的程序、进程隔离与保护、可控共享、重定位方便、避免碎片。
- TLB 缺失 vs 页缺失:代价差好几个数量级。
自测 · Self-Check
32 位地址、64 KB 缓存、64 B 块。直接映射、4 路、全相联的 Tag / Index / Offset 各几位?
为什么固定容量下块不能无限加大?
写回缓存里,对同一块连写 3 次,主存一共被写几次?写直达呢?
L1 局部缺失率 5%,L2 局部缺失率 2%。L2 的全局缺失率是多少?代表什么?
TLB 缺失一定意味着页缺失吗?
流水线入门与 DLX 指令集
Final 范围Topic II 从这里开始。缓存解决的是"等内存",流水线解决的是"一条指令做完才开始下一条"。这一课先用延迟和吞吐量两个指标说清楚流水线到底快在哪里,给出流水线总时间公式;然后介绍后面三课都要用的教学处理器 DLX:寄存器、指令、寻址模式和 R/I/J 三种编码格式。
10.1 延迟、吞吐与关键路径
- 延迟(latency):一个工作单位从进入系统到完成所需的时间。
- 吞吐量(throughput):单位时间内系统能完成的工作单位数。
- 并行的目标:提高吞吐量;有时也能降低延迟。
组合电路的延迟由关键路径决定:从输入到输出所有路径中门延迟之和最大的那条。讲义的 1 位全加器:AND、OR、非门各 tg,XOR 为 3tg,关键路径 A → XOR → XOR → S,延迟 6tg,吞吐量 = 每 6tg 完成一次加法 = 1/(6tg)。
从输入开始,给每个门的输出标上"最早稳定时间" = 它所有输入的最晚稳定时间 + 本门延迟(输入端的小圆圈是非门,要单独加一个非门延迟)。输出端的时间就是延迟,倒推取最大值的那条输入就是关键路径。Exercise 10.1、10.2 的完整解答见习题小程序。
For the logic circuit below, (a) determine and clearly show the critical path delay using the gate delays NOT = 9, AND = 13, OR = 11, XOR = 23; (b) compute the latency and throughput of the circuit.
对下面的电路:(a) 用门延迟 NOT = 9、AND = 13、OR = 11、XOR = 23 找出并标明关键路径延迟;(b) 求电路的延迟和吞吐量。
Boxes show gate type · delay 方框内为门类型 · 延迟
解答 · Solution
Highlighted = critical path 高亮为关键路径;t = arrival time 到达时间
| Path 路径 | Delay 延迟 |
|---|---|
| A → OR → XOR | 11 + 23 = 34 |
| C/D → OR → NOT → OR → XOR | 11 + 9 + 11 + 23 = 54 |
| B/C → AND → OR → XOR | 13 + 11 + 23 = 47 |
| B/D → XOR → OR → XOR | 23 + 11 + 23 = 57 |
Critical path: B (or D) → XOR → OR → XOR → Z, delay = 57 time units. One result is produced every 57 units, so throughput = 1/57 computations per unit time.
Method: label each gate output with its earliest settle time = latest input settle time + gate delay; the time at the output is the latency.
关键路径:B(或 D)→ XOR → OR → XOR → Z,延迟 = 57 个时间单位。电路每 57 个单位完成一次计算,吞吐量 = 1/57(每单位时间完成的计算次数)。
方法:给每个门的输出标"最早稳定时间" = 各输入最晚稳定时间 + 本门延迟,输出端的时间就是延迟。
For the circuit below (a small circle at a gate input is an inverter, i.e. a NOT gate), (a) determine and clearly show the critical path delay using NOT = 10, AND = 15, OR = 17; (b) compute the latency and throughput.
对下面的电路(门输入端的小圆圈表示非门):(a) 用 NOT = 10、AND = 15、OR = 17 找出并标明关键路径延迟;(b) 求延迟和吞吐量。
Boxes show gate type · delay 方框内为门类型 · 延迟
解答 · Solution
Highlighted = critical path 高亮为关键路径;t = arrival time 到达时间
Latency = 74, throughput = 1/74. Do not forget the NOT delay hidden in the input bubbles.
延迟 74,吞吐量 1/74。别漏掉输入端小圆圈代表的非门延迟。
10.2 空间并行 vs 时间并行
- 空间并行:多套硬件同时工作,同时处理多个任务。通常直接叫"并行"。
- 时间并行:把任务切成几级(像流水线工厂),不同任务同时处在不同级,执行在时间上重叠。这就是流水线。
讲义的饼干例子:擀饼干 5 分钟,烤 15 分钟。一盘的延迟永远是 20 分钟(1/3 小时),不管用哪种并行。
| 方案 | 吞吐量 | 说明 |
|---|---|---|
| Ben 一个人,烤完再做下一盘 | 3 盘/小时 | 每 20 分钟一盘 |
| 空间并行:Alyssa 带着自己的烤箱帮忙 | 6 盘/小时 | 两人各 20 分钟一盘 |
| 时间并行:第一盘进烤箱就开始擀第二盘 | → 4 盘/小时 | n 盘用时 5 + 15n 分钟,n→∞ 时每 15 分钟一盘(瓶颈是烤箱) |
| 两人各自流水 | 8 盘/小时 | 2 × 4 |
讲义说本课重点放在流水线,因为它是 CPU 设计的基础,又能自然引出 CPI、冒险这些概念;空间并行还牵涉同步、通信、内存一致性,复杂得多。
10.3 流水线总时间公式
洗衣房例子:4 个人各有一筐衣服,洗 30 分钟、烘 40 分钟、叠 20 分钟。顺序做要 4 × 90 = 360 分钟(6 小时)。流水起来,一台机器空出来就接下一筐:
如果每级时间相同、各占 1 个周期,k 级流水线处理 n 个任务需要 k + n − 1 个周期(Tutorial 10.1:第一个任务 k 个周期出来,之后每周期出一个)。
流水线的几条结论
- 流水线不降低单个任务的延迟,提高的是整批工作的吞吐量。
- 吞吐量受最慢的一级限制。
- 理论最大加速比约等于级数;各级时间不均衡会降低加速比。
- 填充(filling)和排空(draining)流水线的时间也会拉低加速比,任务越多这部分影响越小。
讲义里出现了两种模型。任务流水(洗衣房、Tutorial 10.5/10.6 写明"一级做完就能立刻接下一个任务"):第一个任务只需 ΣTi,用上面的公式。时钟流水(处理器、DLX):每级都被拉长到一个时钟周期,第一条指令的延迟 = 级数 × 时钟周期(讲义 5 × 250 = 1250 ps),总时间 = (k + n − 1) × Tclk。n 很大时两者几乎相等,加速比都趋近 ΣTi / max Ti。
10.4 单周期 vs 流水线处理器
讲义的五级时间:取指 250 ps、译码读寄存器 150、执行 200、访存 250、写寄存器 100。
| 单周期处理器 | 五级流水线处理器 | |
|---|---|---|
| 时钟周期 | 950 ps(五级之和) | 250 ps(最慢一级) |
| 单条指令延迟 | 950 ps | 5 × 250 = 1250 ps(变长了) |
| 吞吐量 | 1/950 ps ≈ 1.05×10⁹ 条/秒 | 1/250 ps = 4×10⁹ 条/秒 |
| 大量指令时的加速比 | 950 / 250 = 3.8(不是 5,因为各级不均衡) | |
讲义第 22 页把单周期吞吐量写成 1.59 × 10⁹ 条/秒,第 25 页写"每 950 ns 一条"。正确值是 1/950 ps ≈ 1.05 × 10⁹ 条/秒,单位是 ps。
Tutorial 10.7(d) 问"流水线的单条延迟更长,所以流水线更慢"对不对:不对。延迟只描述一条指令,处理器跑的是成千上万条指令的程序,决定程序运行时间的是吞吐量。
Stage delays: IF 180 ps, ID 220 ps, EX 250 ps, MEM 300 ps, WB 150 ps. (a) For a single-cycle processor, find the instruction latency and the throughput. (b) For a 5-stage pipeline, find the clock period, the instruction latency and the throughput once the pipeline is full. (c) For a very large program, find the speedup of pipelining. (d) Explain why the speedup is less than the number of stages.
各级延迟:IF 180、ID 220、EX 250、MEM 300、WB 150 ps。(a) 单周期处理器的单条延迟和吞吐量;(b) 五级流水线的时钟周期、单条延迟、填满后的吞吐量;(c) 程序很大时的加速比;(d) 为什么加速比小于级数?
解答 · Solution
(d) The stages are unbalanced: the clock is set by the slowest stage (MEM, 300 ps) while the others idle (the average stage is only 220 ps). Pipeline-register overhead, fill/drain time and hazard stalls reduce it further.
(d) 各级不均衡:时钟由最慢的 MEM(300 ps)决定,其他级在空等(平均每级只有 220 ps)。此外还有流水线寄存器开销、填充排空和冒险停顿。
10.5 DLX 寄存器、数据类型与指令
DLX(读作 "deluxe")是教学用的简化 RISC 处理器,结构和 MIPS 很像,五级流水线:取指、译码、执行、访存、写回。和 MIPS 相比,DLX 指令更少、寻址模式更简单,冒险处理也用教学化的例子。
- 32 个 32 位通用寄存器 R0–R31。
R0恒为 0(只读,写它无效);R31是链接寄存器,JAL/JALR 把返回地址存在这里。 - 数据类型:整数 B(8 位字节)、H(16 位半字)、W(32 位字),U 表示无符号;浮点 F(32 位单精度)、D(64 位双精度);HI/LO 存放扩展结果的高、低 32 位。
- 立即数是 16 位,嵌在 I 型指令里(ADDI、LW 等)。
| 类别 | 指令 | 例子与语义 |
|---|---|---|
| 取数 | LB/LBU, LH/LHU, LW, LF/LD, LHI | LW R5, 400(R3):R5 ← Mem[R3 + 400] |
| 存数 | SB, SH, SW, SF/SD | SW R4, 400(R3):Mem[R3 + 400] ← R4。存数没有无符号版本,因为写进内存的只是比特,有无符号只影响读出时的解释 |
| 算术 | ADD, SUB, ADDU, SUBU ADDI, SUBI, ADDUI, SUBUI | SUBI R4, R2, #10:R4 ← R2 − 10。U = 无符号(不产生溢出异常),I = 立即数 |
| 逻辑 | AND/ANDI, OR/ORI, XOR/XORI | ANDI 清位、ORI 置位、XORI 翻转位 |
| 置条件 | SLT, SLE, SGT, SGE, SEQ, SNE(及 I 版本) | SLT R1, R2, R3:R2 < R3 则 R1 ← 1,否则 0。常配合 BEQZ/BNEZ 使用 |
| 条件分支 | BEQZ, BNEZ | BEQZ R7, Loop1:R7 = 0 就跳到 Loop1,否则顺序执行 |
| 无条件跳转 | J, JR, JAL, JALR | J 跳到标号;JR 跳到寄存器里的地址;JAL/JALR 跳之前先 R31 ← PC + 4 |
位操作例子:R1 = 10110011₂,ANDI R2, R1, #254(掩码 11111110)清第 0 位 → 10110010;ORI R2, R2, #8 置第 3 位 → 10111010;XORI R2, R2, #2 翻转第 1 位 → 10111000。
函数调用:JAL SUM 先存 R31 ← PC + 4 再跳到 SUM,函数结尾 JR R31 回到 JAL 的下一条。JALR 用于函数指针、虚函数这类运行时才确定目标的调用。
10.6 寻址模式与 load-store 架构
| 寻址模式 | 例子 | 操作数在哪 |
|---|---|---|
| 寄存器寻址 | ADD R1, R2, R3 | 都在寄存器里,不访存,最快 |
| 立即数寻址 | ADDI R2, R1, #3 | 一个操作数是指令里的常数 |
| 偏移(基址)寻址 | LW R17, 400(R23) | 有效地址 = 基址寄存器 + 偏移(以字节计),用于数组、结构体 |
| PC 相对 | BEQZ R2, label、J label | 目标 = PC + 4 + 偏移(立即数字段当作分支位移) |
只有 load/store 能访问内存,ALU 指令只能操作寄存器。所以 ADD R3, R2, 256(R1) 是非法的,必须拆成两条:LW R4, 256(R1) 把数据取进寄存器,再 ADD R3, R2, R4。Tutorial 10.8(d) 考的就是这个。
注意存数指令有两种写法:讲义写 SW R4, 400(R3),Tutorial 写 SW 20(R2), R3(经典 DLX 语法)。两者意思相同:把寄存器的值存到"基址 + 偏移"处。
R1 = 10, R2 = 20, R3 = 5, R4 = 100. Determine the value written to the destination register (each instruction executes independently): (a) ADD R5, R1, R2 (b) SUB R6, R2, R3 (c) ADDI R7, R1, 15
R1 = 10、R2 = 20、R3 = 5、R4 = 100。各指令独立执行,求写入目的寄存器的值:(a) ADD R5, R1, R2 (b) SUB R6, R2, R3 (c) ADDI R7, R1, 15
解答 · Solution
(a) R5 = 10 + 20 = 30 (b) R6 = 20 − 5 = 15 (c) R7 = 10 + 15 = 25
(a) R5 = 10 + 20 = 30 (b) R6 = 20 − 5 = 15 (c) R7 = 10 + 15 = 25
For LW R1, 0(R2), ADD R3, R1, R4, SW R3, 8(R2), BEQZ R3, LOOP on the 5-stage DLX pipeline (IF → ID → EX → MEM → WB): identify R-type or I-type, the stages used, whether a register is written back, and briefly describe the operation.
对 LW R1, 0(R2)、ADD R3, R1, R4、SW R3, 8(R2)、BEQZ R3, LOOP(五级 DLX):判断 R 型或 I 型、用到的流水级、是否写回寄存器,并简述操作。
解答 · Solution
| Instruction 指令 | Type 类型 | Stages 流水级 | Writes reg? 写寄存器 | Operation 操作 |
|---|---|---|---|---|
LW R1,0(R2) | I | IF ID EX MEM WB | Yes 是(R1) | R1 ← Mem[R2 + 0] |
ADD R3,R1,R4 | R | IF ID EX (MEM pass) WB | Yes 是(R3) | R3 ← R1 + R4 |
SW R3,8(R2) | I | IF ID EX MEM | No 否 | Mem[R2 + 8] ← R3 |
BEQZ R3,LOOP | I | IF ID EX MEM (branch completion) | No 否 | if R3 = 0 then PC ← LOOP |
Only LW uses all five stages for real work. ADD passes through MEM doing nothing; SW writes memory but no register; BEQZ completes the branch in MEM (updates the PC) and writes no register.
只有 LW 走满五级并且真正用到 MEM 和 WB;ADD 的 MEM 级空过;SW 写存储器但不写寄存器;BEQZ 在 MEM 级完成分支(更新 PC),也不写回。
R1 = 10, R2 = 100, R3 = 20, R4 = 5, Mem[104] = 30. Program: LW R5, 4(R2); ADD R6, R5, R1; SUB R7, R6, R4; SW R7, 8(R2). (a) Effective address of LW. (b) Value loaded into R5. (c) Values in R6 and R7. (d) Memory location and value written by SW. (e) Pipeline stages used by each instruction.
R1 = 10、R2 = 100、R3 = 20、R4 = 5,Mem[104] = 30。程序:LW R5, 4(R2);ADD R6, R5, R1;SUB R7, R6, R4;SW R7, 8(R2)。求 (a) LW 的有效地址;(b) R5;(c) R6、R7;(d) SW 写到哪个地址、写什么值;(e) 每条用到的流水级。
解答 · Solution
(e) LW: IF ID EX MEM WB; ADD and SUB: IF ID EX WB (MEM passes through); SW: IF ID EX MEM.
(e) LW:IF ID EX MEM WB;ADD、SUB:IF ID EX WB(MEM 空过);SW:IF ID EX MEM。
10.7 R / I / J 三种指令格式
| 格式 | 用于 | 要点 |
|---|---|---|
| I 型 | 立即数运算、load、store、条件分支、JR、JALR | 一个源/基址寄存器 Rs + 一个 Rt + 16 位立即数。Rt 不一定是目的寄存器:LW 时 Rt 接收数据,SW 时 Rt 提供数据,ADDI 时 Rt 接收结果。立即数的含义随指令变:算术里是常数,load/store 里是地址偏移,分支里是位移 |
| R 型 | 寄存器-寄存器 ALU 运算(ADD、SUB、AND、OR、SLT…) | Rs、Rt 永远是两个源操作数,Rd 永远接收结果;Opcode 只说明是 ALU 类,Function 字段才指定具体做哪种运算 |
| J 型 | J、JAL(以及 trap、异常返回) | 只有 Opcode 和 26 位偏移,不读寄存器;只有 JAL 会写寄存器(R31) |
有三个寄存器、没有常数 → R 型。有一个常数、偏移或标号,且有寄存器 → I 型(包括 BEQZ/BNEZ、LW/SW、所有带 I 的运算)。只有标号、没有寄存器 → J 型(J、JAL)。
Identify whether each instruction uses the R-type, I-type or J-type format: ADD R1,R2,R3, LW R4,8(R5), J LOOP, ADDI R6,R7,#10, SUB R8,R9,R10, BEQZ R1,EXIT, JAL FUNCTION, SW R3,16(R4).
判断格式(R / I / J):ADD R1,R2,R3、LW R4,8(R5)、J LOOP、ADDI R6,R7,#10、SUB R8,R9,R10、BEQZ R1,EXIT、JAL FUNCTION、SW R3,16(R4)。
解答 · Solution
R-type: ADD, SUB (three registers). I-type: LW, ADDI, BEQZ, SW (register + immediate / offset / displacement). J-type: J, JAL (26-bit offset only).
R 型:ADD、SUB(三个寄存器)。I 型:LW、ADDI、BEQZ、SW(寄存器 + 立即数/偏移/位移)。J 型:J、JAL(只有 26 位偏移)。
For each instruction, fill in the fields used by its format (Opcode, Rs, Rt, Rd, Immediate/Target): ADDI R1,R2,#12, LW R3,40(R4), ADD R5,R6,R7, J LOOP, plus SUB R8,R9,R10 and BEQZ R1,EXIT.
填写每条指令格式中用到的字段(Opcode、Rs、Rt、Rd、立即数/目标):ADDI R1,R2,#12、LW R3,40(R4)、ADD R5,R6,R7、J LOOP,以及 SUB R8,R9,R10、BEQZ R1,EXIT。
解答 · Solution
| Instruction 指令 | Opcode | Rs | Rt | Rd | Imm / Target 立即数/目标 |
|---|---|---|---|---|---|
ADDI R1,R2,#12 | ADDI | R2 | R1 | — | 12 |
LW R3,40(R4) | LW | R4 | R3 | — | 40 |
ADD R5,R6,R7 | ALU (func = ADD) | R6 | R7 | R5 | — |
SUB R8,R9,R10 | ALU (func = SUB) | R9 | R10 | R8 | — |
BEQZ R1,EXIT | BEQZ | R1 | — | — | offset to EXIT 到 EXIT 的位移 |
J LOOP | J | — | — | — | LOOP (26-bit offset) |
In I-type instructions the role of Rt depends on the instruction: for ADDI and LW it is the destination. For R-type, the opcode only says "ALU operation"; the function field selects the exact operation.
I 型里 Rt 的角色随指令变化:ADDI、LW 中 Rt 是目的寄存器。R 型的 Opcode 只说明是 ALU 类,具体运算由 Function 字段指定。
Program: LW R1, 0(R2); ADD R3, R1, R4; SW R3, 8(R2); J DONE. For each instruction state the format and whether it reads memory, writes memory and writes a register.
程序:LW R1, 0(R2);ADD R3, R1, R4;SW R3, 8(R2);J DONE。对每条写出格式,以及是否读存储器、写存储器、写寄存器。
解答 · Solution
| Instruction 指令 | Format 格式 | Reads mem 读存储器 | Writes mem 写存储器 | Writes reg 写寄存器 |
|---|---|---|---|---|
LW R1,0(R2) | I | Yes 是 | No 否 | Yes 是 |
ADD R3,R1,R4 | R | No 否 | No 否 | Yes 是 |
SW R3,8(R2) | I | No 否 | Yes 是 | No 否 |
J DONE | J | No 否 | No 否 | No 否 |
"Memory" here means data memory; every instruction still reads instruction memory in IF.
这里的"读写存储器"指数据存储器;每条指令在 IF 都要读指令存储器。
自测 · Self-Check
3 级流水线、每级 1 周期。至少要处理多少个任务,才能比顺序执行快 2.5 倍以上?
五级流水线各级 220/180/250/200/150 ps。时钟周期、单条延迟、吞吐量、各级完全平衡时的最大加速比?
为什么把 ALU 那一级从 200 ps 缩短到 150 ps,流水线加速比反而从 4 降到 3.75?
BEQZ R3, LOOP 和 J EXIT 分别是什么格式?
SW R1, 40(R2) 里 Rt 是谁?它是目的寄存器吗?
五级 DLX 流水线
Final 范围这一课把一条 DLX 指令拆成五个时钟步骤,逐个看数据通路里每个部件在干什么、产生什么值、存进哪个临时寄存器。Tutorial 11 几乎全是这种"填表题":给一条指令和寄存器初值,写出每一级的操作、关键输出和流水线寄存器内容。把下面的实验跑熟,这类题就是送分题。
11.1 五个步骤的微操作
| 步骤 | 名称 | 微操作(RTL) | 说明 |
|---|---|---|---|
| IF | 取指 | IR ← Mem[PC] NPC ← PC + 4 | 用 PC 从指令存储器取指令;加法器同时算下一条地址(每条指令 4 字节) |
| ID | 译码 / 读寄存器 | A ← Regs[Rs] B ← Regs[Rt] Imm ← SignExt(imm16) b ← 目的寄存器号 | 译码、读源寄存器、符号扩展立即数、产生控制信号,三件事并行(固定字段译码)。b 是目的寄存器的编号:R 型是 Rd,I 型是 Rt |
| EX | 执行 / 算地址 | load/store:ALUOut ← A + Imm R 型:ALUOut ← A op B I 型运算:ALUOut ← A op Imm 分支:ALUOut ← NPC + Imm;Cond ← (A == 0) | ALU 要么算结果,要么算有效地址,要么算分支目标;Zero? 单元判断分支条件(BEQZ 判 A = 0,BNEZ 判 A ≠ 0) |
| MEM | 访存 / 完成分支 | load:LMD ← Mem[ALUOut] store:Mem[ALUOut] ← B 分支:PC ← Cond ? ALUOut : NPC | 只有 load/store 访问数据存储器;分支在这里更新 PC;ALU 指令直接穿过 |
| WB | 写回 | R 型:Regs[Rd] ← ALUOut I 型运算:Regs[Rt] ← ALUOut load:Regs[Rt] ← LMD | WB 前的 MUX 选 ALUOut(ALU 指令)或 LMD(load)。store 和分支不写回 |
讲义的 MEM 例子:SW R3, 500(R4),R4 = 100、R3 = 99:EX 算出 ALUOut = 600,MEM 执行 Mem[600] ← 99。LW R1, 30(R2),R2 = 100、Mem[130] = 55:ALUOut = 130,LMD = 55。BEQZ R4, LOOP,R4 = 0、NPC = 104、偏移 40:ALUOut = 144,Cond = 1,MEM 阶段 PC ← 144。
11.2 补码与符号扩展
I 型指令的立即数只有 16 位,进 ALU 前要扩展成 32 位。符号扩展就是把第 15 位(符号位)复制到高 16 位:正数前面补 0,负数前面补 1,数值不变。
Tutorial 11.2 用的就是 ADDI R5, R2, #-12(R2 = 40):A = 40,Imm = −12(0xFFFFFFF4),ALUOut = 40 + (−12) = 28,WB 写 R5 ← 28。在下面的实验里选这个预设,可以看到每一位。
Consider ADDI R5, R2, #12 with R2 = 20. For each stage (IF, ID, EX, MEM, WB): describe the operation, give the key signal values, and state whether memory is accessed.
ADDI R5, R2, #12,R2 = 20。对每一级(IF、ID、EX、MEM、WB)写出所做操作、关键信号值,以及是否访问存储器。
解答 · Solution
| Stage 级 | Operation 操作 | Key output 关键输出 | Memory 存储器 |
|---|---|---|---|
| IF | IR ← Mem[PC]; NPC ← PC + 4 | IR = ADDI R5,R2,#12; NPC = PC + 4 | Instruction memory 指令存储器 |
| ID | A ← Regs[R2]; Imm ← SignExt(12); b ← Rt = R5 | A = 20, Imm = 12, b = R5 | — |
| EX | ALUOut ← A + Imm (MUX selects Imm) | ALUOut = 32 | — |
| MEM | no data access, pass ALUOut 不访存,直接传递 | ALUOut = 32 | No 否 |
| WB | Regs[R5] ← ALUOut | R5 = 32 | — |
Data memory is never accessed; only IF reads instruction memory. For an I-type ALU instruction the destination register is Rt (R5).
数据存储器全程没有被访问,只有 IF 读了指令存储器。I 型运算指令的目的寄存器是 Rt(R5)。
LW R1, 30(R2) with R2 = 1000 and Mem[1030] = 500. Determine (1) A after ID, (2) Imm after sign extension, (3) ALUOut after EX, (4) the memory location accessed in MEM, (5) the value loaded into LMD, (6) the value written into R1 in WB.
LW R1, 30(R2),R2 = 1000,Mem[1030] = 500。求 (1) ID 后的 A;(2) 符号扩展后的 Imm;(3) EX 后的 ALUOut;(4) MEM 访问的地址;(5) LMD;(6) WB 写入 R1 的值。
解答 · Solution
(1) A = 1000 (2) Imm = 30 (3) ALUOut = 1030 (4) address 1030 (5) LMD = 500 (6) R1 = 500
(1) A = 1000 (2) Imm = 30 (3) ALUOut = 1030 (4) 地址 1030 (5) LMD = 500 (6) R1 = 500
For ADD R1,R2,R3 and LW R1,40(R2): which stages are used, is data memory accessed, what does the ALU do, what value is written back, and which MUX input is selected in EX?
对 ADD R1,R2,R3 和 LW R1,40(R2):用到哪些级?是否访问数据存储器?ALU 做什么?写回什么值?EX 级的 MUX 选哪个输入?
解答 · Solution
ADD R1,R2,R3 | LW R1,40(R2) | |
|---|---|---|
| Stages 级 | IF ID EX (MEM) WB | IF ID EX MEM WB |
| ALU inputs ALU 输入 | A = R2, B = R3 (MUX → B) | A = R2, Imm = 40 (MUX → Imm) |
| ALU operation ALU 操作 | add → result 加法得结果 | add → effective address 加法得有效地址 |
| Data memory 数据存储器 | No 否 | Read 读 |
| Written back 写回 | ALUOut = R2 + R3 | LMD = Mem[R2 + 40] |
Both add in the ALU; the difference is the second ALU input (B for R-type, Imm for loads/stores) and the WB multiplexer (ALUOut for ADD, LMD for LW).
两条都在 ALU 里做加法,区别在第二个输入:R 型选 B,load/store 选 Imm;写回时 WB 的 MUX 一个选 ALUOut,一个选 LMD。
Register R2 contains 0 and PC = 1000. Consider BEQZ R3, +40. Determine (1) NPC, (2) the ALU inputs, (3) the branch target address, (4) the output of the Zero? block, (5) whether the branch is taken, (6) the new PC.
R2 = 0,PC = 1000。对 BEQZ R3, +40 求 (1) NPC;(2) ALU 输入;(3) 分支目标地址;(4) Zero? 的输出;(5) 是否跳转;(6) 新 PC。
解答 · Solution
The question gives R2 = 0 but the branch tests R3, which looks like a typo; assume R3 = 0:
If R3 ≠ 0: Zero? = 0, not taken, new PC = NPC = 1004.
题目给的是 R2 = 0,但指令测试 R3,应是笔误,按 R3 = 0 理解:
若 R3 ≠ 0:Zero? = 0,不跳转,新 PC = NPC = 1004。
11.3 为什么所有指令都要经过 MEM
| 指令类型 | 用数据存储器吗 | MEM 阶段在做什么 |
|---|---|---|
| LW | ✓ 读 | 把数据读进 LMD |
| SW | ✓ 写 | 把寄存器的值写进存储器 |
| BEQZ / BNEZ | ✗ | 完成分支:按 Cond 更新 PC |
| ADD / SUB / AND … | ✗ | 空操作,结果直接穿过 |
所以第 4 步全名叫"Memory Access / Branch Completion"。DLX 流水线是固定五级结构,每条指令都占用这一级,即使什么都不做。
11.4 非流水 DLX 的 CPI
非流水实现中,每一步用一个时钟周期,用 NPC、IR、A、B、Imm、Cond、ALUOut、LMD 这些临时寄存器保存步与步之间的中间结果。不需要 WB 的指令可以少一个周期:
11.5 流水线寄存器 F/D · D/A · A/M · M/W
流水化以后,原来的临时寄存器变成四组级间缓冲(inter-stage buffer),用边沿触发寄存器实现:时钟沿到来时锁存输入,保持到下一个时钟沿。这样每一级在一个周期结束时把结果交给下一级,多条指令才能同时在流水线里走而互不干扰。讲义类比的器件是 74LS374 边沿触发 D 锁存器。
| 缓冲 | 位置 | 存什么(讲义) |
|---|---|---|
| F/D | IF → ID | IR(指令本身)、NPC |
| D/A | ID → EX | A、B、Imm、b(目的寄存器号)、OP(操作码)、Cond、NPC |
| A/M | EX → MEM | ALUOut(运算结果或有效地址)、NPC、Cond、b、OP;store 还要带上 B(要写的数据,图上叫 SMD) |
| M/W | MEM → WB | LMD(load 读到的数据)、ALUOut、b、OP |
每个缓冲存的是"下一级要用、而且后面还要接着用"的东西。目的寄存器号 b 和 OP 要一路带到 WB,因为写回时才用得上。以 LW R1, 20(R2)、R2 = 100、Mem[120] = 55 为例(Tutorial 11.7):F/D = {IR = LW R1,20(R2), NPC};D/A = {A = 100, Imm = 20, b = R1, OP = LW};A/M = {ALUOut = 120, b = R1, OP};M/W = {LMD = 55, b = R1, OP}。
For LW R1, 30(R2), identify the information that must be stored in each inter-stage register (F/D, D/A, A/M, M/W) so that the next stage can continue.
对 LW R1, 30(R2),写出每个级间寄存器(F/D、D/A、A/M、M/W)必须保存的信息,使下一级能继续执行。
解答 · Solution
| Register 寄存器 | Contents 内容 |
|---|---|
| F/D | IR (the instruction 指令本身), NPC |
| D/A | A = Regs[R2], Imm = 30, b = R1 (destination 目的号), OP = LW (also B, NPC) |
| A/M | ALUOut = R2 + 30 (effective address 有效地址), b, OP |
| M/W | LMD = Mem[R2 + 30], b, OP (ALUOut) |
Each buffer keeps what the next stage needs plus what later stages still need. The destination register number b and OP travel all the way to WB, where the write happens.
每个缓冲存"下一级要用、后面还要接着用"的东西。目的寄存器号 b 和 OP 一路带到 WB,因为写回时才用。
选一条指令和初始值,按"下一步"走过 IF → ID → EX → MEM → WB。图中高亮的是这一拍用到的部件和连线,下方表格逐行填出 Tutorial 11 要求的"操作 / 关键输出 / 是否访问数据存储器",以及四个流水线寄存器的内容。
| 阶段 | 执行的操作 | 关键输出 | 数据存储器 |
|---|
11.6 流水线性能与要点
理想情况下五级流水线每个周期完成一条指令,CPI = 1。讲义例子:非流水 DLX 时钟 1 ns,ALU 指令 4 周期(40%)、分支 4 周期(20%)、访存指令 5 周期(40%);流水化额外增加 0.1 ns 时钟开销(寄存器延迟、时钟偏移)。
- 流水线提高的是吞吐量,不减少单条指令的延迟。
- 单端口存储器里,取指(IF)和访存(MEM)要同时用存储器,不同指令会抢同一资源,这就是结构冒险,需要停顿。
- 实际流水线因为冒险,CPI > 1。三类冒险是 Lesson 12 的内容。
R1 = 10, R2 = 100, R3 = 20, Mem[104] = 50. Sequence: LW R4, 4(R2); ADDI R5, R4, #10; ADD R6, R5, R1. For each: the values of A, B (if used) and Imm, ALUOut, the value written back, and the datapath components used.
R1 = 10、R2 = 100、R3 = 20,Mem[104] = 50。序列:LW R4, 4(R2);ADDI R5, R4, #10;ADD R6, R5, R1。对每条写出 A、B(若用到)、Imm、ALUOut、写回值和用到的部件。
解答 · Solution
| Instruction 指令 | A / B / Imm | ALUOut | Write-back 写回 | Components 部件 |
|---|---|---|---|---|
LW R4,4(R2) | A = 100, Imm = 4 | 104 | R4 ← 50 | IM, RegFile, SignExt, ALU, DM, LMD, WB MUX |
ADDI R5,R4,#10 | A = 50, Imm = 10 | 60 | R5 ← 60 | IM, RegFile, SignExt, ALU (MUX → Imm), WB |
ADD R6,R5,R1 | A = 60, B = 10 | 70 | R6 ← 70 | IM, RegFile, ALU (MUX → B), WB |
Values are computed as if the instructions run one after another. In a real pipeline LW → ADDI is a load-use hazard and ADDI → ADD is a RAW hazard, handled by forwarding and stalls (Lesson 12).
这里按顺序执行、不考虑冒险来算数值。真正流水起来,LW → ADDI 是 load-use,ADDI → ADD 也是 RAW,要靠转发和停顿(Lesson 12)。
Draw a timing diagram for LW R1, 0(R2); ADD R3, R4, R5; ADDI R6, R7, #8; BEQZ R8, LOOP, showing the stage of each instruction in every clock cycle. Assume ideal pipelining with no stalls.
画出 LW R1, 0(R2);ADD R3, R4, R5;ADDI R6, R7, #8;BEQZ R8, LOOP 的时序图,标出每个周期每条指令所在的级。假设理想流水、无停顿。
解答 · Solution
| Cycle 周期 | I1 LW | I2 ADD | I3 ADDI | I4 BEQZ |
|---|---|---|---|---|
| 1 | IF | |||
| 2 | ID | IF | ||
| 3 | EX | ID | IF | |
| 4 | MEM | EX | ID | IF |
| 5 | WB | MEM | EX | ID |
| 6 | WB | MEM | EX | |
| 7 | WB | MEM | ||
| 8 | WB |
Total 4 + 5 − 1 = 8 cycles. The four instructions share no registers, so ideal pipelining holds.
共 4 + 5 − 1 = 8 个周期。四条指令之间没有寄存器依赖,所以理想流水成立。
R2 = 1000, Mem[1030] = 300, R3 = 25, PC = 4000. Sequence: LW R1, 30(R2); ADDI R4, R1, #5; BEQZ R3, +20. For each: the values entering the ALU, ALUOut, whether data memory is accessed, the value written back (if any), and the final result after WB.
R2 = 1000,Mem[1030] = 300,R3 = 25,PC = 4000。序列:LW R1, 30(R2);ADDI R4, R1, #5;BEQZ R3, +20。对每条写出 ALU 输入、ALUOut、是否访问数据存储器、写回值(若有)和最终结果。
解答 · Solution
| Instruction 指令 | ALU inputs ALU 输入 | ALUOut | Data mem 数据存储器 | Result 结果 |
|---|---|---|---|---|
LW R1,30(R2) @4000 | A = 1000, Imm = 30 | 1030 | Read 读 | R1 ← 300 |
ADDI R4,R1,#5 @4004 | A = 300, Imm = 5 | 305 | No 否 | R4 ← 305 |
BEQZ R3,+20 @4008 | NPC = 4012, Imm = 20 | 4032 | No 否 | R3 = 25 ≠ 0 → not taken 不跳, PC = 4012 |
BEQZ sits at 4008, so NPC = 4012 and the target is 4012 + 20 = 4032; R3 = 25 is not zero, so the branch is not taken and no register is written.
BEQZ 位于 4008,NPC = 4012,目标 4012 + 20 = 4032;R3 = 25 不为 0,所以不跳转,也不写任何寄存器。
自测 · Self-Check
ADD R7, R3, R4,R3 = 25,R4 = 17,PC = 200。每一级的关键输出是什么?
为什么 ID 阶段对 R 型指令也做符号扩展?会出错吗?
LW 和 ADD 在 EX 阶段,ALU 第二个输入的 MUX 分别选什么?
非流水实现里 CPI = 4.6、时钟 0.8 ns;流水线 CPI = 1.2、时钟 1.0 ns。加速比多少?
为什么 b(目的寄存器号)要一路存到 M/W,而不是在 ID 直接写好?
流水线冒险
Final 范围理想流水线 CPI = 1,加速比等于级数。现实里,下一条指令经常不能在它该进入的那个周期进入:要么硬件资源被占(结构冒险),要么要用的数据还没算出来(数据冒险),要么连下一条该取哪条都还不知道(控制冒险)。这一课逐个分析三类冒险,以及转发、互锁、编译器调度、分支预测、延迟槽这些对策,最后都落到一个公式上:CPI = 1 + 每条指令的平均停顿周期。
12.1 三类冒险与 CPI
| 冒险 | 原因 | 例子 | 典型对策 |
|---|---|---|---|
| 结构冒险 | 硬件资源冲突 | 单端口存储器:取指和 load/store 同一周期都要用 | 双端口存储器、分离的指令/数据缓存 |
| 数据冒险 | 指令依赖前面指令的结果 | ADD R1,… 后紧跟 SUB …,R1,… | 转发、流水线互锁、编译器调度 |
| 控制冒险 | 分支结果还不知道 | BEQZ R1, Label | 提前判断分支、分支预测、延迟槽 |
例:10% 的指令停顿 1 周期。加权平均 0.9 × 1 + 0.1 × 2 = 1.1,或者 1 + 0.1 × 1 = 1.1,两种算法一样。加速比 5 / 1.1 = 4.54,比理想的 5 低。
Identify the hazard type: (1) a load instruction and an instruction fetch both need the same memory in the same cycle; (2) an instruction uses a register value before a previous instruction writes it; (3) the processor does not yet know whether a branch will be taken.
判断冒险类型:(1) load 和取指在同一周期要用同一个存储器;(2) 一条指令在前一条写寄存器之前就读了它;(3) 处理器还不知道分支跳不跳。
解答 · Solution
(1) Structural hazard (2) Data hazard (RAW) (3) Control hazard
(1) 结构冒险 (2) 数据冒险(RAW) (3) 控制冒险
A pipelined processor has an ideal CPI of 1 and an average of 0.25 stall cycles per instruction. Compute (1) the pipeline CPI, (2) the speedup relative to an unpipelined processor with CPI = 5.
流水线理想 CPI 1,平均每条停顿 0.25 周期。求 (1) 流水线 CPI;(2) 相对 CPI = 5 的非流水处理器的加速比。
解答 · Solution
12.2 结构冒险
两条或更多指令在同一周期要用同一个硬件资源,资源又不能同时满足,就是结构冒险。例子:寄存器堆只有一个写端口;指令和数据共用一个单端口存储器。解决方法:停顿到资源空出来,或者复制资源(双端口存储器有两套独立的地址、数据和控制信号,一个端口给 IF,一个给 MEM,冲突就消失了)。
讲义例子:LOAD 在第 4 周期处于 MEM,此时第 4 条指令正要 IF,两者都要访问存储器,只能让取指晚一拍。
结论:消除结构冒险往往比小幅提高主频更划算。
A processor uses a single-port memory. Can a structural hazard occur between LW R1,0(R2) and ADD R3,R4,R5 when LW is in the MEM stage and ADD is in the IF stage?
处理器使用单端口存储器。当 LW R1,0(R2) 处于 MEM 级、ADD R3,R4,R5 处于 IF 级时,会发生结构冒险吗?
解答 · Solution
Yes. LW needs a data read and ADD needs an instruction fetch in the same cycle, but there is only one memory port, so the fetch must be delayed by one cycle (a stall). Separate instruction/data memories or a dual-port memory remove the hazard.
会。LW 要读数据、ADD 要取指,两者在同一周期都要用唯一的存储器端口,只能把取指推迟一拍(插入停顿)。用分离的指令/数据存储器或双端口存储器可以消除。
25% of instructions are loads/stores and each causes one structural stall. Find (1) the average stall cycles per instruction, (2) the CPI.
25% 的指令是 load/store,每条造成 1 个结构停顿。求 (1) 平均每条停顿;(2) CPI。
解答 · Solution
12.3 数据冒险:RAW / WAR / WAW
流水线改变了对操作数的读写顺序,如果和顺序执行时的顺序不一致,指令就可能用到错误的值。设指令 i 在前、j 在后:
| 类型 | 含义 | 例子 | DLX 五级流水线会发生吗 |
|---|---|---|---|
| RAW 写后读 真相关 true dependence | j 在 i 写之前读了寄存器 | LW R1,0(R2)SUB R4,R1,R5 | 会。最常见,是 DLX 唯一要处理的数据冒险 |
| WAR 读后写 反相关 anti-dependence | j 在 i 读之前写了寄存器 | ADD R1,R2,R3(读 R2)LW R2,0(R5)(写 R2) | 不会:所有读都在 ID(第 2 级),所有写都在 WB(第 5 级),读永远早于后面指令的写 |
| WAW 写后写 输出相关 output dependence | j 比 i 先写同一寄存器,最终值错了 | LW R1,0(R2)ADD R1,R3,R4 | 不会:所有指令都在 WB 写、长度相同,按程序顺序到达 WB |
WAR 和 WAW 在指令延迟不等、乱序执行、超标量处理器里才会出现(例如 ADD 比 LW 先到写回级,LW 后写反而覆盖了 ADD 的结果)。讲义把它们列出来是为了完整。
看两条指令的寄存器:前一条的目的 = 后一条的源 → RAW;前一条的源 = 后一条的目的 → WAR;两条的目的相同 → WAW;都不相交 → 无。注意 LW 的目的是 Rt,SW 没有目的寄存器(它的两个寄存器都是源)。
Classify each dependency: (1) ADD R1,R2,R3 / SUB R4,R1,R5; (2) ADD R1,R2,R3 / LW R2,0(R5); (3) LW R1,0(R2) / ADD R1,R3,R4.
分类:(1) ADD R1,R2,R3 / SUB R4,R1,R5;(2) ADD R1,R2,R3 / LW R2,0(R5);(3) LW R1,0(R2) / ADD R1,R3,R4。
解答 · Solution
(1) RAW (ADD writes R1, SUB reads it) (2) WAR (ADD reads R2, LW writes it) (3) WAW (both write R1)
(1) RAW(ADD 写 R1,SUB 读 R1) (2) WAR(ADD 读 R2,LW 写 R2) (3) WAW(两条都写 R1)
Which of RAW, WAR and WAW can occur in a basic DLX 5-stage pipeline? Explain briefly.
RAW、WAR、WAW 中哪些会出现在基本 DLX 五级流水线里?简要说明。
解答 · Solution
Only RAW. WAR cannot occur: all reads happen in ID (stage 2) and all writes in WB (stage 5), so an earlier instruction always reads first. WAW cannot occur: all instructions have the same length and write in WB, so writes complete in program order.
只有 RAW。WAR 不会:所有读都在 ID(第 2 级),所有写都在 WB(第 5 级),前一条一定先读完。WAW 不会:所有指令长度相同、都在 WB 写,写回顺序与程序顺序一致。
12.4 寄存器分半周期与转发
序列 ADD R1,R2,R3 后面跟着 SUB R4,R1,R5、AND R6,R1,R7、OR R8,R1,R9、XOR R10,R1,R11。ADD 在第 5 周期(WB)才写 R1:
- 寄存器堆分半周期:一个周期内前半写、后半读。所以和 WB 同一周期的 ID 能读到新值。
- SUB 在第 3 周期读 R1、AND 在第 4 周期读 → 都读到旧值,有冒险。
- OR 在第 5 周期的后半读,正好拿到 ADD 前半写进去的新值 → 无冒险。XOR 更晚,也没问题。
- 结论:没有转发时,只有紧跟其后的两条指令需要停顿。
转发(forwarding,也叫旁路 bypassing):结果其实在 EX 结束时就已经算出来了,没必要等写回寄存器堆。把 EX/MEM(A/M)和 MEM/WB(M/W)流水线寄存器里的值直接送回 ALU 输入端:
- 硬件:在 ALU 每个输入前加一个 MUX,加上从 EX/MEM、MEM/WB 回来的通路,再加冒险检测逻辑来选输入。
- 正常情况选寄存器堆读出的值;检测到冒险就选转发来的值。
- ADD → SUB、SUB → AND 这类 ALU 到 ALU 的 RAW 全部可以零停顿解决。紧挨着的那条从 EX/MEM 拿值(Exercise 12.10 选 B)。
Determine which instructions have RAW hazards: ADD R1,R2,R3; SUB R4,R1,R5; AND R6,R1,R7; OR R8,R9,R10.
判断哪些指令有 RAW 冒险:ADD R1,R2,R3;SUB R4,R1,R5;AND R6,R1,R7;OR R8,R9,R10。
解答 · Solution
SUB and AND: both read R1, which ADD writes, before ADD has written it back. OR does not use R1, so it has no hazard.
SUB 和 AND:都读 ADD 写的 R1,而且在 ADD 写回之前读。OR 不用 R1,没有冒险。
Assume the register write occurs in the first half of a cycle and the read in the second half. ADD writes R1 in CC5. Which instruction can safely read R1 in CC5?
假设寄存器前半周期写、后半周期读。ADD 在 CC5 写 R1。哪条指令可以在 CC5 安全地读 R1?
解答 · Solution
The instruction that is in ID during CC5, i.e. the third instruction after ADD (OR R8,R1,R9 in the slide example): ADD writes in the first half of the cycle and OR reads the new value in the second half.
在 CC5 处于 ID 级的那条,也就是 ADD 之后的第三条指令(讲义例子里的 OR R8,R1,R9):ADD 前半周期写入,它后半周期读出,拿到的是新值。
Determine whether forwarding eliminates the hazard: (1) ADD R1,R2,R3 / SUB R4,R1,R5; (2) SUB R1,R2,R3 / AND R4,R1,R5.
判断转发能否消除冒险:(1) ADD R1,R2,R3 / SUB R4,R1,R5;(2) SUB R1,R2,R3 / AND R4,R1,R5。
解答 · Solution
Yes for both. Each is an ALU result used by the next ALU instruction, forwarded from EX/MEM to the ALU input with zero stalls.
两个都能。都是 ALU 结果给紧跟的 ALU 指令用,从 EX/MEM 转发到 ALU 输入,零停顿。
ADD R1,R2,R3 followed by SUB R4,R1,R5 requires forwarding. Which pipeline register provides the forwarded value? A. IF/ID B. EX/MEM C. Register file
ADD R1,R2,R3 后紧跟 SUB R4,R1,R5,需要转发。转发值来自哪个流水线寄存器?A. IF/ID B. EX/MEM C. 寄存器堆
解答 · Solution
B. EX/MEM. When SUB is in EX, ADD is in MEM and its result sits in the EX/MEM pipeline register (A/M in the slides).
B. EX/MEM。SUB 处于 EX 时,ADD 正好在 MEM,结果存在 EX/MEM(讲义叫 A/M)流水线寄存器里。
12.5 Load-use 冒险与硬件互锁
转发不能解决所有 RAW。LW R1,0(R2) 的数据要到 MEM 结束才从存储器读出来,而紧跟的 SUB R4,R1,R6 在下一个周期的 EX 开始就要用,时间上往回倒了,转发也来不及。
| 情形 | 需要停顿 |
|---|---|
| ALU → ALU(ADD → SUB、SUB → AND),有转发 | 0 |
| LW → 紧跟的使用者,有转发 | 1(load delay = 1) |
| LW → 紧跟的使用者,无转发 | 2(要等 WB) |
| ALU → 紧跟的使用者,无转发 | 2 |
硬件流水线互锁(interlock)检测到这种冒险后自动停顿流水线,插入一个气泡(NOP),依赖指令和后面所有指令都推迟一拍,正确性由硬件保证,不需要软件介入:
| 指令 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| LW R1,0(R2) | IF | ID | EX | MEM | WB | ||||
| SUB R4,R1,R6 | IF | ID | stall | EX | MEM | WB | |||
| AND R6,R1,R7 | IF | stall | ID | EX | MEM | WB | |||
| OR R8,R1,R9 | stall | IF | ID | EX | MEM | WB |
Determine whether a stall is needed for LW R1,0(R2) followed by SUB R4,R1,R5, assuming forwarding is available.
有转发时,LW R1,0(R2) 后紧跟 SUB R4,R1,R5,需要停顿吗?
解答 · Solution
Yes, one cycle. The loaded data is only available at the end of MEM, but SUB needs it at the start of its EX in that same cycle, so even forwarding is too late; after a one-cycle stall it is forwarded from MEM/WB.
需要,1 拍。load 的数据在 MEM 结束才从存储器读出,紧跟的 SUB 在同一周期的 EX 开始就要用,转发也来不及;停 1 拍后从 MEM/WB 转发。
20% of instructions are loads, half of them are immediately followed by a dependent instruction, and each load-use hazard causes one stall. Find the CPI.
20% 的指令是 load,其中一半紧跟着依赖它的指令,每次 load-use 停 1 拍。求 CPI。
解答 · Solution
12.6 编译器调度消除停顿
与其让硬件停顿,不如让编译器把不相关的指令挪进停顿的位置,让 CPU 在等数据的那一拍干点有用的活。只要被移动的指令和周围没有依赖,程序结果就不变。
| 原始代码 | 调度后 |
|---|---|
| LW Rb,b LW Rc,c ADD Ra,Rb,Rc ← 停 1 拍 SW a,Ra LW Re,e LW Rf,f SUB Rd,Re,Rf ← 停 1 拍 SW d,Rd |
LW Rb,b LW Rc,c LW Re,e ← 提前,填掉空拍 ADD Ra,Rb,Rc LW Rf,f SW a,Ra SUB Rd,Re,Rf SW d,Rd |
调度后 ADD 距离 LW Rc 隔了一条,SUB 距离 LW Rf 也隔了一条,两次 load-use 停顿都消失了,不需要额外硬件。
输入一段 DLX 代码(每行一条,支持 ADD/SUB/AND/OR/XOR/SLT、ADDI/SUBI/ANDI/ORI、LW、SW、BEQZ/BNEZ、J、NOP,标号写在行首加冒号),选择是否转发、分支在哪一级判定和用什么方案,自动画出每条指令每个周期所处的阶段,并列出所有冒险和停顿数。
模型说明:寄存器堆前半周期写、后半周期读;有转发时 ALU 结果在下一周期的 EX 可用,load 结果在 MEM 之后的下一周期 EX 可用(SW 要存的数据在 MEM 才用);无转发时使用者必须在生产者 WB 的同一周期或之后才能读寄存器。分支的控制代价按讲义口径单独计算(EX 判定代价 3、ID 判定代价 1),分支比较用到的寄存器按普通 ALU 操作数处理。时序表只画实际执行路径上的指令,跳转后被冲掉的错误路径指令不画出来,用 bubble 表示损失的周期。
12.7 控制冒险
原始 DLX 在 EX 级才算出分支目标和判断条件。等结果出来时,后面已经有三条顺序指令进了流水线;如果分支跳转,这些指令都不该执行。最简单的办法是停顿:
严格说,EX 结束时结果就知道了,下一拍就能取目标,代价是 2。讲义按经典 DLX 的做法,在 MEM 级才把新 PC 写回,所以记为 3 周期。考试按讲义:EX 判定 = 3,ID 判定 = 1。题目给定了代价(如 Tutorial 12.8 的"错误预测代价 3")就直接用给定值。
实测统计:条件分支约占指令的 14–16%;向前分支约 60% 跳转,向后分支(循环)约 85% 跳转,整体约 67% 跳转。分支又多又常跳,所以值得做预测。
Branch frequency = 30%, branch penalty = 3 cycles. Find the CPI.
分支占 30%,分支代价 3 周期。求 CPI。
解答 · Solution
The enhanced DLX reduces the branch penalty from 3 cycles to 1 cycle. With a branch frequency of 30%, compute the new CPI.
改进 DLX 把分支代价从 3 周期降到 1 周期。分支占 30%,求新的 CPI。
解答 · Solution
12.8 四种分支处理方案
| 方案 | 做法 | 每个分支的平均代价 | CPI |
|---|---|---|---|
| 1 停顿 | 知道结果前不取新指令 | 总是 1 | 1.30 |
| 2 预测不跳转 | 继续取 PC + 4;猜错(实际跳转)就冲掉取错的那条,从目标重新取 | 0.47 × 0 + 0.53 × 1 = 0.53 | 1.159 |
| 3 预测跳转 | 从目标地址取;但 DLX 在 ID 结束才知道目标,猜对也要等一拍 | 0.53 × 1 + 0.47 × 1 = 1 | 1.30 |
| 4 延迟分支 | 分支后面那一条(延迟槽)无论跳不跳都执行,由编译器放一条有用的指令进去 | 60% 能填 × 80% 有用 = 48% 无代价,0.52 × 1 | 1.156 |
填延迟槽的三种来源
| 来源 | 做法 | 安全性 | 适用 |
|---|---|---|---|
| From before | 把分支前面一条无关指令挪进槽里(分支条件不能依赖它) | 高,本来就要执行 | 首选 |
| From target | 把跳转目标处的第一条指令复制进槽里 | 中:不跳转时它也会执行 | 常跳的分支,比如循环末尾的向后分支 |
| From fall-through | 把不跳转时会执行的下一条挪进槽里 | 中:跳转时它也会执行 | 常不跳的分支,比如 if 语句的向前分支 |
Tutorial 12.3 是一个带修改的 from-before 例子:循环里把 SW 200(R3),R2 挪到 BNEZ 后面的槽里,但它前面的 SUBI R3,R3,#4 已经把 R3 减了 4,所以偏移要改成 SW 204(R3),R2 才能存到原来的地址。
可取消分支(canceling / nullifying branch):编译器同时给出预测方向,预测对了槽里的指令正常执行,预测错了就把它当 NOP 作废,不会产生错误结果。延迟槽的问题是合适的指令常常找不到、from target / fall-through 不一定安全、流水线一深就要填好几个槽,所以现代 CPU 改用硬件动态分支预测:猜对零代价,猜错冲刷流水线重来。
| 做法 | 谁来解决 | 性能 | 硬件复杂度 |
|---|---|---|---|
| NOP 延迟槽 | 没人 | 差(每个分支浪费 1 拍) | 很低 |
| 填充延迟槽 | 编译器 | 较好(常为 0 代价) | 低 |
| 分支预测 | 硬件 | 最好 | 高 |
Branch frequency = 30%, predict-not-taken accuracy = 47%. Compute the CPI (misprediction penalty = 1 cycle).
分支占 30%,预测不跳转的准确率 47%。求 CPI(猜错代价 1 周期)。
解答 · Solution
Code: ADD R10,R11,R12; BEQZ R1,Label; NOP; SUB R4,R5,R6. Which instruction could be moved into the delay slot? A. ADD R10,R11,R12 B. SUB R4,R5,R6
代码:ADD R10,R11,R12;BEQZ R1,Label;NOP;SUB R4,R5,R6。哪条可以挪进延迟槽?A. ADD R10,R11,R12 B. SUB R4,R5,R6
解答 · Solution
A (from before): ADD executes anyway and does not affect the branch condition R1, so it is correct whether or not the branch is taken. SUB belongs to the fall-through path; in the slot it would also run when the branch is taken, which is unsafe.
A(from before):ADD 本来就要执行,又与分支条件 R1 无关,放进槽里无论跳不跳都正确。SUB 只在不跳转时执行,挪进槽里会在跳转时也被执行,不安全。
Consider LW R1,0(R2); ADD R3,R1,R4; BEQZ R3,Label; SUB R5,R6,R7. Identify (1) structural hazards, (2) data hazards, (3) control hazards, (4) the number of stall cycles assuming forwarding and a 1-cycle branch penalty.
代码:LW R1,0(R2);ADD R3,R1,R4;BEQZ R3,Label;SUB R5,R6,R7。找出 (1) 结构冒险;(2) 数据冒险;(3) 控制冒险;(4) 有转发、分支代价 1 拍时的停顿周期数。
解答 · Solution
(1) None if instruction and data memories are separate; with a single-port memory, LW's MEM would clash with a later IF. (2) LW → ADD (R1, load-use, 1 stall); ADD → BEQZ (R3, resolved by forwarding in the slides' model). (3) SUB after BEQZ must wait for the branch outcome: 1-cycle penalty. (4) 2 stall cycles in total.
Note: if the branch really compares in ID (enhanced DLX), R3 is needed in ID but ADD produces it only at the end of EX, adding one more stall (3 in total). The slides do not discuss this; answer 2.
(1) 指令与数据存储器分离时没有;若是单端口存储器,LW 的 MEM 会和后面某条的 IF 冲突。(2) LW → ADD(R1,load-use,停 1 拍);ADD → BEQZ(R3,按讲义口径靠转发解决)。(3) BEQZ 之后的 SUB 要等分支结果,代价 1 拍。(4) 共 2 个停顿周期。
补充:如果分支真在 ID 级比较(改进 DLX),R3 必须在 ID 就拿到,而 ADD 的结果在 EX 结束才有,还要再停 1 拍,共 3 拍。讲义没有讨论这一点,按 2 拍答。
| 方案 | 每个分支平均代价 | CPI | 加速比(对 5 级理想 = 5) |
|---|
自测 · Self-Check
ADD R1,R2,R3 后面是 LW R2,0(R5)。这是什么冒险?会在 DLX 里出问题吗?
有转发时,LW R1,0(R2) 紧跟 ADD R3,R1,R4,需要停几拍?为什么转发救不了?
为什么 WAW 在 DLX 五级流水线里不会发生?
15% 是分支,60% 跳转,预测不跳转,猜错代价 3。CPI 多少?和每个分支都停 3 拍比快多少?
DLX 里"预测跳转"为什么没有比"停顿"好?
Part 2 的两条主线其实是同一个思路:CPU 太快了,别让它等。存储层次用局部性把"等内存"压到 AMAT 和停顿周期里;流水线用时间并行把"等上一条指令"压到 CPI 接近 1,再用转发、互锁、调度、预测把冒险造成的停顿一点点抠回来。考试题几乎都能写成两个公式之一:AMAT = HT + MR × MP 和 CPI = 理想 CPI + 平均停顿。回到侧边栏勾选已经掌握的课,然后去习题小程序把 Tutorial 7–12 做一遍。