Files

329 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- 计算机系统结构
- 复习
- Cache
create time: 2026-06-15 10:00
---
# 存储系统与 Cache
## 概述
本文档系统讲解存储层次结构、Cache 的工作原理(映像方式、替换策略、写策略)、命中率计算以及 17 种 Cache 优化技术。Cache 是计算机系统结构的核心考点,几乎每年都有大分值的计算和分析题。
> [!tip] 考试重点
> Cache 命中率计算、平均访存时间计算、分离 Cache vs 混合 Cache 的性能对比是必考内容。17 种优化技术的分类和代表性技术需熟记。
## 正文
### 一、存储层次结构
```mermaid
graph TD
A["Register"] --> B["L1 Cache"]
B --> C["L2 Cache"]
C --> D["Main Memory"]
D --> E["Disk/SSD"]
style A fill:#ff8a80
style B fill:#ff80ab
style C fill:#ea80fc
style D fill:#82b1ff
style E fill:#80d8ff
```
| 层次 | 容量 | 速度 | 价格/位 | 管理方式 |
|:----:|:----:|:----:|:-------:|:--------:|
| 寄存器 | KB | 最快 | 最贵 | 编译器 |
| L1 Cache | 32~64 KB | ~1 ns | 较贵 | 硬件 |
| L2 Cache | 256 KB~1 MB | ~5 ns | 中等 | 硬件 |
| 主存 | 4~64 GB | ~100 ns | 便宜 | OS+硬件 |
| 磁盘 | TB 级 | ~10 ms | 最便宜 | OS |
> [!question] 为什么存储系统要分层?
> 单一存储器无法同时满足"大容量、高速度、低价格"的要求。分层设计利用**局部性原理**,将频繁访问的数据放在快速但小的上层,不常用的放在慢速但大的下层,以较低成本获得接近最快速度的平均性能。
### 二、Cache 基本原理
#### 2.1 映像方式
| 映像方式 | 原理 | 优点 | 缺点 |
|----------|------|------|------|
| **直接映像** | 主存块只能映射到 Cache 的固定位置 | 简单、命中时间短 | 冲突不命中率高 |
| **全相联** | 主存块可映射到 Cache 任意位置 | 冲突不命中率最低 | 硬件开销大、查找慢 |
| **组相联** | 折中方案,分为 $2^n$ 组,组内全相联 | 平衡性能和开销 | 复杂度适中 |
> [!note] 组相联 Cache 的命名
> "$n$ 路组相联"表示每组有 $n$ 个 Cache 行。2 路组相联 = 每组 2 行,4 路组相联 = 每组 4 行。
**地址解析格式**:
处理器发出的地址需要被拆分为 Tag / Index / Offset 三部分,不同映像方式的拆分规则不同:
```mermaid
graph LR
subgraph "直接映像地址"
A1["Tag"] --- A2["Index"] --- A3["Block Offset"]
end
subgraph "全相联地址"
B1["Tag"] --- B2["Block Offset"]
end
subgraph "组相联地址"
C1["Tag"] --- C2["Set Index"] --- C3["Block Offset"]
end
```
各字段含义:
| 字段 | 含义 | 位数计算 |
|------|------|----------|
| **Block Offset** | 块内偏移,定位块中具体哪个字节 | $\log_2(\text{块大小})$ |
| **Index**(直接映像) | 定位 Cache 中的哪一行 | $\log_2(\text{Cache 行数})$ |
| **Set Index**(组相联) | 定位 Cache 中的哪一组 | $\log_2(\text{组数})$ |
| **Tag** | 标记位,用于比对确认是否命中 | 地址总位数 - Index位数 - Offset位数 |
> [!example] 地址位分解示例
>
> **已知**:32 位地址,Cache 容量 16KB,块大小 64B,采用 4 路组相联。
>
> **计算各字段位数**:
>
> - Offset 位数 = $\log_2(64) = 6$ 位
> - Cache 行数 = $16\text{KB} / 64\text{B} = 256$ 行
> - 组数 = $256 / 4 = 64$ 组
> - Set Index 位数 = $\log_2(64) = 6$ 位
> - Tag 位数 = $32 - 6 - 6 = 20$ 位
>
> **地址 `0x1A2B3C47` 的解析**:
>
> | 字段 | 位数 | 二进制值 |
> |:----:|:----:|:---------|
> | Tag(高 20 位) | 31~12 | `0001 1010 0010 1011 0011` |
> | Set Index(6 位) | 11~6 | `110001` = 第 49 组 |
> | Offset(6 位) | 5~0 | `000111` = 块内第 7 字节 |
>
> > [!question] 如果改成直接映像呢?
> > 直接映像:Index = $\log_2(256) = 8$ 位,Tag = $32 - 8 - 6 = 18$ 位。
> > 同一地址会映射到第 `10010001` = 第 145 行,而不是第 49 组的某一行。
> > 相联度越高,Index 位数越少,Tag 位数越多,硬件比对逻辑越复杂。
#### 2.2 替换策略
| 策略 | 原理 | 特点 |
|------|------|------|
| **LRU**(最近最少使用) | 替换最久未被访问的块 | 最常用,效果好 |
| **FIFO** | 替换最早进入的块 | 简单,但可能替换活跃块 |
| **随机** | 随机选择替换块 | 实现简单,效果尚可 |
#### 2.3 写策略
| 策略 | 命中时 | 不命中时 | 一致性 |
|------|--------|----------|--------|
| **写直达** | 同时写 Cache 和主存 | Write-Allocate 或 No-Write-Allocate | 简单但总线流量大 |
| **写回** | 只写 Cache,标记为脏 | Write-Allocate | 复杂但总线流量小 |
### 三、Cache 性能计算
#### 3.1 命中率与不命中率
$$\text{不命中率} = w_I \times mr_I + w_D \times mr_D$$
其中 $w_I$、$w_D$ 分别为指令和数据的访问占比,$mr_I$、$mr_D$ 为各自的不命中率。
> [!example] 完整例题:地址序列命中/不命中判断
>
> **已知**:直接映像 Cache,4 行,块大小 16B(4 个字,每字 4B)。地址按字节编址。
>
> 地址结构:`[ Tag(高位) | Index(2位) | Offset(4位) ]`
>
> - Index 位数 = $\log_2(4) = 2$ 位(4 行直接映像)
> - Offset 位数 = $\log_2(16) = 4$ 位(块大小 16B)
>
> **访问以下地址序列**(十进制,假设地址按字节编址):
>
> | 序号 | 地址(十进制) | 地址(二进制) | Tag | Index | Offset | 块号 | 行号 | 结果 |
> |:----:|:-------------:|:--------------:|:---:|:-----:|:------:|:----:|:----:|:----:|
> | 1 | 0 | `00000000` | 0 | 00 | 0000 | 0 | 0 | **不命中**(冷启动) |
> | 2 | 4 | `00000100` | 0 | 00 | 0100 | 0 | 0 | **命中**(同一块) |
> | 3 | 16 | `00010000` | 0 | 01 | 0000 | 1 | 1 | **不命中**(新行) |
> | 4 | 132 | `10000100` | 8 | 01 | 0100 | 8 | 1 | **不命中**(替换行 1) |
> | 5 | 136 | `10001000` | 8 | 01 | 1000 | 8 | 1 | **命中**(同一块) |
> | 6 | 64 | `01000000` | 4 | 00 | 0000 | 4 | 0 | **不命中**(替换行 0) |
> | 7 | 48 | `00110000` | 3 | 00 | 0000 | 3 | 0 | **不命中**(替换行 0) |
> | 8 | 64 | `01000000` | 4 | 00 | 0000 | 4 | 0 | **不命中**(已被替换) |
>
> **Cache 行状态变化**:
>
> | 步骤 | 行 0 | 行 1 | 行 2 | 行 3 |
> |:----:|:----:|:----:|:----:|:----:|
> | 初始 | 空 | 空 | 空 | 空 |
> | 访问 0 | Tag=0 | 空 | 空 | 空 |
> | 访问 4 | Tag=0 | 空 | 空 | 空 |
> | 访问 16 | Tag=0 | Tag=0 | 空 | 空 |
> | 访问 132 | Tag=0 | **Tag=8** | 空 | 空 |
> | 访问 64 | **Tag=4** | Tag=8 | 空 | 空 |
> | 访问 48 | **Tag=3** | Tag=8 | 空 | 空 |
> | 访问 64 | **Tag=4** | Tag=8 | 空 | 空 |
>
> 命中率 = $2/8 = 25\%$
>
> > [!question] 观察到了什么?
> > 地址 64 在第 6 次访问时被调入,但第 7 次访问 48 时将它替换出,导致第 8 次再次访问 64 时又不命中——这就是典型的**颠簸(Thrashing)**现象。解决方法:提高相联度或增大 Cache 容量。
#### 3.2 平均访存时间
$$\text{平均访存时间} = \text{命中时间} + \text{不命中率} \times \text{不命中开销}$$
> [!example] 分离 Cache vs 混合 Cache(详解)
>
> **已知条件**:
> - 程序中 78% 是指令访问(取指),22% 是数据访问(load/store)
> - 不命中开销均为 40 周期
>
> **方案一:分离 Cache**(指令 16KB + 数据 16KB = 共 32KB)
> - 指令 Cache 不命中率 $mr_I = 1\%$,数据 Cache 不命中率 $mr_D = 5\%$
> - 命中时间均为 1 周期(指令和数据各有独立端口,不会冲突)
>
> 逐步计算:
> 1. 指令部分平均访存时间 = $1 + 0.01 \times 40 = 1.40$ 周期
> 2. 数据部分平均访存时间 = $1 + 0.05 \times 40 = 3.00$ 周期
> 3. 加权平均 = $0.78 \times 1.40 + 0.22 \times 3.00 = 1.092 + 0.660 = \mathbf{1.752}$ 周期
>
> **方案二:混合 Cache**(统一 32KB)
> - 整体不命中率 $mr = 1.5\%$(因为容量相同,但指令和数据共享,冲突不命中增加)
> - 命中时间:取指仍为 1 周期,但 load/store 需 **+1 周期**(因为指令和数据共用端口,load/store 需要额外仲裁)
>
> 逐步计算:
> 1. 取指平均访存时间 = $1 + 0.015 \times 40 = 1.60$ 周期
> 2. 数据平均访存时间 = $2 + 0.015 \times 40 = 2.60$ 周期(命中时间 2 周期)
> 3. 加权平均 = $0.78 \times 1.60 + 0.22 \times 2.60 = 1.248 + 0.572 = \mathbf{1.820}$ 周期
>
> **结论**:分离 Cache 更优(1.752 < 1.820),节省约 $3.7\%$ 的平均访存时间。
>
> > [!question] 什么时候混合 Cache 反而更好?
> > 当数据和指令的访问模式不均匀时——比如某个阶段全是取指(循环),另一阶段全是数据访问——混合 Cache 能让 32KB 容量被充分利用,而分离 Cache 各 16KB 可能不够用。此时混合 Cache 的不命中率可能显著低于分离方案。
#### 3.3 CPU 时间与 Cache 的关系
$$CPU 时间 = IC \times (CPI_{exe} + \frac{\text{访存次数}}{指令} \times mr) \times \tau$$
### 四、17 种 Cache 优化技术
17 种优化技术围绕 Cache 性能公式中的三个因素展开,分类关系如下:
```mermaid
graph TD
ROOT["Cache 性能优化"] --> A["降低不命中率 (8种)"]
ROOT --> B["减少不命中开销 (5种)"]
ROOT --> C["减少命中时间 (4种)"]
A --> A1["增大块大小"]
A --> A2["增大 Cache 容量"]
A --> A3["提高相联度"]
A --> A4["伪相联 Cache"]
A --> A5["硬件预取"]
A --> A6["编译器控制预取"]
A --> A7["编译优化"]
A --> A8["Victim Cache"]
B --> B1["非阻塞 Cache"]
B --> B2["写合并"]
B --> B3["请求字优先"]
B --> B4["写缓冲"]
B --> B5["早重启"]
C --> C1["小容量简单 Cache"]
C --> C2["虚拟 Cache"]
C --> C3["流水化 Cache 访问"]
C --> C4["路预测"]
```
| 分类 | 技术 | 基本思想 | 影响 |
|:----:|------|----------|------|
| **降低不命中率**(8种) | 增大块大小 | 利用空间局部性 | 块过大会增加不命中开销 |
| | 增大 Cache 容量 | 利用时间局部性 | 增加命中时间 |
| | 提高相联度 | 减少冲突不命中 | 增加命中时间 |
| | 伪相联 Cache | 降低冲突不命中 | 复杂度适中 |
| | 硬件预取 | 提前调入数据 | 增加总线流量 |
| | 编译器控制预取 | 编译器插入预取指令 | 增加指令开销 |
| | 编译优化 | 优化访问模式 | 依赖编译器能力 |
| | Victim Cache | 小全相联 Cache 保存替换出的块 | 减少冲突不命中 |
| **减少不命中开销**(5种) | 非阻塞 Cache | 不命中时允许后续请求 | 提高流水线效率 |
| | 写合并 | 合并多次写操作 | 减少写缓冲区占用 |
| | 请求字优先 | 优先传输请求的字 | 减少等待时间 |
| | 写缓冲 | 写操作进入缓冲区异步执行 | 减少写延迟 |
| | 早重启 | 不命中时尽早返回请求字 | 减少等待时间 |
| **减少命中时间**(4种) | 小容量简单 Cache | 降低命中时间 | 适用于 L1 Cache |
| | 虚拟 Cache | 用虚拟地址直接索引 | 增加别名问题 |
| | 流水化 Cache 访问 | Cache 访问分段流水化 | 提高时钟频率 |
| | 路预测 | 预测 Cache 行所在路 | 预测错误时需重取 |
> [!warning] 优化技术的权衡
> 几乎每种优化技术都存在"收益-代价"权衡。例如增大块大小降低了强制性不命中,但增加了不命中开销;提高相联度降低了冲突不命中,但增加了命中时间。设计时需要根据具体场景权衡。
### 五、不命中的三种类型
| 类型 | 原因 | 解决方法 |
|------|------|----------|
| **强制性不命中**(Compulsory) | 首次访问某块 | 增大块大小、预取 |
| **容量不命中**(Capacity) | Cache 容量不足 | 增大 Cache 容量 |
| **冲突不命中**(Conflict) | 映射冲突 | 提高相联度、Victim Cache |
### 六、综合计算题
> [!example] 综合计算题:Cache 参数计算与性能分析
>
> **已知条件**:
> - 处理器 32 位地址
> - Cache 容量 32KB
> - 块大小 64B
> - 采用 8 路组相联
> - LRU 替换策略
> - 写回策略
> - 命中时间 1 个时钟周期
> - 不命中开销 100 个时钟周期
> - 不命中率 2%
> - 程序中 30% 为访存指令(load/store)
>
> **问题 1:计算 Tag / Index / Offset 位数**
>
> - Cache 行数 = $32\text{KB} / 64\text{B} = 512$ 行
> - 组数 = $512 / 8 = 64$ 组
> - Offset 位数 = $\log_2(64) = 6$ 位
> - Index 位数 = $\log_2(64) = 6$ 位
> - Tag 位数 = $32 - 6 - 6 = 20$ 位
>
> | 字段 | Tag | Set Index | Block Offset |
> |:----:|:---:|:---------:|:------------:|
> | 位数 | 20 | 6 | 6 |
> | 位置 | [31:12] | [11:6] | [5:0] |
>
> **问题 2:计算平均访存时间**
>
> $$\text{AMAT} = \text{命中时间} + \text{不命中率} \times \text{不命中开销}$$
> $$= 1 + 0.02 \times 100 = 1 + 2 = \mathbf{3 \text{ 周期}}$$
>
> **问题 3:计算对 CPI 的影响**
>
> 假设理想 CPI(无 Cache 不命中)为 2.0:
>
> $$\text{实际 CPI} = \text{CPI}_{exe} + \text{访存指令比例} \times \text{不命中率} \times \text{不命中开销}$$
> $$= 2.0 + 0.30 \times 0.02 \times 100 = 2.0 + 0.6 = \mathbf{2.6}$$
>
> Cache 不命中使 CPI 增加了 $0.6$,性能下降了 $30\%$。
>
> **问题 4:如果将 Cache 容量增大到 64KB(不命中率降为 1%),是否值得?**
>
> 新的 CPI = $2.0 + 0.30 \times 0.01 \times 100 = 2.0 + 0.3 = 2.3$
>
> 性能提升 = $(2.6 - 2.3) / 2.6 = 11.5\%$
>
> > [!question] 如何权衡?
> > 64KB Cache 的面积是 32KB 的约 2 倍,但性能仅提升 11.5%。若芯片面积紧张,可考虑用其他优化技术(如提高相联度、硬件预取)来替代简单增大容量——这就是**17 种优化技术的组合运用**。
## 关联笔记
- [[计算机系统结构/复习文档/计算机系统结构基础与定量原理]]
- [[计算机系统结构/复习文档/总线与I/O系统]]
- [[计算机系统结构/index|试题册索引]]