系列文章《深入 perf 第二版:理解 CPU 的每一个时钟周期》第 2 篇
perf list列出的命名事件只覆盖了 PMU 硬件支持事件的一小部分。ARM Cortex-A76 有数百个硬件事件,perf 内置的只有二十几个。本文教你用原始事件编号直接与 PMU 硬件对话,并用 STALL_FRONTEND/STALL_BACKEND 做 Top-Down 微架构分析。
"Not everything that counts can be counted, and not everything that can be counted counts."(不是所有重要的东西都能被计量,也不是所有能计量的东西都重要。)
------ William Bruce Cameron
上一篇我们掌握了 cycles、instructions、IPC 这三个基础指标(第 1 篇《你真的会用 perf 吗?从 cycles 和 instructions 说起》)。但 perf list 能列出的事件只是冰山一角------本篇教你用原始事件编号,直接读取 PMU 的数百个隐藏指标。
[提示] :本篇假设你已读过第 1 篇的 cycles/instructions/IPC 概念。如果对
perf stat的基本用法还不熟悉,建议先回看第 1 篇。
3. 原始 PMU 事件编号(Raw Event)
3.1 为什么需要原始事件
团队怀疑某个后端服务存在流水线后端停顿瓶颈------IPC 只有 0.4,但 perf list 里翻来翻去找不到 STALL_BACKEND 这个事件。直到有人翻出 ARM Cortex-A76 的 Technical Reference Manual,发现后端停顿对应事件编号 0x24,用 perf stat -e r0024 手动指定,一条命令就确认了 56% 的 cycles 浪费在后端停顿上。谜底揭开了------但如果不知道原始事件编号这个"后门",这个瓶颈可能永远藏在暗处。
那么,什么是原始事件编号?简单说,它是绕过 perf 内置别名、直接用十六进制数字指定 PMU 硬件事件的方式。
perf list 列出的是内核预定义的事件别名,但这只覆盖了 PMU 硬件支持的一小部分事件。例如:
bash
perf list | grep -c "Hardware" # 通常只有 10~20 个预定义硬件事件
而一颗现代 CPU 的 PMU 往往支持 上百个 微架构事件(前端停顿、后端停顿、各级 cache 详细统计、TLB 事件等)。要使用这些事件,就需要通过原始事件编号(Raw Event) 来指定。在第 1 篇中我们用 perf stat -e cycles,instructions 采集命名事件,而原始事件编号让你突破这个"菜单"的限制,直接点"隐藏菜单"。
3.2 ARM64 原始事件语法
ARM64 的 PMU 事件编号存储在 PMEVTYPER(Performance Monitors Event Type Register,事件类型选择寄存器)中,语法为:
text
rXXXX
其中 XXXX 是事件编号的十六进制表示。PMEVTYPER 寄存器的 evtCount 字段为 16 位(bits [15:0]),但常用事件集中在低 10 位(0x0000-0x03FF),所以常见事件通常只需 2-3 位十六进制。其中 0x0000-0x003F 为架构级事件,从 0x0040 起为微架构级事件(Implementation-defined)。
这两个范围的区别很实际:架构级事件 (如 0x08 INST_RETIRED、0x11 CPU_CYCLES)是 ARM 架构定义的通用事件,主流 ARMv8 CPU 广泛支持(其中 CPU_CYCLES 等核心事件为强制实现,STALL_FRONTEND 等为推荐实现)------你写的 perf 命令在 Cortex-A76 和 Neoverse N1 上都能跑。微架构级事件 (从 0x0040 起,部分核心还有 0x4000+ 扩展范围)则是各厂商自定义的,只在特定核心上有效。换句话说,架构级事件是"通用菜单",微架构级事件是"特色菜"------换一家餐厅(换一颗 CPU)可能就点不到了。
bash
# 例如:STALL_FRONTEND 的事件编号是 0x23
perf stat -e r0023 ./my_program
# STALL_BACKEND 的事件编号是 0x24
perf stat -e r0024 ./my_program
# 同时统计多个原始事件
perf stat -e r0023,r0024,r0011 ./my_program # r0011 = CPU_CYCLES
等一下------如果 PMU 通用计数器只有 4-6 个(以 Cortex-A76 为例是 6 个),我同时指定 10 个原始事件怎么办?
好问题。这就是 multiplexing(多路复用) 要解决的问题。当事件数超过硬件计数器数量时,内核会在时间片之间轮转分配计数器,最后按比例线性缩放估算总数(估算值 = 采集值 × enabled_time / running_time)。代价是精度下降------perf 输出中会出现类似 (83.33%) 的百分比,表示该事件只有 83% 的时间在实际计数。所以关键事件尽量控制在 4-6 个以内,避免 multiplexing 影响精度。
[提示] :ARM64 PMUv3 有一个专用的周期计数器 PMCCNTR,独立于 6 个通用计数器。这意味着
cycles(CPU_CYCLES)不占用通用计数器名额------当你同时监控cycles和其他 6 个事件时,实际上用的是 PMCCNTR + 6 个通用计数器,不会触发 multiplexing。但instructions(INST_RETIRED)没有专用计数器,会占用一个通用名额。
3.3 x86 原始事件语法
x86 的 PMU 事件由两部分组成:Event 编号 + Umask,最常见的短格式为:
text
rUUEE
其中:
EE(低字节):Event 编号------选择要监控的事件类别UU(高字节):Umask(子事件选择器)------在该类别内筛选具体子类型
为什么 x86 需要两部分,而 ARM 只用一个编号?因为 x86 PMU 的设计是"一个 Event 编号对应一组相关事件,用 Umask 位选择要计哪几种"。举个例子:Event 0x24 代表 "L2 请求"这一大类,Umask 0x3F 选择其中的 "所有 miss",而 Umask 0x01 只选 "demand 读 miss"。好处是一个 Event 编号能覆盖多个细分场景;代价就是语法比 ARM 复杂一层。
bash
# 例如:L2_RQSTS.MISS(L2 cache 请求 miss,以 Skylake 为例)
# Event = 0x24, Umask = 0x3F
# 注意:Umask 值因微架构而异,其他微架构需查阅对应 SDM 或 perfmon event list
perf stat -e r3F24 ./my_program
# MEM_LOAD_RETIRED.L3_MISS
# Event = 0xD1, Umask = 0x20
perf stat -e r20D1 ./my_program
[注意] :
rUUEE只是最简单的短格式。现代 Intel CPU(如 Sapphire Rapids)的某些事件还需要cmask、inv、edge、any等额外字段,完整格式可以扩展到rNNNNNNNN(最多 8 位十六进制)。遇到复杂事件时,建议使用cpu/event=0xEE,umask=0xUU,cmask=N/完整语法(见 3.4 节),更清晰也更不容易出错。详见 Intel SDM(Software Developer's Manual)Vol. 3 Performance Monitoring 章节。
[提示] 试一试 :在你的 x86 机器上运行perf list pmu,找到一个感兴趣的事件和它的编号,然后用rXXXX格式采集一次数据。例如perf stat -e r3F24 sleep 1,看看能否成功采集到 L2 cache miss 数据。
3.4 完整语法与 name= 别名技巧
除了简写的 rXXXX,perf 还支持更具可读性的完整语法:
ARM64 完整语法
bash
# 格式:armv8_pmuv3/event=0xNN/
perf stat -e armv8_pmuv3/event=0x23/ ./my_program
# 加上 name= 别名,让输出更易读
perf stat -e armv8_pmuv3/event=0x23,name=stall_frontend/ ./my_program
perf stat -e armv8_pmuv3/event=0x24,name=stall_backend/ ./my_program
使用 name= 后,perf 输出中会显示你指定的名称,而不是难以辨认的事件编号:
text
# 不带 name=
850,000,000 armv8_pmuv3/event=0x23/
# 带 name=
850,000,000 stall_frontend
x86 完整语法
bash
# 格式:cpu/event=0xEE,umask=0xUU/
perf stat -e cpu/event=0x24,umask=0x3F,name=l2_miss/ ./my_program
[提示] :
name=别名只影响显示,不影响实际采集的事件。善用name=能让复杂的perf stat输出变得一目了然。
3.5 如何查找事件编号
事件编号可以从以下渠道获取:
方法 1:sysfs 文件系统
bash
# 列出所有 PMU 事件
ls /sys/bus/event_source/devices/armv8_pmuv3/events/
# 或者 x86
ls /sys/bus/event_source/devices/cpu/events/
# 查看某个事件的编号
cat /sys/bus/event_source/devices/armv8_pmuv3/events/stall_frontend
# 输出类似:event=0x23
方法 2:ARM 架构参考手册(TRM)
- 对于 ARM64 通用事件:参考 Arm Architecture Reference Manual(ARMv8-A)的 PMU 章节
- 对于特定核心的微架构事件:参考对应核心的 Technical Reference Manual(如 Cortex-A76 TRM)
方法 3:内核源码
bash
# ARM64 通用事件定义
# 文件路径:arch/arm64/kernel/perf_event.c 或 include/linux/perf/arm_pmuv3.h
# x86 事件映射
# 文件路径:arch/x86/events/intel/core.c
方法 4:perf list 查询
bash
# 列出所有事件(推荐,所有版本通用)
perf list
# 按类型筛选
perf list hw # 只看硬件事件
perf list pmu # 按关键词 "pmu" 筛选(行为随版本而异)
# 某些版本支持 raw dump 模式
perf list --raw-dump pmu # 较早版本
perf list --raw-dump # 较新版本可能不带 pmu 参数
perf list -j # JSON 格式输出(较新版本,约 6.x+)
[注意] :
perf list的子命令和参数在不同版本间有变化。如果某个命令报错,先用perf list pmu作为通用替代。
3.6 常见问题与避坑
使用原始事件之前,先了解这几个常见坑:
权限问题
bash
# 检查当前权限级别
cat /proc/sys/kernel/perf_event_paranoid
# -1:完全禁用限制(等同 root,仅用于开发/测试环境)
# 0:允许非 root 访问所有 CPU 事件(含内核态)
# 1:非 root 只能采集用户态事件(默认值)
# 2:非 root 只能采集自己进程的用户态事件
# 3(Debian/Ubuntu 特有):非 root 完全禁止
# 临时放开限制(需 root)
echo 0 > /proc/sys/kernel/perf_event_paranoid
[注意] :当
perf_event_paranoid >= 2时,非 root 用户使用原始事件可能报Permission denied。生产环境建议通过CAP_PERFMONcapability 授权,而不是全局降低 paranoid 级别。
虚拟化环境
在虚拟机中,PMU 事件可能不完整或不可用------KVM 需要配置 vPMU 透传(-cpu host 或 -cpu xxx,pmu=on),否则只能看到少数模拟的通用事件。容器环境中通常可以正常使用宿主机 PMU(除非被 seccomp 或 cgroup 限制)。
PMU 名称差异
不同内核版本中 PMU 设备名称可能不同。ARM64 上通常是 armv8_pmuv3,但某些旧内核或特定平台可能使用其他名称。用 ls /sys/bus/event_source/devices/ 确认你的系统上的实际名称。
3.7 ARM64 常用 PMU 事件速查表
以下是 ARMv8 PMUv3 架构定义的常用事件(所有 ARMv8 CPU 通用):
| 事件编号 | 名称 | 说明 |
|---|---|---|
0x01 |
L1I_CACHE_REFILL |
L1 指令 cache 未命中 |
0x02 |
L1I_TLB_REFILL |
L1 指令 TLB 未命中 |
0x03 |
L1D_CACHE_REFILL |
L1 数据 cache 未命中 |
0x04 |
L1D_CACHE |
L1 数据 cache 访问次数 |
0x05 |
L1D_TLB_REFILL |
L1 数据 TLB 未命中 |
0x08 |
INST_RETIRED |
退休指令数(= instructions) |
0x09 |
EXC_TAKEN |
异常发生次数 |
0x0A |
EXC_RETURN |
异常返回次数 |
0x10 |
BR_MIS_PRED |
分支预测失败次数 |
0x11 |
CPU_CYCLES |
CPU 活跃周期(= cycles) |
0x12 |
BR_PRED |
可预测分支的推测执行次数(包含预测正确和预测错误的分支) |
0x13 |
MEM_ACCESS |
数据内存访问次数 |
0x14 |
L1I_CACHE |
L1 指令 cache 访问次数 |
0x15 |
L1D_CACHE_WB |
L1 数据 cache 写回 |
0x16 |
L2D_CACHE |
L2 数据 cache 访问次数 |
0x17 |
L2D_CACHE_REFILL |
L2 数据 cache 未命中 |
0x18 |
L2D_CACHE_WB |
L2 数据 cache 写回 |
0x19 |
BUS_ACCESS |
总线访问次数 |
0x23 |
STALL_FRONTEND |
前端停顿周期 |
0x24 |
STALL_BACKEND |
后端停顿周期 |
0x25 |
L1D_TLB |
L1 数据 TLB 访问次数 |
0x26 |
L1I_TLB |
L1 指令 TLB 访问次数 |
0x2D |
L2D_TLB_REFILL |
L2 数据 TLB 未命中 |
0x2F |
L2D_TLB |
L2 数据 TLB 访问次数 |
0x31 |
REMOTE_ACCESS |
远端(跨 NUMA)内存访问 |
[提示] :以上是架构级事件(Architecture-defined),主流 ARMv8 实现普遍支持(其中
CPU_CYCLES、INST_RETIRED等核心事件为强制实现,其余为推荐实现)。具体 CPU 核心(如 Cortex-A76、Neoverse N1)还会有额外的微架构级事件(Implementation-defined),编号通常从0x40起,需查阅对应 TRM。注意
BR_PRED(0x12)的含义是"可预测分支推测执行次数"(不区分预测对错),BR_MIS_PRED(0x10)只计退休阶段确认的预测失败。两者在流水线不同阶段计数,简单相减并不精确------如需评估分支预测质量,直接看BR_MIS_PRED / BR_PRED的比例即可(该比例同样是近似值,但作为趋势指标足够判断分支预测是否存在问题)。
3.8 Uncore PMU 事件简介
前面介绍的所有事件都发生在 CPU 核心"内部"。但有一类性能问题------比如多核竞争 LLC、内存带宽打满------根因并不在核心里,而在核心之间的"公共设施"上。这就需要 Uncore 事件。
Uncore (也叫 System 或 off-core)是指 CPU 核心之外、但仍在芯片上的组件,包括:
- LLC(Last Level Cache):最后一级缓存(通常 L3)
- IMC(Integrated Memory Controller):内存控制器
- 互联总线:核心之间的通信网络(如 Intel 的 Ring Bus / Mesh)
- PCIe 控制器 、IO 单元 等
Uncore 事件用于分析这些共享资源的行为:
bash
# 查看系统支持的 Uncore PMU
ls /sys/bus/event_source/devices/ | grep uncore
# x86 示例:统计 LLC miss(PMU 名称因平台而异:
# Haswell/Broadwell: uncore_cbox_N, Skylake-SP+: uncore_cha_N)
perf stat -e uncore_cbox_0/event=0x34,umask=0x08,name=llc_miss/ -a sleep 5
# ARM64 示例(如果平台支持)
perf stat -e arm_cmn_0/event=0x01,name=cmn_rxflit/ -a sleep 5
[注意]:Uncore 事件有三个重要限制:
- 必须使用
-a(全系统模式) ,Uncore PMU 不绑定特定进程,缺少-a会报错- 容器/虚拟机内通常不可用,Uncore PMU 属于物理硬件资源,虚拟化层一般不暴露
- 多 socket 系统中各 socket 的 Uncore PMU 相互独立 ,需要分别指定(如
uncore_cbox_0对应第一个 LLC slice)
3.9 实战:用 STALL_FRONTEND + STALL_BACKEND 做 Top-Down 分析
Top-Down 分析方法将 CPU 流水线的效率问题分为几大类。这里使用的是 ARM 简化版 Top-Down 方法 ------用 STALL_FRONTEND 和 STALL_BACKEND 两个停顿周期计数器来判断瓶颈方向:
text
总 cycles
/ \
有效工作 流水线停顿
/ \
前端停顿 后端停顿
(取指/解码) (执行/访存)
STALL_ STALL_
FRONTEND BACKEND
[注意] ARM Top-Down vs Intel TMA :Intel 的 TMA(Top-down Microarchitecture Analysis)将每个 pipeline slot 分为四类------Frontend Bound、Backend Bound、Bad Speculation、Retiring,四者加起来恰好 100%。ARM 的方法更简单:
STALL_FRONTEND和STALL_BACKEND只计停顿周期数,二者可能重叠(同一周期前端和后端同时停顿),所以STALL_FRONTEND + STALL_BACKEND可能大于cycles。ARM 的方法用于快速判断瓶颈方向足够好,但不要把它当作精确的百分比分解。详见 ARM Telemetry Solution Guide。
前端停顿意味着什么? 流水线前端负责取指令和解码------如果前端停顿占比高,说明 CPU "吃不饱",可能的原因包括:I-cache miss(代码太大或跳转太多)、ITLB miss(代码分布在太多页上)、指令解码瓶颈(复杂指令序列)。
后端停顿意味着什么? 流水线后端负责执行和访存------如果后端停顿占比高,说明 CPU "消化不了",可能的原因包括:D-cache miss(数据访问模式差)、内存延迟高(穿透到 DDR)、执行单元争用(某类指令太密集)。
步骤 1:采集数据
bash
perf stat -e cycles,instructions,\
armv8_pmuv3/event=0x23,name=stall_frontend/,\
armv8_pmuv3/event=0x24,name=stall_backend/ \
./my_program
步骤 2:假设得到以下输出
text
Performance counter stats for './my_program':
5,000,000,000 cycles
2,000,000,000 instructions # 0.40 insn per cycle
1,500,000,000 stall_frontend
2,800,000,000 stall_backend
IPC 只有 0.40------第 1 篇我们说过 IPC < 1 需要关注,这里显然有严重的流水线效率问题。接下来用停顿指标定位方向。
步骤 3:展开计算各比例
text
前端停顿占比 = stall_frontend ÷ cycles
= 1,500,000,000 ÷ 5,000,000,000
= 0.30 = 30%
后端停顿占比 = stall_backend ÷ cycles
= 2,800,000,000 ÷ 5,000,000,000
= 0.56 = 56%
有效工作占比 ≈ 1 - 前端停顿占比 - 后端停顿占比
= 1 - 0.30 - 0.56
= 0.14 = 14% (近似值,因停顿可能重叠)
为什么停顿会重叠?因为流水线是多级并行的------前端取指令的同时,后端在执行之前取到的指令。在同一个时钟周期里,前端可能因 I-cache miss 停住,后端也可能因 D-cache miss 停住,两个计数器同时 +1。所以 STALL_FRONTEND + STALL_BACKEND 超过 cycles 是完全正常的。上面的例子中,30% + 56% = 86%,没有超过 100%,说明重叠不多;但如果你看到两者之和达到 120%,说明存在大量前后端同时停顿的周期,需要进一步细化分析定位根因。
步骤 4:解读
| 分类 | 占比 | 解读 | 下一步细化事件 |
|---|---|---|---|
| 前端停顿 | 30% | I-cache miss、ITLB miss、指令解码瓶颈 | L1I_CACHE_REFILL(r0001)、L1I_TLB_REFILL(r0002) |
| 后端停顿 | 56% | D-cache miss、内存延迟、执行单元争用 | L1D_CACHE_REFILL(r0003)、L2D_CACHE_REFILL(r0017) |
| 有效工作 | ~14% | CPU 流水线效率极低 | --- |
结论 :后端停顿是主要瓶颈(56%),应重点分析内存访问模式。这一结论也呼应了开头那个故事------当你发现 IPC 异常低,STALL_FRONTEND/STALL_BACKEND 就是你的第一把手术刀。后续的 cache miss 细化分析(L1D/L2/L3),将在第 4 篇《从 L1 到 DRAM》中系统展开。
步骤 5:进一步细化后端停顿
bash
perf stat -e cycles,\
armv8_pmuv3/event=0x24,name=stall_backend/,\
armv8_pmuv3/event=0x03,name=l1d_refill/,\
armv8_pmuv3/event=0x17,name=l2d_refill/,\
armv8_pmuv3/event=0x10,name=br_mis_pred/ \
./my_program
[提示] 试一试 :用本项目
tests/下的test_cache_miss程序体验后端停顿。编译后运行perf stat -e cycles,instructions,r0023,r0024 ./test_cache_miss random,对比sequential和random模式下前后端停顿占比的变化------你会看到 random 模式下后端停顿占比显著上升。
3.10 诊断工作流:从 IPC 异常到定位根因
当你用 perf stat 发现 IPC 异常低时,按以下路径逐步缩小范围:
text
发现 IPC < 1
│
▼
采集 STALL_FRONTEND (r0023) + STALL_BACKEND (r0024)
│
├── 前端停顿占比高 (>30%)
│ │
│ ▼
│ 采集 L1I_CACHE_REFILL (r0001) + L1I_TLB_REFILL (r0002)
│ │
│ ├── I-cache miss 高 → 代码布局优化(-ffunction-sections, PGO)
│ └── ITLB miss 高 → 减少代码页数(合并热点函数、huge page for text(配置复杂,优先考虑 `-ffunction-sections` + link-time layout))
│
├── 后端停顿占比高 (>40%)
│ │
│ ▼
│ 采集 L1D_CACHE_REFILL (r0003) + L2D_CACHE_REFILL (r0017) + MEM_ACCESS (r0013)
│ │
│ ├── L1D miss 高但 L2 命中 → 优化数据局部性(struct layout、prefetch)
│ ├── L2 miss 高 → 内存访问模式问题,可能穿透到 DDR
│ │ → 进一步用第 4 篇《从 L1 到 DRAM》的 cache/TLB/NUMA 事件细化
│ └── 执行单元争用 → 检查是否有大量除法/浮点/特殊指令
│
├── 两者都高 (前端>30% 且 后端>40%)
│ └── 内存子系统全面过载,前后端同时受阻 → 优先解决后端(收益更大)
│
└── 两者都不高
└── 可能是指令依赖链、长延迟指令、或 SMT 资源竞争 → 第 3 篇微架构分析
这条链路的核心思想是:先定方向(前端 vs 后端),再钻细节(哪一级 cache、哪类 miss)。避免一上来就采集 20 个事件------既触发 multiplexing 又让自己淹没在数据里。
3.11 速查表
原始事件语法速查
| 平台 | 短格式 | 完整格式 | 说明 |
|---|---|---|---|
| ARM64 | rXXXX |
armv8_pmuv3/event=0xNN/ |
事件编号最多 16-bit |
| x86 | rUUEE |
cpu/event=0xEE,umask=0xUU/ |
复杂事件用完整格式 |
| 别名 | --- | 加 name=xxx |
只影响显示 |
Top-Down 快速判断
| 指标 | 计算公式 | 判断标准 |
|---|---|---|
| 前端停顿占比 | STALL_FRONTEND / cycles |
> 30% 需关注前端 |
| 后端停顿占比 | STALL_BACKEND / cycles |
> 40% 需关注后端 |
| IPC | instructions / cycles |
< 1 效率低,> 2 优秀 |
3.12 FAQ
Q1:multiplexing 会影响原始事件的准确性吗?
会。当同时监控的事件数超过硬件 PMU 计数器数量(通常 4-6 个通用计数器)时,内核会在时间片间轮转。perf 输出中出现 (83.33%) 这样的百分比就是 multiplexing 的标志------意味着该事件只有 83% 的时间在实际计数,最终值是估算的。关键指标建议分多次采集,每次不超过 4-6 个事件。
Q2:原始事件能用于 perf record 采样吗?
可以。语法完全一致:perf record -e r0024 ./my_program。采样模式下,每当该事件计数溢出时触发中断采样,可以用 perf report 看到哪些函数触发了最多的后端停顿。
Q3:如何确认某个事件编号在我的 CPU 上可用?
三步验证:(1) 查看 /sys/bus/event_source/devices/armv8_pmuv3/events/ 下是否有对应事件;(2) 用 perf stat -e rXXXX sleep 1 试跑,如果报 not supported 说明当前 CPU 不支持该编号;(3) 对照 CPU 的 TRM 确认事件定义和编号。
Q4:ARM64 和 x86 的事件编号能通用吗?
不能。每个架构、甚至同架构不同微架构的事件编号都不同。ARM 的 0x23(STALL_FRONTEND)在 x86 上毫无意义。跨平台分析时需要分别查阅对应平台的手册。
Q5:为什么 sysfs 里能看到事件名,但 perf list 里没有?
perf list 只显示内核 perf 子系统明确注册的事件别名。sysfs 中的事件来自 PMU 驱动,有时 PMU 驱动暴露了事件但 perf 用户态工具没有对应的别名映射。这正是原始事件编号存在的价值------它不依赖别名,直接用编号访问 PMU 硬件。
下一篇预告:同一段代码,给数组排个序就快了 3 倍------不是编译器优化,不是算法改进。CPU 到底在背后干了什么?第 3 篇《为什么排序后代码快 3 倍?分支预测和流水线的秘密》揭晓答案。