DDCA —— 缓存(Cache):缓存体系结构、缓存操作

1. 存储器层次(The Memory Hierarchy)

1.1 现代系统中的存储器

其中包括L1、L2、L3和DRAM

1.2 存储器的局限

理想存储器的需求如下:

  • 零延迟
  • 容量无限
  • 零成本
  • 带宽无限
  • 零功耗

但理想存储器的需求彼此冲突:

  • 容量更大的存储器意味着更大的延迟:需要花更长的时间来确定数据所在位置
  • 更快的存储器价格更贵
    • 存储器工艺:SRAM vs. DRAM vs. Disk vs. Tape
  • 更高带宽的存储器更贵
    • 需要更多的banks,更多的端口,更快的工作频率,更快的工艺

1.3 为什么使用存储器分层结构

我们想要存储器又快,容量又大,这并不能通过单层存储器实现。

Idea: 多层次存储(离处理器越远的存储器容量越大,速度越慢)并且确保处理器所需的大部分数据能够保持在较快的层次中。

Memory System:

只要拥有良好的 引用局部性 ,存储器层次结构可以使得存储器又快,容量又大。

1.4 存储器分层结构

从小到大,从快到慢依次包括: 寄存器(Register File) 缓存(Cache) 主存储器(Main Memory) 硬盘(Hard Disk)

1.5 Locality(局部性)

  • 通过一个人不久的过去能够预测其不远的将来

  • 时间局部性(Temporal Locality): 如果你刚好干了某事,那么你极有可能很快再干同样的事情。

    • 你今天在这间教室,那么将有很大可能你将会有规律地一次又一次出现在这个教室。
  • 空间局部性(Spatial Locality): 如果你做了某件事情,你很可能会做类似/相关的事情(在空间上)。

    • 每次我在这间教室找到你时,你可能都和同样的人坐得很近,或者坐在很近的座位上。

1.6 内存局部性

  • 一个”典型“程序在内存引用方面有很多局部性
    • loop 组成的程序
  • 时间: 程序往往会在一小段时间内多次引用同一内存位置
  • 空间: 程序倾向于在一个时间窗口内引用附近的内存位置
    • 指令内存引用 → 通常是顺序/流式(顺序执行指令)
    • 对数组/向量的引用 → 通常是流式/串式的

1.7 缓存基础

1.7.1 利用时间局部性

  • Idea: 将最近访问过的数据存储在自动管理的快速内存(称为缓存(cache))
  • 预期结果: 相同的存储器地址将很快被访问

1.7.2 利用空间局部性

  • Idea: 在自动管理的快速存储器中,将数据存储在与最近访问的地址相邻的地址中
    • 逻辑上将内存划分为大小相等的区块
    • 将访问的区块整个提取到缓存中
  • 预期结果: 与当前访问内存位置相邻的内存位置将很快被访问

2. 缓存分层结构

2.1 流水线设计中的缓存

  • 缓存需要与流水线紧密集成

    • 理想情况下希望能在1个周期内访问数据,这样依赖load的操作才不会停滞
  • 然而高频流水线 → 不能让 cache 太大

    • 但是,我们需要大的 cache 和流水线设计
  • Idea:缓存层次结构

其中,一级缓存(L1)设计决策主要由处理器的频率决定,并且L1非常小;二级缓存(L2)比L1大,但访问速度也比L1慢。

2.2 最基本的Cache结构

其中包括一个标签存储(Tag Store)和一个数据存储(Data Store)。

当处理器需要查询缓存时,它需要向Tag Store发送一个地址。

如果Tag Store显示该地址存在缓存中,那么你可以从Data Store中获取数据。

如果Tag Store显示该地址不在缓存中,那么你必须去主内存取数据。

2.3 存储器分层结构设计分析

2.3.1 分层延迟分析

  • 程序查询该层,会得到访问数据命中或未命中(Hit/Miss),这意味着数据在该层或不在。

  • 假设给定的存储器分层结构第 \(i\) 层具有 固有访问时间(technology-intrinsic access time) \(t_i\)

  • 但实际的 感知访问时间(perceived access time) \(T_i\) 要比 \(t_i\) 长。

  • 除了最外层的层次结构外,在查找给定地址时,有以下几种情况

    • 当你命中”hit“的时候(命中率 hit-rate \(h_i\) ),访问时间为 \(t_i\)
    • 当你未命中”miss“的时候(未命中率 miss-rate \(m_i\) ),访问时间为 \(t_i + T_{i+1}\)
    • \(h_i + m_i = 1\)
  • 因此 \(T_i = h_i \cdot t_i + m_i \cdot (t_i + T_{i+1})\) ,即 \(T_i = t_i + m_i \cdot T_{i+1}\)

2.3.2 分层结构设计考虑因素

  • 递归延迟方程

  • 目标: 在允许的成本范围内实现所需的 \(T_1\)

  • 希望 \(T_i \approx t_i\) ,虽然这是不可能的

  • 降低未命中率 \(m_i\)

    • 增加容量 \(C_i\) 可降低 \(m_i\) ,但这样会增加固有访问时间 \(t_i\)
    • 通过更智能的缓存管理(替换预测不需要的内容,预取预测需要的内容)降低 \(m_i\)
  • 降低下一层级的感知访问时间 \(T_{i+1}\)

    • 使用更快的层级,但要注意成本的增加
    • 引入中间层次作为妥协

2.3.3 举例:Intel奔腾 4

  • 90nm P4, 3.6 GHz
  • L1 D-cache
    • \(C_1\) = 16 kB
    • \(t_1\) = 4 cyc int / 9 cycle fp
  • L2 D-cache
    • \(C_2\) = 1024 kB
    • \(t_2\) = 18 cyc int / 18 cyc fp
  • Main memory
    • \(t_3\) = ~ 50ns or 180 cyc

\(T_i = t_i + m_i\cdot T_{i+1}\)

  • if \(m_1=0.1, m_2=0.1\)
    • \(T_2=t_2 + m_2T_3 = 18+0.1\times 180 =36cyc\)
    • \(T_1=t_1+m_1T_2=4+0.1\times36=7.6cyc\)
  • if \(m_1=0.01, m_2=0.01\)
    • \(T_1=4.2, T_2=19.8\)
  • if \(m_1=0.05, m_2=0.01\)
    • \(T_1=5.00, T_2=19.8\)
  • if \(m_1=0.01, m_2=0.50\)
    • \(T_1=5.08, T_2=108\)

3. 缓存基础和操作

3.1 缓存

  • 任何 "存储" 已使用(或已生成)数据的结构
    • 以避免重复从头开始复制/获取数据所需的长延时操作
    • e.g. a web cache
  • 在处理器设计中最常见的是:自动管理的内存结构
    • e.g. 在快速 SRAM 中存储最频繁或最近访问过的 DRAM 内存位置,以避免重复支付 DRAM 访问延迟费用
    • 管理策略:
      • What data bring to cache?
      • What data keep in cache?

3.2 基本硬件缓存

  • 我们将从基本的硬件缓存示例和缓存操作开始
  • 然后,我们将正式确定一些缓存的基本概念
  • 最后,我们将研究各种想法和创新,以提高缓存性能

3.2.1 访问缓存

  • 缓存大小: 缓存的总大小是64字节,由8个8字节的存储单元(8-byte words)组成,每个存储单元可以存储8个字节的数据。

  • 字节地址(Byte Address): 后3位表示字节的 Offset ,由于每个存储单元为8-byte。

  • 索引位(Index Bits): 有3位 index bits ,由于有8个缓存行( \(2^3 = 8\) ),每行可以存储一个8字节的数据块。

直接映射缓存(Direct-mapped Cache) :每个主存地址直接映射到缓存中的唯一位置。地址中的索引位决定数据在缓存中的位置,所以每个地址都会对应到一个唯一的缓存行。

3.2.2 Tag Array(标签数组)

当主存地址映射到某一缓存行时,系统会将该地址的Tag值与标记数组中的Tag值进行比较,以判断数据是否在缓存中。 如果Tag值匹配,说明数据已在缓存中(命中);如果不匹配,则说明需要从主存中加载该数据(未命中)。

3.2.3 增加 Block 大小

从每行存储8-byte数据变为每行存储32-byte数据:

  • 缓存大小: 缓存的总大小是256字节,由8个32字节的存储单元(32-byte words)组成,每个存储单元可以存储32个字节的数据。

  • 字节地址(Byte Address): 后5位表示字节的 Offset ,由于每个存储单元为32-byte。

  • 索引位(Index Bits): 有3位 index bits ,由于有8个缓存行( \(2^3 = 8\) ),每行可以存储一个32字节的数据块。

更大的缓存行的影响

  • 较小的标签数组(Tag Array) :因为每个缓存行可以容纳更多数据,因此只需较少的行数来覆盖同样的数据范围,减少了Tag数组的大小。
  • 减少缓存缺失 :由于 空间局部性(Spatial Locality) ,访问某个数据块后,往往会访问附近的数据。较大的缓存行可以一次性加载更多相邻数据,减少后续访问时的缺失率。

3.2.4 关联性(Associativity)

两路组相联缓存(2-way set associative cache)的结构:

  • 标记数组(Tag Array) :由于是两路组相联缓存,每个地址会对应多个”路“(Way)。在这个例子中有两条路(Way-1 和 Way-2),因此标记数组包含两组标签,以允许每组缓存行可以存储不同的数据块。
  • 数据数组(Data Array) :数据分为两条路(Way-1 和 Way-2),每个路都有自己的数据存储块。相比于直接映射缓存,组相联缓存能够容纳更多的相同索引的数据块,从而减少缓存冲突。
  • 比较(Compare) :当进行读取操作时,Tag数组中的两个标签(对应 Way-1 和 Way-2)都需要与请求的地址Tag进行比较,以确定是否有缓存命中。若其中一个路的标签与请求的地址匹配,则表示缓存命中,数据可以从对应的路中读取。

组相联的优势与劣势

  • 优势 :减少了缓存冲突,因为多个数据块可以映射到同一个缓存位置,解决了直接映射缓存中的冲突问题。
  • 劣势 :功耗增加,因为需要同时读取多个标签和数据块,以进行匹配和选择。

3.2.5 Example

32 KB 4 路组关联缓存阵列,行大小为 32 字节,假设地址为 40 位

  • How many sets?

    \(32KB / (32Byte\times4)=32KB/128Byte=256set\)

  • How many index bits, offset bits, tag bits?

    index bits(8): \(2^8=256set\)

    offset bits(5): \(2^{5}=32Byte\)

    tag bits(27): \(40-5-8=27bits\)

  • How large is the tag array?

\(27bit \times 256 \times 4=27Kb\)

3.3 缓存 Block/Line

  • Block(line)(缓存块):缓存中的存储单元
    • 内存在逻辑上被划分为缓存块,这些缓存块映射到缓存中的位置
    • 一个固定大小的数据集合, 包含所请求的字 ,从主存储器中取出并放入缓存中
  • 对于引用:

    • HIT: 如果在缓存中,则使用缓存数据,而不是访问内存
    • MISS: 如果不在缓存中,则将数据块引入缓存
      • 可能需要将其他东西踢出缓存才能做到这一点 (替换
  • 一些重要的缓存设计决策

    • 放置:在缓存中何处以及如何放置/查找/索引块?
    • 替换:删除/执行/替换哪些数据以在缓存中腾出空间?
    • 管理粒度:大块还是小块?
    • 写入策略:如何处理写入?Write-back、Write-through
    • 指令/数据:我们是否将它们分开处理?

3.4 缓存 Miss 的类型

  • 强制未命中: 首次访问内存 word 时发生——无限缓存的未命中次数
  • 容量未命中: 发生这种情况是因为程序在重新接触同一 word 之前接触了许多其他单词——完全关联缓存的缺失量
  • 冲突未命中: 由于两个 word 映射到高速缓存中的相同位置而发生——从完全关联高速缓存到直接映射高速缓存时产生的缺失

附注: 全关联缓存的未命中次数会比相同大小的直接映射缓存多吗?会,增加了冲突未命中

3.5 降低 Miss Rate

  • 增加缓存块大小 :增大缓存块的大小可以减少强制性未命中(即数据首次被访问时的未命中),并且在存在空间局部性时,可以降低未命中的惩罚(因为可以一次性预取更多相关数据)。但增大块大小也带来一些负面影响,比如:

    • 不同缓存层之间的流量增加

    • 可能导致空间浪费(当只需访问其中一小部分数据时)

    • 可能增加冲突未命中(因为较大的块会占用更多缓存空间,导致其他数据无法缓存)

  • 增大缓存容量 :更大的缓存可以减少容量未命中和冲突未命中,因为可以存储更多的数据。然而,增大缓存容量会带来访问时间的惩罚(较大的缓存通常访问速度会稍慢)。

  • 高关联度 :增加缓存的相联度(如从1路变为2路或更高)可以减少冲突未命中,因为多个数据块可以存储在同一缓存组中,减少了因为地址冲突而导致的数据丢失。

    • 经验法则 :容量为N/2的2路相联缓存的未命中率与容量为N的1路相联缓存相当,但高相联度会消耗更多能量。

3.6 更多 Caches 基础

  • L1缓存分为指令缓存和数据缓存,而L2和L3是统一缓存 :在一级缓存(L1)中,将指令和数据分别存储在不同的缓存中,以提高访问效率;而在L2和L3级别,指令和数据共享同一个缓存空间,即为统一缓存。

  • L1/L2缓存层次结构可以是包容性、排他性或非包容性

    • 包容性 :L2缓存包含所有L1缓存中的数据内容,这样如果L2有数据,L1肯定也有。这种结构便于管理,但会占用更多L2空间。

    • 排他性 :L2缓存中的数据不在L1中,两个缓存不会重复存储相同数据,这种结构可以更有效地利用总的缓存空间。

    • 非包容性 :L1和L2之间没有严格的包容关系,数据可以在L1或L2中单独存在。

  • 写操作时可以选择写分配或不写分配

    • 写分配 (Write-Allocate):在写入缓存时,如果数据不在缓存中,会将其加载到缓存中后再写入。

    • 不写分配 (Write-No-Allocate):如果数据不在缓存中,直接写到下一级缓存或主存,而不加载到缓存中。

  • 写操作时可以选择回写(Write-Back)或直写(Write-Through)

    • 回写 (Write-Back):写入时只更新缓存中的数据,而不立即写回内存,只有当缓存行被替换时才写回内存,减少了内存总线的流量。

    • 直写 (Write-Through):每次写入缓存时,同时更新内存,这样简化了数据一致性管理,但增加了内存访问流量。

  • 读操作优先于写操作,且写操作通常会进行缓冲 :为了保证数据的快速读取,读操作通常具有更高优先级,而写操作会被缓冲,等待合适的时机再写入,避免影响读操作的效率。

  • L1缓存并行进行标签和数据访问,L2/L3缓存串行进行标签和数据访问

    • 在L1缓存中,标签(Tag)和数据的访问是并行的,以减少访问时间。

    • 在L2和L3缓存中,标签和数据访问是串行的,先检查标签是否命中,再决定是否读取数据,这种设计减少了电路的复杂性。

3.7 容忍罚失

在处理缓存未命中(cache miss)时,如何通过一些策略来减少等待带来的性能损失:

  • 乱序执行(Out of Order Execution) :在等待缓存未命中数据返回的时间内,处理器可以继续执行其他有用的工作,以提高整体的执行效率。这意味着处理器不需要因一次缓存未命中而停下来等待,而是可以继续处理其他指令。另外,乱序执行允许处理器同时处理多个缓存未命中事件,从而最大化资源利用率。
    • 多次缓存未命中(Multiple Cache Misses) :在这种情况下,缓存控制器需要能够跟踪多个未命中的请求,这就需要非阻塞缓存(Non-Blocking Cache)。非阻塞缓存允许处理器在等待一个未命中的数据时继续发出其他请求,而不是阻塞住当前的所有操作。
  • 硬件预取(Hardware Prefetching) :通过硬件将未来可能会访问的数据提前加载到预取缓冲区(Prefetch Buffer)中,以减少未来访问的等待时间。这种方法在程序具有 空间或时间局部性 时尤其有效,因为处理器可以预测到接下来可能需要的数据。
    • 激进预取(Aggressive Prefetching) :虽然预取可以减少等待时间,但如果预取过多,会增加总线(Bus)的争用压力,影响其他内存访问的性能。因此,预取的策略需要平衡数据加载和总线资源的竞争。

3.8 预取(Prefetching)

  • 硬件预取可以应用于任何缓存级别 :预取是一种硬件技术,用来在数据实际被需要之前提前将其加载到缓存中,以减少等待时间。预取技术可以在任何缓存层次(如L1、L2、L3)中应用。
  • 预取会引起缓存污染 :如果预取的数据并不被立即使用,可能会占用缓存中的位置,从而导致原本更有用的数据被替换掉,这种情况称为缓存污染。为避免污染,预取的数据通常放在一个单独的预取缓冲区中。这样,预取的数据不会与主缓存中的数据混合,但在访问缓存时需要并行查找这个缓冲区,以确保是否存在预取的数据。
  • 激进的预取增加“覆盖率”,但降低“准确率” :激进的预取意味着尽可能多地预取数据,这样可以覆盖更多的潜在数据需求,称为“增加覆盖率”。但是,这也可能导致许多预取的数据其实不会被用到,从而浪费了内存带宽,称为“降低准确率”。
  • 预取的时机很重要 :预取数据的时机需要把握得当。预取必须足够提前,以便在数据需要时已经加载完毕,从而隐藏内存访问的延迟。但如果预取过早,数据可能在被使用前就因为缓存替换策略而被驱逐,导致缓存污染或资源浪费。

4. 缓存抽象(Abstraction)和结构(Structure)

4.1 缓存抽象和度量

给一个地址来索引数据存储,tag store 回答寻找的地址是否在缓存中,它会发出信号Hit/miss,如果Hit,Data store会给出Data。

  • Cache hit rate = (# hits) / (# hits + # misses) = (# hits) / (# accesses)
  • Average memory access time (AMAT) = ( hit-rate * hit-latency ) + ( miss-rate * miss-latency )(include hit latency)
  • AMAT越小越好吗?

4.2 Block和缓存寻址

  • 内存在逻辑上被划分为固定大小的block
  • 每个数据块映射到缓存中的某个位置,该位置由地址中的index (索引) bits 或 set (组) bits 决定。
    • 用于索引标签和数据存储
  • 缓存访问过程:
  1. 将索引输入 tag 和 data 存储,索引位位于地址中
  2. 检查tag store中的有效位
  3. 将地址中的 tag bits 与 tag store 中存储的标签进行比较
  • 如果数据块在缓存中(缓存命中),则存储的 tag 应 有效valid ,并与数据块的 tag 相匹配

4.3 直接映射缓存:放置和访问

  • 假设字节可寻址内存:256bytes,8-byte blocks -> 32 blocks

  • 假设缓存: 64 bytes,8 blocks

    • 直接映射: 一个block只能前往一个location
  • 具有相同 index 的地址争夺相同位置

    • 导致冲突未命中
  • 直接映射缓存:内存中映射到高速缓存中相同 index 的两个块不能同时出现在缓存中

    • 一个 index -> 一个 entry
  • 如果以 交错方式访问 的多个 block 映射到同一索引,则可能导致 0% 的命中率

    • 假设地址 A 和地址 B 的 index 位相同,但 tag 位不同
    • A, B, A, B, A, B, A, B, … -> 缓存 index 冲突
    • 所有访问都是 冲突未命中

4.4 组关联性(Set Associativity)

4.4.1 2组关联

  • 在直接映射缓存中,地址 0 和 8 总是冲突的
  • 将一列 8 个 blocks 改为 2 列 4 个 blocks
  • index位减1,tag位加1
  • 组内关联存储器
    • 更好地适应冲突(更少的冲突未命中)
    • 更复杂、访问更慢、tag store更大

4.4.2 4组关联

  • 冲突未命中的可能性更低
  • 更多 tag 比较器和更宽的数据复用器;更大的 tag

4.4.3 全关联

  • 完全关联式缓存
    • 一个数据块可放置在任何高速缓存位置

4.4.4 关联性(和权衡)

  • 关联性程度: 有多少 block 可以映射到同一个 index(或数据集)?

  • 更高的关联性

    • 更高的命中率
    • 更慢的缓存访问时间(命中延迟和数据访问延迟)
    • 更昂贵的硬件(更多的比较器)
  • 高关联度带来的收益递减

4.5 降低未命中率(Miss Rate)的创新方法

  • Victim 缓存
  • 更好的替换策略——伪 LRU、NRU、DRRIP
    • 插入、提升、victim 选择
    • 预取、缓存压缩

4.5.1 Victim 缓存

  • 直接映射缓存会出现未命中情况,因为多个数据映射到同一位置
  • 处理器经常尝试访问最近丢弃的数据
    • 所有丢弃的数据都放在一个小的 victim 缓存中(4 或 8 个条目)
    • 在进入 L2 之前检查 victim 缓存
  • 可将其视为冲突最多的几个数据集的额外关联性

4.6 组关联缓存中的问题

  • 认为 set 中的每个block都有一个 "优先级"
    • 表示将该 block 保留在缓存中的重要性
  • 关键问题:如何确定/调整数据块的优先级?
  • 在一个 set 中有三个关键决定:
    • 插入、提升、剔除(替换)
  • 插入(Insertion):缓存填充时优先级会发生什么变化?
    • 在哪里插入进入的 block,是否插入 block
  • 提升(Promotion):缓存命中时优先级会发生什么变化?
    • 是否以及如何改变 block 的优先级
  • 驱逐/替换(Eviction/replacement):缓存未命中时优先级会发生什么变化?
    • 驱逐哪个 block 以及如何调整优先级

4.7 驱逐/更换策略

  • 在缓存 miss 时,要替换数据集中的哪个数据块?
    • 先替换任何无效 block
    • 如果所有 block 都有效,则参考替换策略
      • Random
      • FIFO
      • Least recently used (how to implement?) 最近最少使用(LRU)
      • Not most recently used 最近未使用
      • Least frequently used?使用频率最低
      • Least costly to re-fetch?重新获取的成本最低
      • Why would memory accesses have different cost?
      • Hybrid replacement policies 混合替换
      • Optimal replacement policy?

5. LRU

  • Idea :驱逐最近访问次数最少的区块

  • Problem: 需要跟踪 block 的访问顺序

  • 问题:2路组关联缓存

    • 要完美实现 LRU,需要哪些条件? 需要1bit记录访问顺序
  • 问题:4路组关联缓存

    • 完美实现 LRU 需要哪些条件?
    • 集合中的 4 个块可能有多少种不同的顺序? \(4\times 3\times 2\times 1 = 4! = 24\)
    • 需要多少位来编码块的 LRU 顺序? 最少需要5bits
    • 确定 LRU victim 需要哪些逻辑?

5.1 近似LRU

  • 大多数现代处理器都没有在高关联缓存中实现 "真正的 LRU"(也称为 "完美的 LRU")。

    • 因为真正的LRU是复杂的
    • LRU只是预测本地性的近似值(即不是最佳的高速缓存管理策略
  • Example:

    • NRU,非最近使用
    • DRRIP

5.2 驱逐/更换策略

  • NRU: 集合中的每个 block 都有一个 bit;block 被访问时,该 bit 变为 0;如果所有 bit 都为 0,则将所有位变为 1;将 bit 设置为 1 的 block 驱逐出去

  • DRRIP: 使用多个 NRU 位(3 位),将进入的 block 设置为高位,使其接近被驱逐;类似于将进入的 block 置于 LRU 列表的头部而非尾部附近

6. 缓存策略

6.1 Tag Store

Tag的组成可以有以下几部分:

  • Valid bit
  • Tag
  • Replacement policy bits(更换策略位)
  • Dirty bit(脏位)
    • 表示该数据为 write back 模式还是 write through模式

6.2 处理 Write 操作

6.2.1 写回模式(Write-back) vs. 写直达模式(Write-through)

  • 我们何时将缓存中修改过的数据写入下一级?

    • write through: 写入时
    • write back: 当 block 被驱逐时
  • Write-back(写回模式): 更新缓存时,并不同步更新memory

    • 可以在驱逐前合并对同一 block 的多次写入
      • 有可能节省缓存级别之间的带宽,减少能耗
    • 需要在 tag store 中加入一个位,表明该数据块是 "脏的/修改过的"。
  • Write-through(写直达模式): CPU向cache写入数据时,同时向memory写

    • 简单
    • 所有级别的 Cache 都是最新的。 一致性: 缓存一致性更简单,因为无需检查接近处理器缓存的 Tag store 是否存在
    • 带宽更密集;不合并写入

6.2.2 写入未命中时分配(Allocate on write miss) vs. 写入未命中时不分配(No-allocate on write miss)

  • 我们是否会在 写入未命中时分配缓存块(cache block)即是否会从缓存中驱逐其他缓存块
    • 写入未命中时分配(Allocate on write miss):是
    • 写入未命中时不分配(No-allocate on write miss):否
  • 写入未命中时分配(Allocate on write miss):将要写的地址所在的 block 先从 memory 读到 cache 中,然后写 cache
    • 可以 合并写入 ,而不是将每一个要写的数据每次都单独写入下一级
    • 更简单,因为 写入未命中与读取未命中的处理方式相同
    • 需要传输整个缓存块(cache block)
  • 不分配(No-allocate):将要写的内容直接写回 memory
    • 如果写入 block 的局部性较低,则可节省缓存空间(可能会提高缓存命中率

6.3 分块(Subblocked/Sectored)缓存

  • 如果处理器在较短时间内写入整个数据块怎么办?是否有必要首先将数据块从内存带入缓存?

  • 为什么我们不能只写入数据块的一部分,即子数据块?

    • e.g. 写 64byte 中的 4byte
    • Problem:有效位和脏位与整个 64byte 相关联,而不是与每个 4byte 相关联
  • Idea:将一个 block 划分为子区块 subblocks(or 扇区sectors)

    • 每个 subblock(sector)都有单独的有效位和脏位
    • 一个请求只分配一个 subblocks(或一个 subblocks 子集)

  • 优点:

    • 无需将整个缓存块 block 传输到缓存中(写入只需 验证和更新一个 subblock
    • 将 subblocks 传输到缓存的自由度更大(缓存块 block 不需要完全在缓存中,只需要确定一次读取要传输多少个 subblocks?)
  • 缺点:

    • 设计更复杂;更多的有效位和脏位
    • 可能无法充分利用空间局部性

6.4 指令缓存(Instruction Cache) vs. 数据缓存(Data Cache)

  • 要分开(Separate)管理还是统一(Unified)管理

  • Unified的优缺点:

    • 动态共享缓存空间:不会出现静态分区(即独立的 Instruction 和 Data 缓存)可能出现的超额配置情况

    • 指令和数据会互相驱逐(即两者都没有充足的空间

    • Instruction 和 Data 在流水线的不同位置被访问。 我们应该把统一缓存放在哪里才能实现快速访问? 这是大多数处理器使用单独(Separate)缓存的主要原因

  • 一级缓存几乎总是被拆分

    • 主要就是因为这个原因 “Instruction 和 Data 在流水线的不同位置被访问。 我们应该把统一缓存放在哪里才能实现快速访问?”
  • 高级缓存几乎总是统一的

    • 这是因为 “动态共享缓存空间:不会出现静态分区(即独立的 Instruction 和 Data 缓存)可能出现的超额配置情况”

6.5 流水线设计中的多级缓存

  • 一级缓存 L1(指令和数据):
    • 决定在很大程度上受周期时间的影响
    • 小、关联度低;延迟至关重要
    • Tag store 和 Data Store 并行访问
    • 指令和数据也是并行访问的
  • 二级缓存 L2:
    • 决策需要平衡命中率和访问延迟
    • 通常容量大,关联性强;延迟不那么重要
    • 以串行方式访问 Tag store 和 Data store
      • 延迟并不重要,重要的是命中率和节能
  • 缓存级别的串行与并行访问:
    • 串行:只有在一级缓存 miss 时才访问二级缓存
    • 第二层的访问权限与第一层不同
      • 第一层起到过滤器的作用(过滤一些时间和空间局部性)
      • 管理策略有所不同

7. 提高缓存性能

缓存参数与失误/命中率的关系

  • Cache 大小
  • Block 大小
  • 关联性
  • 驱逐策略
  • 插入策略

7.1 Cache 大小

  • 缓存大小:数据(不包括 tag)总容量

    • 越大越能利用时间局部性
    • 并非总是越大越好
  • 过大的缓存会对命中和未命中延迟产生不利影响

    • 越小越快 => 越大越慢
    • 访问时间可能会降低关键路径的性能
  • 缓存太小

    • 不能很好地利用时间局部性
    • 有用数据经常被替换
  • 工作集(working set)点:应用程序所引用的全部数据集可能存在于缓存中

7.2 Block 大小

  • Block 大小是与地址 tag 关联的数据

    • 不一定是层级间的传输单位
      • Sub-blocking:一个 block 分为多个部分(每个部分都有 Valid/Dirty 位)
  • Block 太小

    • 不能很好地利用空间局部性
    • tag 开销较大
  • block 太大

    • 总 block 数过少
    • 时间局部性利用较少
    • 如果空间局部性不高,则会浪费缓存空间和带宽/能源

7.2.1 Large Block:Critical-Word and Subblocking

  • 大型缓存块可能需要很长时间才能填入缓存

    • 先填入缓存行 关键字(Critical-Word)
    • 在完成填入之前重新启动缓存访问
  • 大型缓存块会浪费总线带宽

    • 将一个块划分为多个子块
    • 为每个子块分别设置有效位和脏位

7.3 关联性

同一索引(即组)中可以出现多少个block?

  • 更大的关联性
    • 更低的未命中率(减少冲突)
    • 更高的命中延迟和面积成本
  • 更小的关联性
    • 更低的成本
    • 更低的命中延迟
    • 对 L1 缓存尤为重要

7.4 缓存 miss 的分类

  • 强制未命中
    • 对address(block)的首次引用总是导致未命中
    • 随后的引用应该命中,除非缓存块由于以下原因被移位
    • 解决方式: 预取:预测哪些 block 即将需要
  • 容量未命中
    • 缓存太小,无法容纳所需的所有内容
    • 定义为即使在相同容量的完全关联缓存(具有最佳替换功能)中也会发生的缺失次数
    • 解决方式:
      • 更好地利用缓存空间:保留将被引用的 block
      • 软件管理:划分工作集和计算,使每个 "计算阶段 "都适合缓存
  • 冲突未命中
    • 定义为既非强制失误也非容量失误的任何失误
    • 解决方式:
      • 更多的关联性
      • 在不使缓存具有关联性的情况下获得更多关联性的其他方法
        • Victim cache
        • 更好的随机索引
        • 软件提示?

7.5 如何提高缓存性能

  • Reducing miss rate
    • 更多关联性
    • 关联性的替代方法/增强方法
    • victim 缓存、散列、伪关联性、倾斜关联性
    • 更好的替换/插入策略
    • 软件方法
  • Reducing miss latency or miss cost
    • 多级高速缓存
    • 关键字优先
    • 分块(subblocking)/扇区
    • 更好的替换/插入策略
    • 无阻塞高速缓存(多个高速缓存并行读取)
    • 每个周期多次访问
    • 软件方法
  • Reducing hit latency or hit cost

标签:游戏攻略