想象你常去的一家老式图书馆。管理员阿姨总能预判你的需求:你刚还回《三体》第一部,第二部已经躺在前台抽屉里;你做数据分析时习惯每隔三本借一本,她也常常提前把你要的那本拿了出来。你从不觉得这有多神奇,只是觉得“这家图书馆真快”。其实,CPU 世界里也有这样一位“管理员”。 CPU 访问内存的速度比寄存器慢好几个数量级——就像你从座位跑到马路对面的仓库取书。为了不让你等,硬件里藏着一位默默观察你阅读习惯的“图书管理员”,它叫硬件预取器(Hardware Prefetcher)。它记录你最近访问过哪些内存地址,推测你接下来要访问哪个,然后提前把数据从慢吞吞的内存搬到手边的缓存(Cache)里。等 CPU 真正开口要数据时,发现数据已经在眼前了——访存延迟就这样被悄悄藏了起来。 这篇文章会带你认识这位“管理员”的工作方式:它如何猜(顺序预取、步长预取),猜得准不准(准确率与覆盖率),以及你作为程序员如何写“借书单”(软件 prefetch 指令)帮它一把。

岗位关联

理解硬件预取机制,是编写缓存友好代码、优化访存密集性能的底层功夫。无论你做数据库引擎还是 AI 推理优化,这里都是绕不开的性能杠杆。

目录
  1. 可视化演示
  2. 预取基本原理
  3. 顺序预取策略
  4. 步长预取策略
  5. 预取性能指标
  6. 软件预取指令
  7. 实际应用效果
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

预取器预测访问地址提前加载数据到缓存,隐藏访存延迟。

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

预取原理与延迟隐藏 L1命中约4周期而主存访问需200周期 预取器提前将数据从内存加载到缓存 Latency_hidden = T_access - T_prefetch 通过预测未来访问地址隐藏访存延迟

预取基本原理

图书管理员的本事不在跑得快,而在“猜得早”。她不需要每次等你报书名才动身,而是看你刚拿起第一本,就推测你马上需要第二本,于是先把第二本从仓库搬到了前台。这份“提前量”,就是她让图书馆显得快的全部秘密。CPU 世界里,这份秘密对应一个朴素事实:内存太慢,不能每次都等。

现代 CPU 一个时钟周期不到 1 纳秒,而访问内存(DRAM)通常要几十到上百纳秒。如果每次取数都等内存,处理器绝大多数时间都在空转。于是 CPU 引入了多级缓存:L1、L2、L3,就像前台、中厅、后院三个越来越远但越来越大的书架。绝大多数紧挨着会用到的数据,都能在前台命中(Cache Hit);一旦没命中(Cache Miss),CPU 就得一直等到“后院仓库”把货送来。硬件预取器要做的,就是让“必中的下一击”提前到位,把 miss 变成 hit。

用公式描述这个代价。设命中率为 H,命中缓存延迟为 $T_{cache}$,未命中要额外花时间从内存搬运 $T_{mem}$,则平均访存时间:

预取器就像一个不断“补货”的管理员,把 H 这个命中率一点点抬高。H 从 90% 提升到 99%,平均访存时间几乎减半——管理员多跑几趟腿,读者少等很多次。这正是预取的全部意义:用提前搬运的“体力”,换回 CPU 最宝贵的“等待时间”。

Tavg=H×Tcache+(1−H)×TmemT_{avg} = H \times T_{cache} + (1 - H) \times T_{mem}
  1. 平均访存时间$T_{avg}$ 表示 CPU 每执行一次访存平均要花多少个周期,是评估缓存系统的基础指标。
    Tavg=H×Tcache+(1−H)×TmemT_{avg} = H \times T_{cache} + (1 - H) \times T_{mem}
  2. 命中部分$H \times T_{cache}$ 是命中缓存时的花费,因为数据就在手边,这部分延迟很短。
    H×TcacheH \times T_{cache}
  3. 未命中部分$(1 - H) \times T_{mem}$ 是未命中后等待内存搬运的额外代价,它在整个公式中占主导。
    (1−H)×Tmem(1 - H) \times T_{mem}

顺序预取策略

图书馆最常见的规律是“顺书架读”。管理员观察到一个读者每次还书的位置总在上一本旁边,于是她会习惯性地把相邻的下一本先拿到前台。这种“紧挨着猜”的直觉,是预取器最朴素也最有效的一招,对应到 CPU 世界里,就是顺序预取(Sequential Prefetching)。

程序中最常见的访问模式同样是顺序访问——遍历数组、读文件、扫描数据表,地址像翻书一样一页页往后翻。顺序预取器的策略简单直接:当某个缓存行 $L_i$ 被 CPU 访问时,立刻把下一个缓存行 $L_{i+d}$ 也读入缓存。这里的 $d$ 称为预取距离(Prefetch Distance),$d=1$ 是最保守的选择,每次多搬一行;$d=4$ 则更激进,一次搬四行,适合内存延迟特别大的场景。

管理员“顺书架读”的直觉看似简单,却是真实世界覆盖面最广的预测:大多数科学计算、流式处理和日志分析都被顺序模式主导。想让她猜得准,你只需把数据摆成“一排书”——连续的内存布局——剩下的交给她。现代 CPU 的流式预取器(Stream Prefetcher)甚至会同时跟踪多个顺序流,一次维护好几排“正在读的书”。

access(Li)  ⇒  prefetch(Li+d)\text{access}(L_i) \;\Rightarrow\; \text{prefetch}(L_{i+d})
  1. 触发条件$\text{access}(L_i)$ 表示 CPU 当前正在访问第 $i$ 个缓存行,这是预取器行动的“发令枪”。
    access(Li)\text{access}(L_i)
  2. 预取动作$\text{prefetch}(L_{i+d})$ 表示把第 $i+d$ 个缓存行提前从内存搬进缓存,为后续访问做好准备。
    prefetch(Li+d)\text{prefetch}(L_{i+d})
  3. 完整规则只要访问了 $L_i$,就立刻为 $L_{i+d}$ 发出预取请求,这是顺序预取的核心逻辑。
    access(Li)⇒prefetch(Li+d)\text{access}(L_i) \Rightarrow \text{prefetch}(L_{i+d})

步长预取策略

但并非所有读者都按顺序读。那位分析师总跳过两本取第三本;递归程序的指针也常以固定间隔跳跃。管理员观察两次便会心一笑:“又隔两本。”于是她开始按“隔两本”的节奏提前取书。这种对“等差数列”的直觉,对应硬件里的步长预取(Stride Prefetching)。

步长(Stride)是两次成功访问地址之间的差值。步长预取器的核心是一个小表格,记录最近几次访问的地址差,如果差值稳定出现,就认为找到了规律,并把下一次访问预测为“现有地址 + 步长”。经典的例子是按列访问二维数组:逻辑上你访问 a[i][j],实际每跳一行物理地址就偏移一整个行长度,地址差始终是同一个常数。

管理员靠“间隔”预判取书,步长预取器靠“地址差”预判访存。走列的二维数组、矩阵转置、按某种跨度采样的信号处理,都是它的用武之地。对程序员来说,如果无法避免跳跃,让跳跃保持固定步长,管理员就能继续“神机妙算”,预取器也能维持接近完美的命中率。

stride=addrk+1−addrkstride = addr_{k+1} - addr_k
next_addr=addrn+stridenext\_addr = addr_n + stride
  1. 计算步长$stride = addr_{k+1} - addr_k$,把相邻两次访问的地址相减,得到本次“跳跃”的距离。
    stride=addrk+1−addrkstride = addr_{k+1} - addr_k
  2. 确认规律当这个差值在多次访问中重复出现,预取器就判定访问模式为固定步长,可以放心预测。
    stride=addrk+1−addrk=addrk+2−addrk+1=⋯stride = addr_{k+1} - addr_k = addr_{k+2} - addr_{k+1} = \cdots
  3. 预测下一地址$next\_addr = addr_n + stride$,拿最近一次访问地址加上步长,就是下一个最可能被访问的位置。
    next_addr=addrn+stridenext\_addr = addr_n + stride

预取性能指标

管理员也不是每次都能猜中。她热心肠地提前从仓库搬回 10 本书,结果你只借走了 7 本——剩下 3 本白跑一趟,还占用了前台空间。这就是预取的代价,也是预取器设计者最头疼的权衡。于是需要两个数字来评价管理员:她拿来的是不是你要的(准确率),她有没有把你要的都拿来(覆盖率)。

预取准确率(Prefetch Accuracy)衡量预取操作的有效性:在全部预取动作里,有多少数据最终被 CPU 真正用到。

预取覆盖率(Prefetch Coverage)则衡量预取消除了多少本应发生的未命中:在所有本来会 miss 的访问中,有多少被预取提前“救活”。

这两个指标常常互相拉扯。预取器变得保守,准确率高但覆盖率低,许多 miss 没被提前消除;预取器变得激进,覆盖率上去了,但大量无效预取挤占缓存和内存带宽,可能让整体速度反而下降。硬件通常用一个可调阈值在两者之间找平衡。管理员心里也有一杆秤:宁可少拿几本,也别让错书占着前台——真正的高手,是在准确率和覆盖率之间走钢丝。

Accuracy=Useful PrefetchesTotal PrefetchesAccuracy = \frac{\text{Useful Prefetches}}{\text{Total Prefetches}}
Coverage=Misses EliminatedTotal MissesCoverage = \frac{\text{Misses Eliminated}}{\text{Total Misses}}
  1. 预取准确率$Accuracy = \frac{\text{Useful Prefetches}}{\text{Total Prefetches}}$,分子是有用的预取次数,分母是全部预取次数。
    Accuracy=Useful PrefetchesTotal PrefetchesAccuracy = \frac{\text{Useful Prefetches}}{\text{Total Prefetches}}
  2. 预取覆盖率$Coverage = \frac{\text{Misses Eliminated}}{\text{Total Misses}}$,分子是被预取消除的未命中数,分母是原本会发生的总未命中数。
    Coverage=Misses EliminatedTotal MissesCoverage = \frac{\text{Misses Eliminated}}{\text{Total Misses}}
  3. 两个视角准确率管“猜得对不对”,覆盖率管“备得全不全”,两者共同决定预取的实际收益。
    Quality=Accuracy×CoverageQuality = Accuracy \times Coverage

软件预取指令

有些读者更主动,进门就递上借书单:“我接下来要这几本,请提前准备。”管理员照单取书,一拿一个准。程序员也可以做同样的事——与其等硬件预取器“猜”,不如直接用软件 prefetch 指令告诉 CPU:“这个地址马上要用,帮我先搬到缓存里。”这就是软件预取(Software Prefetching)。

GCC/Clang 提供了内置函数 __builtin_prefetch(ptr, rw, locality),x86 平台则有 prefetcht0、prefetchnta 等指令。程序员可以在循环里手动插入这些指令,把“未来会用到”的地址提前暴露给硬件。关键在于提前量:太近,数据还在路上,CPU 照样等;太远,数据可能还没来得及用就被其他数据挤出缓存。经验上,提前轮数应接近“内存延迟 ÷ 单轮循环耗时”,并向上取整:

实践中,软件预取常用于内存延迟极长、且访问模式硬件猜不到的场合:链表遍历、稀疏矩阵、按索引跳跃的哈希扫描。配合编译器自动插桩(如 GCC 的 -fprefetch-loop-arrays),编译器能分析循环并自动插入指令。借书单写得越早越精准,prefetch 指令插得越靠前越贴合热点,命中率越高。软件预取让程序员从“被动等管理员猜”升级为“主动递纸条”,在关键循环里,这个动作往往能让性能翻上数倍。

dprefetch=⌈TmemoryTloop⌉d_{prefetch} = \left\lceil \frac{T_{memory}}{T_{loop}} \right\rceil
  1. 提前轮数$d_{prefetch}$ 表示在循环里要提前多少轮迭代发出预取,才能刚好让数据到达时被使用。
    dprefetch=⌈TmemoryTloop⌉d_{prefetch} = \left\lceil \frac{T_{memory}}{T_{loop}} \right\rceil
  2. 内存延迟$T_{memory}$ 是从访存请求发出到数据到达缓存的周期数,决定了“必须提前多久”。
    TmemoryT_{memory}
  3. 单轮耗时$T_{loop}$ 是循环体平均执行一轮的周期数,两者相除得到需要的迭代提前量。
    TloopT_{loop}

实际应用效果

回到图书馆本身——读者形形色色:按顺序读的文学青年、隔三本借一本的数据分析师、在特定区域反复翻阅的论文党。管理员对不同读者采用不同策略:顺序读者顺势走,跳跃读者数间隔,反复读者则把常用书长期摆在最顺手的位置。整体下来,全馆平均等待时间大幅缩水。真实系统也是如此,预取最典型的舞台就是数据库扫描。

PostgreSQL、ClickHouse 做全表扫描时,数据页是一口气顺序读出的;如果每页都等内存,性能会被访存瓶颈死死卡住。数组遍历、矩阵乘法、卷积计算同样受益。把预取和循环展开等优化结合,命中率能从 70% 拉到 95% 以上,访存加速比可以直观写成:

要吃到这波红利,最实用的一条建议是让数据访问保持规整:用连续内存(数组而非散乱链表)、按行访问二维数组、把热点结构体对齐到缓存行。这些“缓存友好代码”不是玄学,而是让管理员更容易看穿你的阅读习惯,把预取器的优势最大化。管理员让读者几乎不用等书,预取器让 CPU 的访存等待被悄悄填满——你写代码时的每一个布局决定,都在告诉管理员“我接下来要看哪一排书”。对齐、连续、步长稳定,这三件事做到位,性能自然水到渠成。

Speedup=TbeforeTafterSpeedup = \frac{T_{before}}{T_{after}}
  1. 加速比定义$Speedup$ 表示优化前后平均访存时间之比,大于 1 说明预取带来了收益。
    Speedup=TbeforeTafterSpeedup = \frac{T_{before}}{T_{after}}
  2. 优化前开销$T_{before}$ 是未预取时的平均访存时间,包含了大量未命中的等待代价。
    TbeforeT_{before}
  3. 优化后开销$T_{after}$ 是预取生效后的平均访存时间,大部分等待被提前搬运悄悄隐藏。
    TafterT_{after}

AI 应用场景

数据库扫描与查询加速

阶段:数据预处理(大规模数据扫描与加载)

解决的问题:全表扫描和范围查询需要顺序读取海量数据页,传统实现里 CPU 每读一批数据就等一次内存,磁盘和内存的延迟被直接暴露在查询路径上。利用硬件顺序预取 + 软件 prefetch 指令,可以把“等数据”的时间藏起来,让扫描吞吐量成倍提升。

具体案例:ClickHouse 的 MergeTree 引擎按列连续存储数据,天然适合预取。性能调优时配合 __builtin_prefetch 手动预取下一批列数据块,大查询延迟下降约 30%,CPU 利用率明显上升。

AI 训练数据管道加载

阶段:训练数据加载与增强

解决的问题:训练时要把海量图片和向量从磁盘读入内存再做增强,数据搬运时间常常超过 GPU 计算时间。预取可以将数据加载与计算重叠,让 GPU 不再“饿肚子”。

具体案例:PyTorch DataLoader 的 prefetch_factor 参数就是软件预取思想的应用——数据加载线程提前准备多个 batch,训练主线程直接取用。调高预取因子配合内存映射文件,训练吞吐可提升 20% 以上。

Python 演示

python50 行
"""模拟硬件顺序预取的效果:对比无预取与预取距离 d=1 时的命中率与总耗时。"""from collections import deque  def simulate(accesses, capacity=64, miss_latency=20, prefetch_distance=0):    cache = {}            # 缓存: 地址 -> 有效标记    queue = deque()       # FIFO 淘汰顺序    hits = 0    cycles = 0     for addr in accesses:        if addr in cache:            hits += 1            cycles += 1               # 命中延迟 1 周期        else:            cycles += miss_latency    # 未命中延迟 20 周期            cache[addr] = True            queue.append(addr)            if len(queue) > capacity:                old = queue.popleft()                cache.pop(old, None)         if prefetch_distance > 0:            pf = addr + prefetch_distance            if pf not in cache:                cache[pf] = True      # 提前把下一块搬进缓存                queue.append(pf)                if len(queue) > capacity:                    old = queue.popleft()                    cache.pop(old, None)     return cycles, hits / len(accesses)  def main():    accesses = list(range(200))  # 顺序访问 0..199     cycles_no, rate_no = simulate(accesses, prefetch_distance=0)    cycles_yes, rate_yes = simulate(accesses, prefetch_distance=1)     print(f"无预取:   命中率 {rate_no:6.1%}  总耗时 {cycles_no} 周期")    print(f"顺序预取:  命中率 {rate_yes:6.1%}  总耗时 {cycles_yes} 周期")    print(f"加速比: {cycles_no / cycles_yes:.2f}x")  if __name__ == "__main__":    main()
目的:用一个小型缓存模拟器对比“无预取”与“顺序预取”在顺序访问场景下的命中率和总耗时,直观展示预取的加速效果。
  1. cache[addr] = True未命中时把数据装入缓存,标记为有效。
  2. queue.append(addr)记录 FIFO 淘汰顺序,模拟缓存容量有限。
  3. old = queue.popleft(); cache.pop(old, None)缓存满了就淘汰最旧的行,模拟真实缓存的替换策略。
  4. pf = addr + prefetch_distance计算预取目标:当前地址加预取距离。
  5. cache[pf] = True提前把下一块数据搬进缓存,这是顺序预取的核心动作。
  6. cycles += miss_latency未命中时要等 20 个周期,命中只要 1 个周期。
  7. simulate(accesses, prefetch_distance=0)用预取距离 0 模拟无预取的基线表现。
  8. cycles_no / cycles_yes两个场景总耗时之比,就是预取带来的加速比。

常见误区

误区以为只要加了预取程序就一定会更快,预取次数越多、距离越大,性能提升越明显。

正解预取不是免费的。每一次预取都要消耗内存带宽和缓存空间,如果预取了 CPU 根本用不到的数据,这些“错书”会挤占前台空间,把真正有用的数据挤出去,性能反而下降。准确率和覆盖率的平衡才是关键。

为何常见:教学演示总是展示预取的正向效果,忽略了它抢占资源时的副作用,让人误以为多多益善。

误区CPU 自带硬件预取器,我写什么代码它都会自动帮我优化,数据布局随便来。

正解硬件预取只能识别规律性强的访问模式,比如顺序或固定步长。链表追逐、指针跳转、依赖很强的随机访问,硬件根本“猜不中”。这时候软件预取和缓存友好的数据布局(连续数组、结构体对齐)依然是程序员不可推卸的责任。

为何常见:硬件预取太“隐形”,让人误以为它是万能的,忽略了它依赖访问规律这个前提。

误区反正预取器会自动预判,我随便乱访问内存,它也能帮我加速。

正解顺序预取只对局部性强的顺序模式有效,步长预取只对稳定步长的跳跃有效。如果程序在完全随机地访问内存,预取器预测什么都是错的,反而会因为大量无效预取拖慢系统。随机访问的优化要靠数据重排或索引结构变化,而不是指望预取。

为何常见:只看到演示里规整数组的完美效果,没意识到预取器建立在对规律的统计之上。

误区预取覆盖率代表了消除的未命中数,覆盖率越高,说明预取越成功,性能应该越好。

正解覆盖率提升往往伴随准确率暴跌。激进预取会触发大量无效搬运,挤占内存带宽、污染缓存,最终导致整体性能不升反降。评价预取必须同时看准确率和覆盖率,两者此消彼长的关系才是设计的核心。

为何常见:从“覆盖率”这个名字直观理解成“覆盖越多越赚”,忽略了无效预取的成本。

误区在循环里随便加一句 `__builtin_prefetch`,编译器会搞定一切,不用管位置和时机。

正解软件预取最讲究“提前量”。插得太近,数据还没到达缓存,CPU 照样 miss;插得太远,预取的数据可能被后续访问挤出缓存,白忙一场。提前轮数需要根据内存延迟和循环体耗时估算,还要避免在条件分支上造成额外开销。

为何常见:调用 API 看起来很简单,容易让人忽略背后“时机”这个最敏感的参数。

面试题

Q1. 硬件预取的核心思想是什么?

单选题 · easy

  • A.降低 CPU 时钟频率以减少发热
  • B.预测未来访问并提前把数据加载到缓存← 正确答案
  • C.扩大内存条的物理容量
  • D.压缩磁盘上的数据文件

答案:B。硬件预取器通过观察历史访问规律,预测CPU即将访问的地址,提前把数据从慢速内存搬到高速缓存,从而隐藏访存延迟。A是降频节能,C是扩容,D是存储压缩,都与“提前搬运”无关。

Q2. 下面哪种访问模式最适合顺序预取?

单选题 · easy

  • A.完全随机的地址访问
  • B.按数组下标递增读取← 正确答案
  • C.二叉树递归遍历
  • D.哈希表查找

答案:B。顺序预取假设“下一个地址就在旁边”,按数组下标递增刚好命中这个规律。随机访问、递归遍历和哈希查找都是跳跃式的,顺序预取无法准确预测。

Q3. 某程序遍历数组时,访问第 i 块数据的同时预取第 i+1 块。这种预取距离 d 等于多少?

单选题 · mid

  • A.0
  • B.1← 正确答案
  • C.2
  • D.缓存行大小

答案:B。预取距离 d 表示比当前访问超前几个缓存行。访问 i 块时预取 i+1 块,说明 d=1,是最保守的“每步多搬一行”策略。

Q4. 预取准确率(Accuracy)的定义是?

单选题 · mid

  • A.缓存命中总次数除以总访问次数
  • B.有用的预取次数除以总预取次数← 正确答案
  • C.预取行数除以缓存总容量
  • D.内存带宽利用率

答案:B。准确率衡量预取的数据有多少真正被CPU使用,是“有没有猜对”的指标。A是缓存命中率,C是空间占用率,D是带宽利用率,都不是对预取行为的质量评估。

Q5. 预取覆盖率(Coverage)的定义是?

单选题 · mid

  • A.被预取消除的未命中数除以总未命中数← 正确答案
  • B.预取次数除以缓存容量
  • C.有用预取数除以总数
  • D.命中缓存次数除以总访问数

答案:A。覆盖率衡量预取在全部未命中里的“救活比例”。B描述的是空间占用,C是准确率,D是命中率,只有A准确描述覆盖率。

Q6. 平均访存时间公式 T = H × T_cache + (1−H) × T_mem 中,硬件预取的作用是?

单选题 · mid

  • A.减小 T_cache
  • B.提高命中率 H← 正确答案
  • C.增大 T_mem
  • D.减小 T_mem

答案:B。预取把原本会 miss 的访问提前变成 hit,本质是提高命中率 H。预取并不能缩短内存本身的延迟 T_mem,也不能改变缓存命中延迟,它只负责让数据“提前到位”。

Q7. 步长预取器稳定检测到访问序列 100, 140, 180, 220,下一个访问地址预测为?

单选题 · hard

  • A.240
  • B.260← 正确答案
  • C.280
  • D.300

答案:B。相邻地址差恒为 140−100=40,步长是 40,因此下一个地址预测为 220+40=260。B正确,A把步长误当20,C把步长误当60,D把步长误当80。

Q8. 下面哪些是硬件预取可能付出的代价?

多选题 · mid

  • A.额外占用内存带宽← 正确答案
  • B.污染缓存,挤掉有用数据← 正确答案
  • C.提高 CPU 主频
  • D.增加功耗← 正确答案

答案:ABD。预取要在内存和缓存之间搬运额外数据,必然占用带宽;预取了用不上的数据会污染缓存;额外的搬运动作也会增加功耗。预取与 CPU 主频无关,C 不正确。

Q9. 关于软件预取指令的提前量,下列说法正确的是?

多选题 · hard

  • A.提前太近,数据还没到达,仍然会未命中← 正确答案
  • B.提前太远,数据可能被其他数据挤出缓存← 正确答案
  • C.提前量越大越好,越早发出越安全
  • D.提前量取决于内存延迟和循环体耗时← 正确答案

答案:ABD。软件预取的关键是时机恰好。太近数据未到,太远数据被挤出,所以提前量要按 内存延迟÷单轮耗时 来估算。C说“越大越好”明显错误,D才是正确做法。

Q10. 某程序访问序列为 0, 2, 4, 6, 8…,步长预取器每次预取两个地址,覆盖率接近 100%,但准确率只有 50%,最可能的原因是?

单选题 · extreme

  • A.每个访问地址旁都预取了一个用不到的地址← 正确答案
  • B.内存带宽不足导致预取速度太慢
  • C.缓存容量太小放不下预取数据
  • D.程序访问频率远高于预取频率

答案:A。步长为 2 的序列每个地址都会被用到,覆盖率自然高;但每次预取两个地址意味着有一半预取是多余的(例如访问0时预取了1和2,其中1永远不会被访问),所以准确率只有50%。这是“激进预取换覆盖率”的典型代价。