想象你走进一家米其林后厨:大厨站在灶台前,手边是一排最常用的刀和调料(寄存器),灶台备料区摆着刚切好的食材(缓存),身后的储物柜囤着一周的主料(主存),地下仓库则堆着整月采购的货物(外存)。越往里走,取东西越慢,但能放的东西也越多。想让做菜又快又稳,秘诀不是把仓库搬进灶台,而是研究“什么东西该放在手边、什么时候搬、什么时候撤”。计算机的存储系统正是照搬了这套后厨哲学。这篇文章会用这家后厨的日常,讲透存储层次的结构与缓存策略:从层次金字塔到局部性原理,从映射方式到命中与缺失,从写策略、替换算法到预取技术,让你彻底看懂“数据住得离 CPU 有多近”这件事。

岗位关联

存储层次的本质是“用便宜的大容量,伪装出昂贵的高速”。理解了缓存策略,你就掌握了定位性能瓶颈的直觉——无论训练数据管线还是 LLM 推理,延迟问题的答案都藏在它的影子里。

目录
  1. 可视化演示
  2. 存储层次概览
  3. 局部性原理
  4. 映射方式
  5. 命中与缺失
  6. 写策略
  7. 替换算法
  8. 预取技术
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过存储层次金字塔、缓存映射方式对比、写直达与写回策略三个模式,展示计算机存储系统的层次结构与缓存核心策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

存储层次结构金字塔 从寄存器到外存逐级扩大容量但放缓速度 访问数据时从上往下逐级查找直到命中 T_avg = h*T_cache + (1-h)*T_mem 局部性原理使命中率h趋近1,平均访问接近缓存速度。

存储层次概览

后厨的生存法则第一条:灶台只有那么大,但菜品源源不断。大厨手边的调料瓶(寄存器)秒取,灶台的备料(缓存)一转身就到,储物柜(主存)要走三五步,地下仓库(外存)得耽误好几分钟。每一级的容量都在变大,速度却在变慢——这不是设计失误,而是成本使然:快的存储器贵得只能做一点点。整条链的目标,是用最便宜的大容量,伪装出最快的速度。

计算机把这条后厨法则复制得一丝不苟。CPU 这位大厨眼里,数据从近到远排列:寄存器(几十字节)、多级缓存(几十 KB 到几十 MB)、主存(几个 GB 到几十 GB)、外存(磁盘/SSD,几百 GB 到几 TB)。相邻两层之间,速度差常常是几十到上百倍。于是出现一个问题:CPU 每取一次数据,到底要等多久?我们不能每次都在“慢的仓库”上赌运气,于是有了“平均访存时间”这个综合指标。

平均访存时间不是一个简单平均值,而是把“在缓存里直接拿到”与“不得不去下一层拿”这两种情况按概率加权:

公式里的门道在于:只要命中率 H 足够高,平均访存时间就被拉回缓存的速度附近。后厨里,这意味着大厨大部分时候伸手就拿到料,只有偶尔才跑一趟储物柜。所以整篇文章的核心任务只有一件:想尽办法把命中率 H 做大。

Tavg=H×Tcache+(1−H)×Tmemory\displaystyle T_{avg} = H \times T_{cache} + (1 - H) \times T_{memory}
  1. T_avg(平均访存时间)CPU 随机访问一次数据时,整个存储系统花费的时间期望值。
    TavgT_{avg}
  2. H(命中率)数据恰好在上层缓存命中的访问比例,取值范围 0 到 1。
    HH
  3. T_cache(缓存访问时间)命中后从缓存取数所需的时间,通常只有几个时钟周期。
    TcacheT_{cache}
  4. (1−H)×T_memory(缺失代价)未命中的概率乘以必须访问主存的耗时,缺失率越高,代价越大。
    (1−H)×Tmemory\displaystyle (1 - H) \times T_{memory}

局部性原理

大厨之所以不必每道菜都跑储藏室,不是因为他记性好,而是做菜自带章法。做番茄炒蛋时,盐在一分钟内被拿起三四次——刚刚用过的东西,很快就再用;备料时,葱姜蒜、油盐酱总是放在相邻的格子——拿到酱油,醋就在旁边。这两条朴素规律,是整座后厨备料体系能够成立的理由。

代码世界同样如此。循环体里的同一个变量被反复读写,同一个函数被反复调用,这是时间局部性;数组按顺序遍历、指令按顺序执行,一段代码附近的指令很快也被执行,这是空间局部性。统计发现,程序执行往往集中在很少一部分代码和数据结构上,这就是著名的“90/10 现象”——90% 的时间花在 10% 的代码上。

局部性强不强,用命中率来衡量最直观:

时间局部性让“先用着、别收走”有道理,空间局部性让“顺带取旁边一块”有道理。后厨的智慧在于:只要发现“这味料的用量有规律”,就值得为它预备位置。反过来,如果大厨做菜毫无章法,东敲一下西摸一下(局部性差的程序),再强的手边备料也救不回效率。

H=NhitNhit+Nmiss\displaystyle H = \frac{N_{hit}}{N_{hit} + N_{miss}}
  1. N_hit(命中次数)在缓存中直接找到数据的次数。
    NhitN_{hit}
  2. N_miss(缺失次数)没找到、不得不下沉访问下层的次数。
    NmissN_{miss}
  3. N_hit + N_miss(总访问次数)两类情况互斥且穷尽,和就是 CPU 发起的全部访存。
    Nhit+NmissN_{hit} + N_{miss}
  4. H(命中率)两者之比,直观衡量局部性有没有被利用起来。
    NhitNhit+Nmiss\displaystyle \frac{N_{hit}}{N_{hit} + N_{miss}}

映射方式

备料台怎么划分格子,是后厨的一大设计难题。第一种做法:每样食材固定对应一个格子,来了就往里放;第二种:食材随便放,哪个空格都行;第三种:先把台面分成几个区,每个区内再自由摆放。三种做法,对应着缓存与主存之间的三种映射方式。

硬件术语里,这三个方案分别叫直接映射、全相联映射、组相联映射。主存被切成固定大小的块,缓存则由若干行(槽位)组成。直接映射:一个主存块永远只能进唯一的一行,行号由取模运算决定。全相联:任意块可进任意行,灵活但每次要比较所有行。组相联:主存块先按组号定位到某一组,组内任意行皆可。直接映射最省硬件,但不同块抢同一行会互相挤兑;全相联最灵活,查找却最贵;组相联是两者的折中,也是现代 CPU 的主流选择。

直接映射的归宿可以用一个取模运算确定:

这行运算快得惊人:硬件只需截取地址中的几个位,无需任何查找。对应到后厨,就好比“酱油永远放 3 号格”——一伸手就准,可惜一旦第 5 号货架的料酒也想用 3 号格,酱油就得让位。组相联则给每组多备几个格子,冲突少得多,代价是每进来一块都要几路同时比较。

缓存行号=主存块号 mod 缓存行数\displaystyle \text{缓存行号} = \text{主存块号} \bmod \text{缓存行数}
  1. 主存块号主存按固定大小切成块后的编号。
    主存块号\text{主存块号}
  2. 模运算 mod除后取余数,把任意大的块号映射到有限的 0 到(行数−1)区间。
     mod \bmod
  3. 缓存行数缓存共有多少个固定大小的槽位,决定取模的“宽度”。
    缓存行数\text{缓存行数}
  4. 缓存行号(结果)取模结果就是该块在直接映射下的唯一归宿,硬件截取地址的几个位就能直接算出。
    缓存行号\text{缓存行号}

命中与缺失

大厨伸手去拿豆瓣酱,手边有——命中;手边没有——缺失。一次缺失意味着从灶台走到储物柜,快则几十步,慢则翻箱倒柜。更让大厨头疼的是,缺失还分三种委屈:第一次用这个料,从来就没上过台面;台面本来有,因为地方不够被收走了;料一直在,但被另一味料挤走腾了格子。

对应到计算机体系结构里,这是经典的 3C 缺失模型:强制缺失(Compulsory),首次访问必然发生,躲不掉;容量缺失(Capacity),缓存整体不够大,工作集装不下;冲突缺失(Conflict),映射策略导致多个块争抢同一行。三种缺失的“病根”不同:强制缺失要靠预取来缓解,容量缺失逼你加大缓存或优化局部性,冲突缺失则靠提高相联度来化解。

每一次缺失都要付出“跑一趟储物柜”的代价,把损失的期望折算进每一次访问,就得到平均访存时间的另一种表达:

这个公式把固定开销与额外代价分得清清楚楚。后厨的启示是:只要把缺失率压下来,哪怕一次缺失要跑很久,整体体验也依然丝滑——这正是一级一级缓存叠加的意义所在。

TAMAT=Thit+M×Tmiss_penalty\displaystyle T_{AMAT} = T_{hit} + M \times T_{miss\_penalty}
  1. T_hit(命中时间)命中时把数据从缓存送到 CPU 的固定开销。
    ThitT_{hit}
  2. M(缺失率)缺失次数占总访问数的比例,即 M = 1 − H。
    M=1−HM = 1 - H
  3. T_miss_penalty(缺失惩罚)一次缺失造成的额外等待,包括访问主存、可能换块、回填缓存的全过程。
    Tmiss_penaltyT_{miss\_penalty}
  4. 乘积 M×T_miss_penalty把缺失惩罚按发生的概率折算进每次访问。
    M×Tmiss_penaltyM \times T_{miss\_penalty}

写策略

光会“取”还不够,后厨还要“记”——写操作。大厨把半成品调好味,要不要立刻在储物柜旁的账本上记一笔?两种风格:一种每次改动都同步誊到账本(写直达),账目永远一致,但一趟一趟跑腿;另一种先在灶台便利贴上记着,等这块备料收摊时才统一誊写(写回),跑腿少得多,前提是得记住哪些写过了。

写直达(write-through)的策略是:写缓存的同时,立刻写主存。优点是实现简单、一致性极强,缺点是每一次写都要占用通往主存的总线带宽。写回(write-back)只在缓存里改,并给块打上一个“脏位”标记(dirty bit),等这块被替换出去时,如果脏位为 1,才真正写回主存。多数程序对同一个块会反复写,写回能把这些写攒成一次,省下大量总线流量。

还有一个容易被忽略的维度:写缺失时怎么办?写分配(write-allocate):先把缺失的块从主存载入缓存,再在缓存里写——适合“马上还要用”;写不分配(no-write-allocate):不载入缓存,直接写主存——适合“就写这一次”。工程上,写回往往搭配写分配,写直达常常搭配写不分配。写回能省多少总线流量,可用期望公式粗算:

这个期望清晰地解释了便利贴的威力:写次数多、脏块占比高时,写回把大量写操作折叠成替换时刻的一次写。后厨里,这就是“先攒着,收摊一笔清”的手艺。当然,账本永远最新是写直达的执念——数据一致性要求高的场景,这份执念反而是优点。

E[Nwrite_back]=Nreplace×pdirty\displaystyle E[N_{write\_back}] = N_{replace} \times p_{dirty}
  1. N_replace(替换次数)缓存满后发生块替换(淘汰)的总次数。
    NreplaceN_{replace}
  2. p_dirty(脏块概率)被淘汰的块带脏标记的概率,程序写同一块越频繁,这个概率越高。
    pdirtyp_{dirty}
  3. 期望写回次数批量算下来平均会发生多少次真正的写主存动作。
    E[Nwrite_back]E[N_{write\_back}]

替换算法

备料台满了,必须撤一样。撤谁?四位后厨师傅各有一套:最早摆上来的先撤(FIFO);最久没碰过的先撤(LRU);闭着眼睛撤(随机);用得最少的先撤(LFU)。看起来都合理,但做菜的人都知道,“最久没碰”往往意味着“暂时还用不上”,所以 LRU 最贴合真实节奏。

替换算法是给缓存做“淘汰决策”。FIFO(先进先出)按入场顺序淘汰,简单但无视使用频率;LRU(最近最少使用)记录每个块上次被访问的时间,优先淘汰最久没被访问的那个;随机替换实现成本最低,某些场景反而表现稳;LFU(最不常用)统计访问次数,淘汰计数最少的块。现代 CPU 中 LRU 及其近似算法(如 Clock 二次机会)最流行,因为它在硬件成本与命中率之间取得了平衡。

LRU 的决策逻辑可以写成一句话:在所有缓存块里,挑出“距离上次被访问最久”的那一个送走。形式化地写:

后厨连线:这四位师傅各有胜负——FIFO 记性差,可能在菜做到一半时把正缺的料收走;LRU 像个有经验的大厨,总能猜中哪些料马上还要用;随机在极端规律负载下偶尔以毒攻毒;LFU 则容易被“过去很常用、现在不用了”的老料占着台面。没有万能算法,这也是硬件工程师喜欢给替换策略留参数的原因。

victim=arg⁡max⁡i∈cacheagei\displaystyle \text{victim} = \arg\max_{i \in \text{cache}} \text{age}_i
  1. cache(缓存块集合)当前占用缓存的全部块。
    cache\text{cache}
  2. age_i(未访问年龄)块 i 从上次被访问到现在经过的时间,越大说明越久没被碰,被访问一次就清零重计。
    agei\text{age}_i
  3. argmax(取最大者)遍历所有块,选出年龄最大、也就是最久未使用的那一个。
    arg⁡max⁡i∈cache\arg\max_{i \in \text{cache}}
  4. victim(被淘汰者)最终被移出缓存的块,为要装入的新块腾出位置。
    victim\text{victim}

预取技术

最高明的后厨,不等大厨开口就动了:看到这道菜切完葱,就知道下一步要蒜,提前把蒜放到台面上。这就是预取——把“需要时再去拿”改成“提前就位”。被预取的数据,等大厨真用到时已经在手边,缺失惩罚被悄悄藏进了做其他事的间隙。

硬件预取由 CPU 内部的预测器负责:它监视访问流,一旦发现顺序访问模式(比如连续读数组),就自动把后续几个块也抓进缓存。软件预取则把主动权交给编译器:编译时识别出循环里即将用到的数据,插入专门的预取指令,让数据提前到达。两者目标一致:降低缺失率与缺失惩罚,让“等待”变短甚至消失。

但预取不是免费的。预取要占用主存到缓存之间的带宽,预取进缓存的数据还会挤占正常数据的位置。如果预测不准,“提前搬来的货”反而污染了台面。工程上用预取有效率来衡量:

有效预取率高,说明后厨的预测很准,带宽花得值;低则说明先知失灵,白白搬了一堆没人要的货。后厨的智慧是:只在模式明显的时候出手,比如顺序遍历、固定步长跳转;遇到乱序访问,宁可少预取,也不要瞎搬——毕竟把整座仓库搬上灶台,锅都盖不上。

Eprefetch=NusefulNtotal_prefetch\displaystyle E_{prefetch} = \frac{N_{useful}}{N_{total\_prefetch}}
  1. N_total_prefetch(预取总次数)硬件或软件发起的预取动作总数。
    Ntotal_prefetchN_{total\_prefetch}
  2. N_useful(有效预取次数)其中真正在数据被用到之前完成抓取、且确实被用到的次数。
    NusefulN_{useful}
  3. E_prefetch(预取有效率)两者比值,越高说明“先知”越准,乱搬仓库的行为越少。
    EprefetchE_{prefetch}

AI 应用场景

CDN 与多级缓存加速 AI 平台

阶段:推理服务的请求处理与数据访问

解决的问题:传统方案中,模型权重、tokenizer 词表、向量索引等大文件每次都回源站拉取,网络往返高,GPU 在等待中空转。这相当于大厨每道菜都跑去仓库取货。CDN 把热数据缓存到边缘节点,算法平台把常用的特征表缓存到本地 SSD、冷数据驻留对象存储——正是“把常用调料放到手边”的存储层次思想。

具体案例:某推荐平台把用户画像向量索引按热度分段,热门段缓存在本地内存(LRU 管理),冷门段留在对象存储;命中率稳定在 90% 以上,特征读取 P95 延迟从 800ms 降到 80ms,训练吞吐提升近一倍。

数据库 Buffer Pool 的缓存调度

阶段:训练样本与特征数据的存取性能优化

解决的问题:训练数据通常放在数据库或文件存储里,全量读入内存既不现实也没必要。数据库引擎的 Buffer Pool 把磁盘页缓存到内存,用 LRU/Clock 替换、预读(read ahead)和脏页刷盘(写回)管理——与文章讲的策略一一对应。理解这些,就能解释“为什么同一句 SQL 时快时慢”。

具体案例:MySQL InnoDB 的 Buffer Pool 被全表扫描“污染”后,热查询命中率骤降;DBA 通过限制扫描页进入 LRU 热端、调大预读系数,把命中率从 70% 拉回 96%,核心报表任务耗时减半。

LLM 推理的 KV Cache 与显存分层

阶段:大模型推理的显存管理与访存优化

解决的问题:大模型推理时,KV Cache(键值缓存)占用大量显存,缓存放不下就导致请求排队;GPU 显存不足时,不常用的权重还要放到 CPU 主存按需换入。这本质上是“备料台太小,需要把不常用的料放回储物柜”的存储层次问题,替换与预取策略直接决定推理吞吐。

具体案例:vLLM 等推理框架用 PagedAttention 把 KV Cache 分块管理、按需分配,配合前缀缓存(类似 LRU 的复用思想)共享公共前缀,显存利用率提升数倍,同卡并发请求数翻番。

Python 演示

python53 行
from collections import deque  class FIFOCache:    def __init__(self, capacity):        self.capacity = capacity        self.blocks = deque()        self.hits = 0        self.misses = 0     def access(self, addr):        if addr in self.blocks:            self.hits += 1            return True        self.misses += 1        if len(self.blocks) >= self.capacity:            self.blocks.popleft()        self.blocks.append(addr)        return False     def hit_rate(self):        total = self.hits + self.misses        return self.hits / total if total else 0.0  class LRUCache(FIFOCache):    def access(self, addr):        if addr in self.blocks:            self.hits += 1            self.blocks.remove(addr)            self.blocks.append(addr)            return True        self.misses += 1        if len(self.blocks) >= self.capacity:            self.blocks.popleft()        self.blocks.append(addr)        return False  def run(label, addrs, capacity):    fifo = FIFOCache(capacity)    lru = LRUCache(capacity)    for a in addrs:        fifo.access(a)        lru.access(a)    print("{}: FIFO={:.1f}% LRU={:.1f}%".format(label, fifo.hit_rate() * 100, lru.hit_rate() * 100))  if __name__ == "__main__":    classic = [1, 2, 1, 3, 1, 2] * 200    random = [(i * 37) % 60 for i in range(12000)]    run("classic 1,2,1,3,1,2", classic, 2)    run("random 60-blocks", random, 8)
目的:用双端队列模拟缓存行,实现 FIFO 与 LRU 两种替换策略,并在两类访问序列上对比命中率,直观验证“局部性越强、LRU 优势越明显”。
  1. self.blocks.popleft()满员时从队头弹出最老的块;同一行代码在两种策略中都扮演“撤菜”动作。
  2. self.blocks.remove(addr); self.blocks.append(addr)LRU 命中时把块移到队尾,等于“被用过就续命”。
  3. class LRUCache(FIFOCache):只用继承改写 access,说明两种策略本质上只有“命中时变不变位置”的区别。
  4. if addr in self.blocks:线性查找对教学足够;真实硬件用相联比较器并行完成这件事。
  5. classic = [1, 2, 1, 3, 1, 2] * 200经典序列:容量 2 时 LRU 每轮命中 2 次,FIFO 只有 1 次。
  6. random = [(i * 37) % 60 for i in range(12000)]伪随机访问抹掉了局部性,两种算法一起吃亏。
  7. print("{}: FIFO={:.1f}% LRU={:.1f}%".format(label, fifo.hit_rate() * 100, lru.hit_rate() * 100))把命中率并排列出,一眼看清策略优劣。

常见误区

误区缓存容量是唯一的指标,容量大命中率就高,所以无脑加大缓存就一定是对的。遇到性能问题,第一反应就是“把缓存调大”。

正解命中率还受映射冲突、替换算法和访问模式影响;而且缓存越大,命中时间(查找延迟)往往越高,成本和功耗也一路飙升。灶台不是越大越好——备料区铺得太开,大厨反而找不到东西、动作变慢。优化缓存要通盘考虑容量、相联度、替换策略三者的平衡。

为何常见:生活里“内存更大跑得更快”的经验被简单套用到了缓存场景,忽略了缓存与内存的本质区别。

误区写回把多次写攒成一次,省了总线流量,所以写回全面吊打写直达,所有场景都应该用写回。

正解写直达有不可替代的优点:实现简单、缓存与主存永远一致、多核之间的一致性协议更容易做;写回的脏数据在断电或崩溃时有丢失风险。便利贴记账效率高,但大风一吹全没了;账本每笔都记虽然慢,却永远有一份可靠底账。两者是“效率”与“一致”的权衡,没有绝对赢家。

为何常见:只看到写回节省写次数的一面,忽略了数据一致性和可靠性这些同样重要的维度。

误区LRU 是万能替换算法,任何负载下命中率都最高,选它准没错。

正解LRU 对顺序扫描工作负载并不友好——工作集大于缓存时依然会持续颠簸;某些流式场景下,随机或 LFU 反而更稳。最久没动的食材不一定是多余的——比如中秋节才用的月饼模具,平时不用但必须保留。硬件上还常用 Clock 二次机会近似 LRU,就是因为真 LRU 的实现成本太高。

为何常见:教材里的例子大多是 LRU 获胜,容易给人留下“LRU 最优”的错觉,忽略了负载的多样性。

误区反正是提前拿数据,多拿一些总没错,预取量越大,等待时间越短。

正解预取要消耗主存到缓存的带宽,预取进来的数据还会挤占正常数据的缓存位置。预取了用不上的数据,等于把整座仓库搬上灶台,锅都盖不上,正常做菜反而变慢。工程上用“预取有效率”把关,模式不明显时宁可少预取。

为何常见:直觉上“提前准备=加速”似乎无懈可击,忽略了预取本身要付出带宽和缓存空间的成本。

面试题

Q1. 在存储层次结构中,速度最快的一层是?

单选题 · easy

  • A.通用寄存器← 正确答案
  • B.高速缓存
  • C.动态内存
  • D.固态硬盘

答案:A。后厨里寄存器=手边刀铲,最快但容量最小。缓存要慢一拍,内存更慢,硬盘最慢。按“从近到远、从快到慢”的层次顺序,速度最快的必然是离 CPU 最近的寄存器。

Q2. 大厨在一道菜里反复拿起同一瓶盐,这类行为对应哪种局部性?

单选题 · easy

  • A.时间局部性← 正确答案
  • B.空间局部性
  • C.顺序局部性
  • D.容量局部性

答案:A。同一数据在短时间内被反复访问,是时间局部性;如果是拿盐时顺手拿了旁边的醋,才叫空间局部性。C 和 D 属于干扰项,并不存在对应定义。

Q3. 某直接映射缓存共有 8 行,主存块 14(块号从 0 开始)会被放到第几行?

单选题 · mid

  • A.第 2 行
  • B.第 4 行
  • C.第 6 行← 正确答案
  • D.第 8 行

答案:C。直接映射的行号=主存块号 mod 缓存行数,14 mod 8 = 6。就像后厨里“酱油永远放 3 号格”一样,取模运算直接定位唯一归宿。

Q4. 写直达与写回策略的本质区别是?

单选题 · mid

  • A.是否同步更新主存← 正确答案
  • B.是否使用高速缓存
  • C.是否支持数据预取
  • D.是否采用组相联映射

答案:A。写直达=每笔账都誊到账本(每次写都同步主存),写回=便利贴攒着收摊再誊(脏块替换时才写回主存)。其余选项与写策略维度无关。

Q5. 一个数据块从未进入过缓存,第一次访问它时发生的缺失属于哪一类?

单选题 · mid

  • A.强制缺失← 正确答案
  • B.容量缺失
  • C.冲突缺失
  • D.替换缺失

答案:A。从没上过台面的第一批食材,第一次要用必然要跑一趟储物柜,这是“强制缺失”,躲不掉。容量缺失是台面装不下,冲突缺失是料被别的料挤走了。

Q6. LRU 替换算法会优先淘汰哪一块?

单选题 · easy

  • A.最久未被访问的块← 正确答案
  • B.最早进入缓存的块
  • C.访问次数最少的块
  • D.随机选中的一块

答案:A。LRU 的“年龄”指距离上次访问的时间,最久没碰的先撤,最贴合“暂时还用不上”的直觉。B 是 FIFO 的思路,C 是 LFU,D 是随机替换。

Q7. 关于写回策略,下列说法正确的有?

多选题 · mid

  • A.用脏位标记未同步数据← 正确答案
  • B.每次写操作都同步主存
  • C.替换脏块时才写回主存← 正确答案
  • D.写命中时开销通常更小← 正确答案

答案:ACD。写回=便利贴记账,脏位=便利贴上的“已改未誊”标记,换块时才统一写回。选项 B 是写直达的特征。因为省掉了每次写都上总线,写回在写命中时开销通常更小。

Q8. 组相联映射相比直接映射,下列说法正确的有?

多选题 · hard

  • A.可以缓解冲突缺失← 正确答案
  • B.需要更多比较电路← 正确答案
  • C.硬件实现更简单
  • D.命中率必然更高

答案:AB。组内多行可换着放,缓解了多个块争抢同一行的“冲突缺失”;代价是每路都要并行比较 tag,比较电路变多。C 说反了,D 中的“必然”太绝对——替换策略不当或负载特殊时组相联未必更优。

Q9. 某缓存命中率 90%,命中时间 2ns,缺失惩罚 20ns,平均访存时间是多少?

单选题 · mid

  • A.4 纳秒← 正确答案
  • B.6 纳秒
  • C.8 纳秒
  • D.10 纳秒

答案:A。T_AMAT = 2 + (1−0.9)×20 = 4ns。命中率足够高时,慢层的时间被“藏”掉了。这正体现了存储层次“用便宜的大容量伪装出高速”的核心目标。

Q10. 关于预取技术,下列说法正确的有?

多选题 · hard

  • A.硬件预取自动识别顺序模式← 正确答案
  • B.预取过多会浪费缓存带宽← 正确答案
  • C.软件预取由编译器插入指令← 正确答案
  • D.预取一定会降低访问延迟

答案:ABC。硬件预取监视访问流、发现顺序模式就自动抓取;软件预取靠编译器插指令。预取要占带宽和缓存空间,过度预取会“污染”缓存,所以 D 的“一定”不成立——预取错了反而更慢。