想象你是一家高档餐厅的主厨,面前是一张窄小的备菜台,身后是一间巨大但很远的库房。每次伸手拿不到食材,就得跑一趟库房,一趟就要几十秒。一位聪明的备菜员提前看你做菜的手势,在你还没开口之前,就把下一份食材放到备菜台上。CPU 里的硬件预取器(Hardware Prefetcher),就是这位备菜员。L1 预取器是站在灶台边的小助手,擅长顺着菜谱一行行往下备料;L2 预取器则像管库房的大管家,能看懂更复杂的用菜规律,甚至会猜“用了酱油之后八成要用醋”。但备菜员也不能乱猜——猜错了会把备菜台塞满没用的食材,反而让主厨连放锅的地方都没有。于是,它需要判断步长、积累信任值、控制污染,并在顺数、跨步和链式取料之间做出取舍。这篇文章就用这套厨房故事,带你完整理解硬件预取器的工作原理,以及如何在 L1/L2 层面调优。

岗位关联

对 AI 从业者而言,理解预取器意味着能看懂性能剖析数据中的 cache miss,能把数据布局和线程调度设计得“喂饱” CPU,而不是让 AI 训练与推理白白等内存。

目录
  1. 可视化演示
  2. 预取器基础理念
  3. L1 流预取器
  4. L2 复杂预取
  5. 步长检测机制
  6. 置信度评估
  7. 污染控制策略
  8. 典型访问模式
  9. 调优实践建议
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

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

预取器基础理念

主厨的备菜台很小,库房却很远。如果每取一次料都跑一趟库房,一道菜可能要等上一顿饭的功夫。于是后厨请来一位备菜员,他观察主厨的做菜顺序,趁主厨正在颠勺时,把下一份可能用到的食材从库房搬到备菜台上。CPU 里的硬件预取器就是这位备菜员,它把“去内存取数据”的漫长等待,藏在了 CPU 正在执行其他指令的背后。

技术上说,CPU 访问寄存器几乎不花时间,访问缓存只需要几个周期,而访问主内存往往需要上百个周期。如果把每次访问都放在“命中缓存”的理想情况下,程序自然快得多。预取器的作用,就是利用空间局部性,在 CPU 真正需要某个地址的数据之前,先把包含它的缓存行(cache line)读入缓存。这样,原本要等待内存的操作,变成了命中缓存,CPU 就不用空转了。

预取器并不能凭空消除内存延迟,它只是让延迟“错峰”。就像备菜员提前动手,主厨感觉不到库房很远;当访问模式足够规律时,预取器就能把有效延迟压到接近缓存命中延迟。

Teff=Thit+Pmiss×(Tmem−Tcache)\displaystyle T_{\text{eff}} = T_{\text{hit}} + P_{\text{miss}} \times (T_{\text{mem}} - T_{\text{cache}})
这个公式说的是:平均取一次料的时间,等于“工作台上直接拿到”的时间,再加上“工作台上没有、必须去库房补一趟”的那部分额外时间。
  1. 命中时间 \(T_{\text{hit}}\)食材已经放在备菜台上,主厨抬手就能拿到的时间,也就是缓存命中延迟。
    ThitT_{\text{hit}}
  2. 缺失概率 \(P_{\text{miss}}\)每 100 次取料中,大约有多少次没能从备菜台拿到,必须跑库房。
    PmissP_{\text{miss}}
  3. 缺失代价 \(T_{\text{mem}} - T_{\text{cache}}\)多跑一趟库房比从工作台取料多花的时间,也就是缓存缺失惩罚。
    Tmem−TcacheT_{\text{mem}} - T_{\text{cache}}
  4. 平均取料时间 \(T_{\text{eff}}\)把命中情况和缺失情况按概率加权平均,就得到 CPU 每次访存的真实期望开销。
    TeffT_{\text{eff}}

L1 流预取器

站在灶台边的小助手只盯着一件事:主厨正在照着菜谱一行一行往下准备食材。第一行是葱,第二行是姜,第三行是蒜。只要连续两次都按照这个顺序取料,小助手就敢把第四五行也先端上来。这就是 L1 Stream Prefetcher 的直觉:检测连续、规则、跨步不变的访问流,并提前预取后续缓存行。

L1 离 CPU 最近,容量最小,预取也必须最“小心”。它通常针对单一访问流工作,记录访问过的地址,一旦发现相邻两次访问的地址差固定,就在当前地址的基础上,往前推若干个缓存行发起预取。这里最关键的是预取距离:如果只提前一格,可能来不及掩盖延迟;如果提前太多,又容易把还没轮到的东西占满 L1。

L1 流预取器非常适合数组遍历、位图扫描、日志数据读取这类顺序访问。它的优点是开销小、反应快;缺点是只能处理“一条直线”的规律。如果程序突然跳到一个完全不同的地址,L1 的流水账就需要重新建立。

prefetch_addr=current_addr+k×line_size\displaystyle \text{prefetch\_addr} = \text{current\_addr} + k \times \text{line\_size}
备菜员猜下一份食材的位置:主厨正在处理的位置,加上“提前量 \(k\)”乘以“每格货架的宽度”。
  1. 当前地址 current\_addr主厨这次正在从备菜台取的食材编号,对应 CPU 当前访问的内存地址。
    current_addr\text{current\_addr}
  2. 预取距离 \(k\)备菜员提前几格开始搬货,不能太近,也不能太远。
    kk
  3. 缓存行大小 line\_size库房货架上最小的一格,通常一个格子能装 64 字节。
    line_size\text{line\_size}
  4. 预取地址 prefetch\_addr推算出的下一格货架位置,也就是 L1 流预取器要提前读取的地址。
    prefetch_addr\text{prefetch\_addr}

L2 复杂预取

管库房的大管家不像小助手那样只盯一条线。他能同时记住主厨的五六道菜、十几种食材搭配,甚至知道“用了酱油之后大概率会用醋”。L2 预取器的工作是捕捉更复杂的非连续访问模式,比如跨步访问、多流交叉,甚至链式数据结构中的热点跳转。

为了实现这种“猜搭配”,硬件里常维护一张转移概率表。它记录:当上一步访问地址为 \(x\) 时,下一步访问地址 \(y\) 出现了多少次。统计次数越多,说明这个搭配越可靠。L2 预取器会同时追踪多股访问流,并为每一股流维护各自的预测状态,因此即使多个线程或循环交替访问内存,它也能尽量跟上。

L2 预取器并不是 L1 的简单放大版。L1 擅长“顺流而下”,L2 则擅长“看出岔路”。两者配合起来,L1 负责快速响应的顺序流,L2 负责更宏观的模式识别,让备菜既快又准。

P(xt+1=y∣xt=x)=count(x,y)∑zcount(x,z)\displaystyle P(x_{t+1}=y \mid x_t=x) = \frac{\text{count}(x,y)}{\sum_{z} \text{count}(x,z)}
表格里记着:厨师刚拿起 \(x\) 这种食材时,接下来拿 \(y\) 的历史比例有多高。比例高,备菜员就敢提前把 \(y\) 端出去。
  1. 历史搭配计数 count\((x,y)\)在过去,主厨先取 \(x\) 再取 \(y\) 一共发生过多少次。
    count(x,y)\text{count}(x,y)
  2. 分母所有后继次数取完 \(x\) 之后,所有可能被取到的后继食材次数加起来,用来做归一化。
    ∑zcount(x,z)\sum_{z} \text{count}(x,z)
  3. 条件概率 \(P(x_{t+1}=y \mid x_t=x)\)在刚才取了 \(x\) 的前提下,下一步取 \(y\) 的估计概率。
    P(xt+1=y∣xt=x)P(x_{t+1}=y \mid x_t=x)
  4. 概率越高越敢预取只有当某个后继的出现比例足够高时,L2 才会把它当作一个可预测的模式。
    count(x,y)∑zcount(x,z)\frac{\text{count}(x,y)}{\sum_{z} \text{count}(x,z)}

步长检测机制

主厨今天做串串,每三块肉中间夹一块青椒。小助手观察了几串,发现取料位置总是“往前跳三格”,于是他把“三格”记在心里,下一次看到肉块,就知道接下来要去找青椒。预取器也这么工作:它用 PC(程序计数器)来区分是哪条取料指令,然后记录同一条指令相邻两次访问的内存地址差,这个差值就是步长。

不同代码位置产生的访问模式通常不同:循环 A 是逐个遍历数组,循环 B 是每隔四个元素取一次。预取器必须按 PC 分账,不能把 A 和 B 的规律混在一起。拿到同一 PC 的两个地址后,它计算地址差,再除以缓存行大小,就得到一个“以缓存行为单位”的整数步长。步长固定下来,下一次就能预测后续地址。

步长检测并非一次就定终身。预取器通常会结合置信度,只有多次观察到相同步长时,才把它当成有效规律。否则,一次偶然的顺序访问就可能让备菜员跑错方向。

s=At−At−1dline\displaystyle s = \frac{A_t - A_{t-1}}{d_{\text{line}}}
备菜员量出主厨两次取料位置之间隔着几格货架。隔 1 格是顺序,隔 \(k\) 格就是跨步。
  1. 两次访问地址 \(A_t - A_{t-1}\)主厨这次下手的位置,减去上次下手的位置,得到实际移动距离。
    At−At−1A_t - A_{t-1}
  2. 货架格宽 \(d_{\text{line}}\)一个缓存行的地址跨度,相当于备菜台上相邻两格的距离。
    dlined_{\text{line}}
  3. 步长 \(s\)以货架格数为单位的跳跃步长,正数向后备,负数向前备。
    ss
  4. 按 PC 分账只有同一条取料指令产生的地址差,才能放进同一本步长账本,避免把不同循环的规律混在一起。
    PC↦s\text{PC} \mapsto s

置信度评估

一位成熟的备菜员不会因为主厨偶尔连续两次拿了同一种食材,就断言整晚都会这样。他会先默默数几次:规律出现一次,信任值加一分;规律被打破,信任值扣两分;只有当信任值高过某个心理门槛,才真正开始提前跑库房。预取器里的置信度计数器,就是这份心理账本。

置信度机制的引入,是为了防止“过度猜疑”和“过度自信”。如果看到两次访问就立即预取,随机程序可能造成大量无用预取,挤占缓存空间;如果始终不预取,又白白浪费了规律访问的提速机会。因此,硬件为每个候选预取流维护一个计数器,预测命中就加一,预测失败就减一个惩罚值。

置信度机制把预取器从“莽撞的搬运工”变成了“有耐心的观察者”。它决定了预取器在什么情况下勇敢、什么情况下保守,是防止缓存污染的第一道防线。

Ct+1={min⁡(Ct+1, Cmax⁡),预测命中max⁡(Ct−β, 0),预测错误\displaystyle C_{t+1}=\begin{cases}\min(C_t+1,\, C_{\max}), & \text{预测命中}\\\max(C_t-\beta,\,0), & \text{预测错误}\end{cases}
信任值只在“猜到”的时候涨,在“猜错”的时候跌。跌到阈值以下,预取器就暂时收手。
  1. 当前信任值 \(C_t\)备菜员此刻对这个规律的信任程度,等价于连续命中的次数。
    CtC_t
  2. 命中分支 \(\min(C_t+1, C_{\max})\)猜对一次就加一分,但信任上限封顶,避免无限膨胀。
    min⁡(Ct+1, Cmax⁡)\min(C_t+1,\, C_{\max})
  3. 错误分支 \(\max(C_t-\beta, 0)\)猜错一次就扣掉 \(\beta\) 分,最低归零。
    max⁡(Ct−β, 0)\max(C_t-\beta,\,0)
  4. 阈值判断只有当 \(C_t\) 大于启动阈值时,预取动作才真正触发。
    Ct≥θC_t \ge \theta

污染控制策略

如果备菜员总是猜错,备菜台上就会被一堆用不上的食材占据,主厨真正要用的锅和盘子反而没地方放。这就是缓存污染:预取进来的数据没用,却把本来有用的缓存行挤了出去。L1 和 L2 都必须控制预取污染,否则预取反而会让性能下降。

硬件通常从几个方面控制污染:一是要求置信度足够高才预取;二是给预取进来的缓存行打上标记,让它们在缓存替换时拥有较低优先级,更容易被优先淘汰;三是动态监测预取准确率,一旦准确率下降,就降低预取深度或暂停预取。准确率的定义很简单:预取的数据里,有多少比例真正被 CPU 用到。

污染控制让预取器学会“克制”。好的预取器不是搬得越多越好,而是把有限的工作台空间留给真正会用到的数据,让命中率和有效容量同时保持健康。

accuracy=usefultotal_prefetched\displaystyle \text{accuracy} = \frac{\text{useful}}{\text{total\_prefetched}}
备菜员要统计:提前搬来的 100 份食材里,主厨最后真的用了多少份。用得太少,说明该收手了。
  1. 预取总数 total\_prefetched备菜员提前从库房搬上备菜台的食材总份数。
    total_prefetched\text{total\_prefetched}
  2. 有用预取数 useful其中被主厨真正用到、没有浪费的那部分份数。
    useful\text{useful}
  3. 准确率 accuracy有用的预取占总预取的比例,反映备菜员猜得准不准。
    usefultotal_prefetched\frac{\text{useful}}{\text{total\_prefetched}}
  4. 阈值节流当准确率低于某个阈值时,预取器就减少预取深度,甚至完全关闭对应数据流。
    accuracy<θprefetch⇒throttle\text{accuracy} < \theta_{\text{prefetch}} \Rightarrow \text{throttle}

典型访问模式

不同菜谱有不同的取料节奏:读菜谱是一行一行往下读,串串是隔三格取一次,做佛跳墙则要按流程一环扣一环。内存访问模式也大致分四类:顺序访问、跨步访问、链式访问和不规则访问。理解这些模式,才能理解预取器什么情况下能帮上忙。

顺序访问是最理想的情况,地址一个接一个,L1 流预取器闭着眼睛都能猜。跨步访问是每隔固定距离取一次,比如二维数组按列遍历时,步长等于一行的长度,这正好是步长检测的用武之地。链式访问没有固定步长,地址藏在链表节点的指针里,L2 预取器需要靠转移概率或下一跳信息来猜。不规则访问则几乎没有规律,连大管家也无能为力。

典型访问模式决定了预取器收益的上限。面向 AI 和数据库从业者的经验是:尽量把随机访问改写成连续或固定跨步访问,让硬件预取器站在你这一边。

Δa={L,顺序访问kL,跨步访问查下一跳指针,链式访问\displaystyle \Delta a =\begin{cases}L, & \text{顺序访问}\\kL, & \text{跨步访问}\\\text{查下一跳指针}, & \text{链式访问}\end{cases}
相邻两次访问的地址间隔,决定了备菜员该用哪一套猜法:顺数格子、数跨步,还是直接看指针。
  1. 地址间隔 \(\Delta a\)主厨这次取料与上次取料之间的地址距离。
    Δa=at+1−at\Delta a = a_{t+1} - a_t
  2. 顺序访问 \(L\)间隔恰好等于一个缓存行,数据像一本书一样一页页翻过去。
    Δa=L\Delta a = L
  3. 跨步访问 \(kL\)间隔是缓存行大小的整数倍,相当于每走 \(k\) 格才取一次料。
    Δa=kL\Delta a = kL
  4. 链式访问查指针没有固定间隔,地址由链表节点的 next 指针决定,预取器需要额外表项来预测下一跳。
    next=node.next\text{next} = \text{node.next}

调优实践建议

把备菜员的“提前量”调得恰到好处,是一门经验活。调太小,等主厨要时还没送到;调太大,备菜台被未来的食材占满,当下的菜反而没地方放。硬件层面,现代 CPU 通常提供寄存器或 BIOS 选项,可以开关 L1/L2 预取器、调整预取距离和预取深度。软件层面,我们无法直接命令硬件预取器,但可以通过数据布局和线程调度来配合它。

比如,把热数据按访问顺序连续存放,避免频繁上下文切换——因为每次进程切换,备菜员脑子里那本“当前主厨的习惯账本”可能就失效了。又比如,当发现 perf 报告里 cache miss 主要来自某种固定跨步访问时,可以调整循环步长、矩阵分块大小,或者用 prefetch 指令作为补充。调优是否有效,最终要用真实负载的基准测试说话,而不是只看理论参数。

调优的终点不是把某个寄存器调到“银弹”值,而是让应用的行为模式与硬件预取器对齐。顺序访问、固定步长、低上下文切换、高空间局部性,这些工程手段都能让备菜员把活干得更漂亮。

Dopt=⌈memory_latencyavg_access_interval⌉\displaystyle D_{\text{opt}} = \left\lceil \frac{\text{memory\_latency}}{\text{avg\_access\_interval}} \right\rceil
最优预取距离,约等于“从库房走一趟需要的时间”除以“主厨平均每隔多久取一次料”。这样,料正好在需要时送到。
  1. 内存延迟 memory\_latency备菜员从库房搬一趟货并回到工作台所需的周期数。
    memory_latency\text{memory\_latency}
  2. 平均取料间隔 avg\_access\_interval主厨连续两次取料之间平均等待多少个周期。
    avg_access_interval\text{avg\_access\_interval}
  3. 二者之比备菜员需要提前多少步开始走,才能在主厨开口那一刻刚好把货放到台上。
    memory_latencyavg_access_interval\frac{\text{memory\_latency}}{\text{avg\_access\_interval}}
  4. 向上取整 \(\lceil \cdot \rceil\)实际硬件只能按整数个缓存行预取,所以最后取不小于比值的整数。
    ⌈memory_latencyavg_access_interval⌉\left\lceil \frac{\text{memory\_latency}}{\text{avg\_access\_interval}} \right\rceil

AI 应用场景

场景一:AI 训练数据管道中的顺序扫描

阶段:数据加载与预处理

解决的问题:PyTorch 的 DataLoader 读取大规模 TFRecord 或 shard 文件时,如果文件内样本随机排列,访问模式接近随机,硬件预取器很难预测,内存延迟直接暴露在训练迭代中。通过把样本按访问顺序连续写入,并让 Batch 大小对齐缓存行,就能让 L1/L2 预取器“看懂”数据流。

具体案例:某图像分类训练任务中,原始数据按类别乱序存放,perf 显示 L2 cache miss 率约 35%。将 TFRecord 改写为顺序存储并按 epoch 顺序读取后,命中率上升,训练吞吐提升约 18%。

场景二:大模型推理中的 GEMM 算子优化

阶段:模型推理与算子优化

解决的问题:矩阵乘法需要反复扫描权重矩阵,循环顺序如果与内存布局不一致,会出现大量跨步访问。采用分块(tiling)和行主序布局,让内层循环访问连续地址,L2 预取器就能提前把下一块数据搬进缓存。

具体案例:优化 LLM 推理的 matmul 内核时,把权重按 K 维连续排布,并让 M 维分块大小匹配 L2 容量,在 A100 上观察到内存停顿周期明显减少。

场景三:多进程推理服务的 CPU 亲和性调度

阶段:在线推理服务部署

解决的问题:容器调度频繁切换 CPU,会导致硬件事务预取器状态被冲刷,每次切换后都要重新学习访问模式。通过设置 CPU affinity,让推理进程固定在一个核心上,能保住预取器的工作记忆。

具体案例:某 NLP 在线服务在 Kubernetes 上频繁漂移,P99 延迟抖动明显;绑定核后,L2 预取器命中率回升,P99 延迟降低约 12%。

Python 演示

python53 行
import random def simulate(trace, line_size=64, prefetch_distance=4,             confidence_threshold=2):    cache_lines = {}    prefetch_state = {}    hits = 0    total = 0    pc_key = 0  # 用固定PC模拟同一条取料指令     for addr in trace:        line = addr // line_size        total += 1         if line in cache_lines:            hits += 1        else:            cache_lines[line] = True         if pc_key not in prefetch_state:            prefetch_state[pc_key] = {                'last': line, 'stride': None, 'conf': 0            }        st = prefetch_state[pc_key]         if st['stride'] is not None:            for i in range(1, prefetch_distance + 1):                cache_lines[st['last'] + st['stride'] * i] = True            st['conf'] += 1            if st['conf'] > 100:                st['conf'] = 100         diff = line - st['last']        if st['stride'] is None:            st['stride'] = diff        elif diff != st['stride']:            st['conf'] = max(0, st['conf'] - 2)            if st['conf'] < confidence_threshold:                st['stride'] = diff         st['last'] = line     return hits / total random.seed(1)sequential = [i * 64 for i in range(2000)]strided = [i * 64 * 2 for i in range(2000)]random_trace = [random.randint(0, 2000 * 64) for _ in range(2000)] for name, trace in [('seq', sequential),                    ('strided', strided),                    ('random', random_trace)]:    print(name, 'hit rate:', simulate(trace))
目的:用 Python 模拟一个小型硬件预取器,输入三种访存地址序列,输出各自缓存命中率,直观对比顺序、跨步和随机访问的预取收益。
  1. line = addr // line_size把内存地址映射为缓存行编号,因为预取器的最小单位是缓存行。
  2. if line in cache_lines:检查这块数据是否已经像备菜一样被提前放上了工作台。
  3. for i in range(1, prefetch_distance + 1):一旦确认步长,就把未来几个缓存行全部提前“搬运”进缓存。
  4. cache_lines[st['last'] + st['stride'] * i] = True核心预取动作:按“上次位置 + 步长 × 提前量”推算后续地址。
  5. diff = line - st['last']计算相邻两次访问的地址差,这是步长检测的原始证据。
  6. st['conf'] = max(0, st['conf'] - 2)预测错误时扣信任分,对应置信度评估中的错误惩罚。
  7. st['stride'] = diff信任值跌破阈值后,放弃旧步长,改用新观察到的地址差。

常见误区

误区备菜员搬来的食材越多、被用到的比例越高,说明预取器越厉害,机器跑得越快。

正解预取命中率高只说明“搬来的东西没浪费”,但预取本身也占用缓存空间、内存带宽和硬件状态。如果一个程序本来空间局部性就很好,不需要预取也能命中,那么再提高预取深度只会挤占其他数据的位置。性能要看最终的有效内存延迟和程序运行时间,而不是单独看预取器准确率。

为何常见:人们习惯把命中率等同于性能,忽略了预取动作本身有成本和副作用。

误区既然提前备料能掩盖延迟,那就提前几十步、上百步,把所有未来的数据都搬过来。

正解预取距离太大会带来两个问题:一是数据在缓存里等太久,还没用到就被替换出去;二是过早占用缓存空间,把正在使用的数据挤出。预取距离应该接近“内存延迟 ÷ 平均访问间隔”,做到刚好在需要时到达。

为何常见:肉眼看到“提前”就觉得是好事,忽略了缓存容量有限和时间敏感性的约束。

误区L2 离 CPU 更远一些,所以 L2 预取器不过是把 L1 的连续访问预测做得更大更深。

正解L1 和 L2 预取器在设计目标上有本质差异。L1 主要追踪单条顺序流,反应快、开销小;L2 要同时追踪多条流,并使用转移概率表、多步预测等机制处理非连续和链式模式。它们的结构和工作方式不同,不能简单理解为“同一个放大镜”。

为何常见:从外部看都是“把数据提前拿来”,很容易忽略内部微架构的具体分工。

误区预取器是 CPU 硬件的一部分,操作系统切换进程是软件行为,两者互不干扰。

正解预取器内部记录了大量与当前进程相关的访问状态,比如 PC、步长、置信度。一旦操作系统切换进程,同一逻辑核上的访问流突然变成另一个程序的规律,旧状态不仅没有帮助,还可能误导预取。因此频繁上下文切换会让预取器不断“重新学习”,降低命中率。

为何常见:只看到硬件与软件的分界,忽略了预取器状态本质上依赖进程上下文。

误区既然程序里可以用 `prefetch` 手动预取,硬件预取器没用了,也不用关注数据布局。

正解软件预取和硬件预取是互补关系。硬件预取器能自动识别规律,适合通用顺序和跨步访问;软件预取则适合特定场景,比如链表下一跳或复杂的非规则地址。两者配合时,数据布局仍然重要——布局越规整,硬件预取器越容易帮忙,软件指令的负担也越轻。

为何常见:看到有新的手动工具,就误以为底层机制可以被完全替代。

面试题

Q1. 硬件预取器的主要目标是?

单选题 · easy

  • A.提高 CPU 主频
  • B.利用空间局部性隐藏内存访问延迟← 正确答案
  • C.增加缓存容量
  • D.替代 TLB 完成地址翻译

答案:B。预取器像备菜员,提前把后续数据搬进缓存,从而隐藏去内存取数的延迟。它不改变主频、不增加容量,也不负责地址翻译。

Q2. L1 Stream Prefetcher 最适合以下哪种访问模式?

单选题 · easy

  • A.按顺序访问连续缓存行← 正确答案
  • B.完全随机的地址跳转
  • C.每次访问不同指令地址
  • D.只读专用寄存器

答案:A。L1 流预取器专门识别固定步长的连续访问,最典型的就是数组顺序遍历。随机跳转没有规律,流预取器无法预测。

Q3. 步长检测中,预取器通常用什么来区分不同的访问流?

单选题 · mid

  • A.程序计数器 PC← 正确答案
  • B.操作系统进程 ID
  • C.缓存行颜色位
  • D.内存条插槽号

答案:A。PC 标识产生访存的是哪条指令。同一条指令的地址差才能计算步长,不同指令要分开记账,类似于不同菜谱要分开记。

Q4. 置信度计数器在预测命中时一般如何处理?

单选题 · mid

  • A.清零并重新计数
  • B.加一,且不超过上限← 正确答案
  • C.减一
  • D.保持不变

答案:B。命中一次就增加信任值,但信任值有最大值,避免无限膨胀。猜错时扣分,低于阈值就停止预取。

Q5. 什么是“预取污染”?

单选题 · mid

  • A.预取的数据在缓存中从未被使用,却挤掉了有用数据← 正确答案
  • B.内存数据被病毒感染
  • C.缓存行内容被修改后写回内存
  • D.多核 CPU 缓存不一致导致死锁

答案:A。预取污染指没用的预取数据占据缓存空间,导致真正要用的缓存行被替换出工作台。污染会抵消预取带来的收益。

Q6. L2 预取器比 L1 流预取器更强的核心能力是?

单选题 · hard

  • A.时钟频率更高
  • B.能追踪多条流并识别非连续模式← 正确答案
  • C.缓存行体积更大
  • D.能消除所有 cache miss

答案:B。L2 预取器负责更复杂的模式识别,比如多流、转移概率和跨步规律。它不是在速度上超过 L1,而是在“见识”上更广。

Q7. 以下哪些策略能降低预取污染?

多选题 · mid

  • A.只在置信度超过阈值时预取← 正确答案
  • B.给预取数据较低的替换优先级← 正确答案
  • C.每次访问都把整个内存预取一遍
  • D.预取准确率低时降低预取深度← 正确答案

答案:ABD。低置信度限制预取、预取标记低优先级、准确率下降时节流都是常用手段。选项 C 会制造大量无用预取,属于污染源。

Q8. 下列哪些程序特征有助于让硬件预取器发挥效果?

多选题 · hard

  • A.数组按内存顺序遍历← 正确答案
  • B.减少不必要的 CPU 上下文切换← 正确答案
  • C.每次访问都换一条新的取料指令
  • D.访问步长长期稳定← 正确答案

答案:ABD。顺序访问、固定步长和一致 PC 都是预取器喜欢的规律。频繁上下文切换会冲刷预取器状态,而频繁换 PC 会让步长账本无法累积。

Q9. 关于软件 prefetch 指令和硬件预取器,下列说法正确的是?

多选题 · hard

  • A.两者可以互补← 正确答案
  • B.数据布局对两者都重要← 正确答案
  • C.只要用了软件预取,硬件预取器就自动关闭
  • D.链式访问可以用软件预取辅助← 正确答案

答案:ABD。软件预取适合硬件看不透的链式跳转,两者互补;数据布局越规整,硬件预取器越省力。选项 C 错误,软件预取不会自动关闭硬件机制。

Q10. 若内存延迟为 100 周期,程序平均每 10 个周期访问一次内存,则最优预取距离大约是多少个缓存行?

单选题 · extreme

  • A.1
  • B.5
  • C.10← 正确答案
  • D.100

答案:C。最优距离 ≈ 延迟 ÷ 访问间隔 = 100 ÷ 10 = 10。备菜员提前 10 步出发,才能在主厨需要时刚好把食材送到,故选 C。