﻿# PMU（Performance Monitoring Unit）—— CPU 里的"黑匣子"

> perf 为什么能告诉你 cache-miss 了几次、分支预测失败了多少回？不是在软件里插桩——是 CPU 芯片里有一组**硬件计数器**在默默地数。这个硬件模块叫 **PMU（性能监控单元）**。本篇从硬件角度讲清 PMU 的架构、事件选择机制、溢出中断和精确采样，不涉及 perf 用户态工具的实现（那部分见 [perf-internals.md](/tools/code/perf-internals.md)）。

> 关联：[core-and-uncore.md](/concepts/cpu/core-and-uncore.md)（Core/Uncore 划分）、[cpu-architecture.md](/concepts/cpu/cpu-architecture.md)（CPU 架构全景）、[perf-internals.md](/tools/code/perf-internals.md)（perf 原理，PMU 为数据源）、[interrupts.md](/concepts/process/interrupts.md)（PMU 溢出走 NMI 通道）

---

## 零、一句话结论

PMU 是 CPU 芯片内置的一组**硬件性能计数器（MSR 寄存器）**，每发生一次微架构事件（指令退休、cache miss、分支预测失败等）就 +1。计数器溢出可触发 **PMI 中断（走 NMI）**，perf 利用这个中断抓取现场（IP、调用栈）实现**采样**。PMU 分 **per-core PMU**（每个核心独立）和 **uncore PMU**（共享基础设施），Intel 和 AMD 的实现有差异但原理相通。

---

## 一、PMU 是什么，解决什么问题

### 1.1 没有 PMU 的时代

在没有 PMU 的年代，要回答"这段代码的 cache miss 率是多少"只能靠：

- **模拟器 / 周期精确模型**：慢 100~10000 倍，无法用于生产环境
- **代码插桩**：改代码加计数器，引入额外开销，改变程序行为（Observer Effect）
- **定时采样 PC**：只能看到"在哪个函数"，不知道"为什么慢"

这些手段都无法在**真实硬件上、不影响程序行为的前提下、精确地回答**微架构级别的问题。

### 1.2 PMU 的核心价值

PMU 是 CPU 设计者在芯片里预留的一张"体检报告单"——它**不干扰程序执行**，只是旁路观测：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<core>> #E3F2FD
  BorderColor<<core>> #1976D2
  BackgroundColor<<pmu>> #FFF3E0
  BorderColor<<pmu>> #E65100
}
rectangle "CPU Core\n取指 → 译码 → 执行 → 退休" <<core>> as CORE
rectangle "PMU（性能监控单元）\n· 固定计数器 (3~4个)\n· 可编程计数器 (4~8个)\n· 事件选择逻辑\n· 溢出检测 → PMI" <<pmu>> as PMU
CORE -right-> PMU : 每发生事件就发信号\n(指令退休/cache miss/分支误预测/...)\nPMU 对应的计数器 +1
@enduml
```

PMU 回答的问题不是"CPU 在干什么"（那是 PC 采样），而是 **"CPU 内部发生了什么微架构事件" **：

| 问题 | 对应 PMU 事件 |
|------|-------------|
| IPC（每拍执行几条指令）是多少？ | `instructions` ÷ `cycles` |
| L1 缓存命中率？ | `L1-dcache-loads` / `L1-dcache-load-misses` |
| 分支预测准确率？ | `branches` / `branch-misses` |
| 前端还是后端瓶颈？ | `stalled-cycles-frontend` vs `stalled-cycles-backend` |
| TLB 抖动了吗？ | `dtlb_load_misses.miss_causes_a_walk` |

---

## 二、PMU 硬件架构

### 2.1 整体结构：计数器 + 事件选择 + 溢出逻辑

```plantuml
@startuml
skinparam shadowing false
rectangle "PMU 内部结构 (per-Core)" {
  rectangle "固定功能计数器\n(3~4个 × 48bit)" as FIXED {
    (FIXED_CTR0: instructions retired)
    (FIXED_CTR1: core cycles)
    (FIXED_CTR2: reference cycles)
  }
  rectangle "通用可编程计数器\n(4~8个 × 48bit)" as GP {
    (GPCTR0 ← EVTSEL0)
    (GPCTR1 ← EVTSEL1)
    (GPCTR2 ← EVTSEL2)
    (GPCTR3 ← EVTSEL3)
   ......
  }
  rectangle "事件选择寄存器\n(每计数器 1 个)" as EVTSEL {
    (EVTSEL0: event_code + umask + os+usr+int)
    (EVTSEL1)
    (EVTSEL2)
    (EVTSEL3)
  }
  rectangle "全局控制寄存器" as GBL {
    (IA32_PERF_GLOBAL_CTRL: 哪些计数器使能)
    (IA32_PERF_GLOBAL_STATUS: 哪些计数器溢出了)
    (IA32_PERF_GLOBAL_OVF_CTRL: 清除溢出状态)
  }
}
EVTSEL --> GP : 配置每个GPCTR\n数什么事件
GBL --> FIXED : 使能
GBL --> GP : 使能
@enduml
```

### 2.2 三类寄存器

| 类别 | MSR 地址范围 | 宽度 | 数量 | 作用 |
|------|------------|:---:|:---:|------|
| **固定计数器** | `0x309` (IA32_FIXED_CTR0) ~ `0x30B` | 48 bit | 3~4 个 | 硬件固定只计一种事件，不可编程 |
| **通用计数器** | `0xC1` (IA32_PERFCTR0) ~ | 48 bit | 4~8 个 | 可编程，通过 EVTSEL 配置数哪种事件 |
| **事件选择寄存器** | `0x186` (IA32_PERFEVTSEL0) ~ | 64 bit | 与 GP 等数 | 配置对应 GPCTR 的 event_code + umask + 权限过滤 |
| **全局控制** | `0x38F` (IA32_PERF_GLOBAL_CTRL) 等 | 64 bit | 3 个 | 全局使能/状态/溢出控制 |

### 2.3 固定计数器 vs 可编程计数器

| | 固定计数器 (Fixed Counter) | 可编程计数器 (GP Counter) |
|---|---|---|
| **数量**| 3~4 个 | 4~8 个 (Skylake 4 个, Icelake+ 8 个) |
| **事件**| 硬件写死，不可改 | 通过 EVTSEL 配置，几百种事件可选 |
| **占用 MSR**| 不占 EVTSEL | 每个 GP 配 1 个 EVTSEL |
| **典型事件**| `instructions retired`, `core cycles`, `reference cycles` | cache-misses, branch-misses, TLB misses, 各种微架构事件 |
| **perf 中的体现**| `perf stat` 默认输出大部分来自固定计数器 | `perf stat -e xxx` 或 `perf record -e xxx` 用可编程计数器 |

> **关键约束**：固定计数器不占可编程计数器的槽位。所以 `perf stat` 默认输出（cycles + instructions + ...）和自定义事件（`-e cache-misses`）可以**同时使用**而互不抢占。

### 2.4 计数器宽度与溢出

每个计数器 48 bit（Intel）/ 48 bit（AMD）：

```bash
48 bit 计数器最大值 = 2^48 - 1 ≈ 281 万亿 (2.81 × 10^14)
在 3GHz CPU 上计 cycles：281T / 3G ≈ 93700 秒 ≈ 26 小时才溢出
在 3GHz CPU 上计 instructions (IPC=2)：281T / 6G ≈ 13 小时
```

正常做 `perf stat` 几乎不会溢出。**溢出机制真正的用途不是"防止溢出"，而是充当采样触发器**——见 §三。

---

## 三、事件选择：怎么让计数器"数正确的东西"

### 3.1 EVTSEL 寄存器的位域

每个 GPCTR 配一个 64 位的 `IA32_PERFEVTSELx` 寄存器（x = 0,1,2,3...）：

```bash
IA32_PERFEVTSELx (0x186 + x, 64-bit)
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│  63  │ 58   │ 56   │  40  │  24  │  16  │   8  │   0  │
├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│ 保留  │ CMASK│  INV │ 保留  │ UMSK │ USR  │  OS  │ E.C. │
│      │(8bit)│ (1b) │      │(8bit)│ (1b) │ (1b) │(8bit)│
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
                                                    ↑
                                     Event Code: 具体数什么事件
                                     UMSK: 事件的子类别掩码
```

| 位域 | 位 | 含义 |
|------|:---:|------|
| **Event Code** | 7:0 | CPU 微架构事件编号（Intel 手册卷 3 第 19 章） |
| **UMASK** | 15:8 | 事件的子类别过滤（如 `MEM_LOAD_RETIRED` 的 UMSK=0x01 表示 L1 hit, 0x02 表示 L2 hit） |
| **USR** | 16 | 计数用户态 (Ring 3) 事件 |
| **OS** | 17 | 计数内核态 (Ring 0) 事件 |
| **E (Edge)** | 18 | 边沿检测模式：计数器从 0 变 N 时记录 |
| **PC (Pin Control)** | 19 | 开启 PEBS（精确事件采样），见 §四 |
| **INT (APIC Interrupt)** | 20 | 溢出时产生 PMI 中断（perf 采样的核心） |
| **INV** | 23 | 反转 CMASK 比较逻辑 |
| **CMASK** | 31:24 | 计数器阈值：每拍事件数 ≥ CMASK 才 +1 |

### 3.2 Event Code + UMASK 的含义

perf 里看到的通用事件名（`cache-misses`）是内核到硬件事件的**映射别名**。实际硬件层面是 `(event_code, umask)` 二元组：

```bash
perf 事件名          →  硬件 (event_code, umask)
─────────────────────────────────────────────────
cycles              =  固定计数器 1（不可编程）
instructions        =  固定计数器 0（不可编程）
cache-misses        =  (0x2E, 0x41) — LONGEST_LAT_CACHE.MISS
cache-references    =  (0x2E, 0x4F) — LONGEST_LAT_CACHE.REFERENCE
branch-misses       =  (0xC5, 0x00) — BR_MISP_RETIRED.ALL_BRANCHES
cpu-cycles          =  (0x3C, 0x00) — CPU_CLK_UNHALTED.THREAD
L1-dcache-load-misses = (0xD1, 0x01) — MEM_LOAD_RETIRED.L1_MISS
dTLB-load-misses    =  (0xD0, 0x01) — MEM_INST_RETIRED.STLB_MISS_LOADS
```

> 用 `perf list --details` 或 `libpfm4` 的 `showevtinfo` 可以看到当前 CPU 支持的完整 (event, umask) 映射表。

### 3.3 CMASK 和 INV：阈值计数

CMASK 不是"每发生事件就 +1"，而是"每拍事件数 ≥ CMASK 阈值才 +1"：

```bash
CMASK=0：每发生一次事件 +1（最常用）
CMASK=1：每拍至少发生 1 次 +1（等于事件发生的拍数）
CMASK=2：每拍至少发生 2 次 +1（找"事件密度高"的拍）
INV=1 时反转：每拍事件数 < CMASK 才 +1
→ 可以计数"流水线空闲的拍数"
```

---

## 四、Per-Core PMU vs Uncore PMU

### 4.1 两类 PMU 的分工

CPU 芯片里不是只有一个 PMU——每个 Core 有自己的 **per-core PMU**，共享的 Uncore 区域有独立的 **uncore PMU**：

```plantuml
@startuml
skinparam shadowing false
rectangle "Socket" {
  rectangle "Core 0" as C0 {
    rectangle "Per-Core PMU" as PMU0
  }
  rectangle "Core 1" as C1 {
    rectangle "Per-Core PMU" as PMU1
  }
  rectangle "..." as CD
  rectangle "Uncore 共享区域" as UNC {
    rectangle "L3 Cache" as L3
    rectangle "IMC (内存控制器)" as IMC
    rectangle "UPI (跨CPU总线)" as UPI
    rectangle "PCIe Root" as PCIe
    rectangle "Uncore PMU\n(多个独立的计数器组)" as UPMU
  }
  C0 -down- UNC
  C1 -down- UNC
  CD -down- UNC
  L3 -[hidden]- UPMU
  IMC -[hidden]- UPMU
  UPI -[hidden]- UPMU
  PCIe -[hidden]- UPMU
}
@enduml
```

| | Per-Core PMU | Uncore PMU |
|---|---|---|
| **位置**| 每个物理核心里 | 片上共享区域（L3/IMC/UPI 旁边） |
| **数量**| 每组 3~4 固定 + 4~8 GP | 多组独立计数器，每组针对一种共享资源 |
| **观测对象**| **这个核**上的指令执行情况 | **整个芯片共享资源**的使用情况 |
| **典型事件**| cycles, instructions, cache-miss, branch-miss, TLB miss | L3 命中率、内存带宽、UPI 带宽、PCIe 带宽 |
| **perf 访问**| `perf stat -e cycles` (默认 per-core) | `perf stat -e uncore_imc/cas_count_read/` |
| **CPU 型号差异**| 较小，Intel 和 AMD 事件名基本标准化 | **极大**，每代 CPU uncore 事件完全不同 |

### 4.2 Uncore PMU 的典型用途

```bash
# 看内存读带宽（Intel IMC uncore 计数器）
perf stat -e uncore_imc/cas_count_read/ -a sleep 10
# 看跨 socket 通信量（Intel UPI uncore）
perf stat -e uncore_upi/tx_flits/ -a sleep 10
# 看 L3 miss 总量（Intel CBOX uncore）
perf stat -e uncore_cbox/event=0x0,umask=0x0/ -a sleep 10
```

> **注意**：Uncore PMU 事件是**系统级**的（不按进程隔离），因为 L3 和内存控制器被所有核心共享，无法区分"这次 L3 miss 是哪个进程导致的"。

---

## 五、计数器溢出 → PMI → perf 采样

### 5.1 为什么需要溢出机制

大多数时候 `perf stat` 只读计数器的最终值。但 `perf record` / `perf top` 需要**采样**——每隔 N 次事件抓一次现场（哪个 IP、什么调用栈）。

**方案 A（轮询）**：每隔 1ms 读一次计数器 → 开销大 + 时间不准  
**方案 B（溢出中断）**：编程计数器初值 = -N（补码），每 N 次事件溢出 → 硬件触发中断 → 抓现场

CPU 硬件选择了方案 B，这就是 **PMI（Performance Monitoring Interrupt）**。

### 5.2 溢出 → PMI → NMI 的完整路径

```bash
perf record -e cycles -F 99 ./a.out
        ↓
perf 设 GPCTR0 = -SAMPLE_PERIOD (用补码)
SAMPLE_PERIOD = cpu_freq / 99 ≈ 3GHz/99 ≈ 3030万 cycles
        ↓
CPU 每执行 ~3030万个 cycle  →  计数器溢出
        ↓
硬件: 置 IA32_PERF_GLOBAL_STATUS.OVF 位
硬件: 通过 LAPIC 发 PMI
        ↓
x86 上 PMI 走 NMI 通道 (不可屏蔽中断)
        ↓
NMI handler → perf_events NMI 回调:
  1. 读当前 IP (指令指针)
  2. 读调用栈 (frame pointer / dwarf / LBR)
  3. 写 record 到 mmap 环形缓冲区
  4. 重置计数器 = -SAMPLE_PERIOD
  5. 清除溢出状态位
        ↓
用户态 perf: poll() 感知有新数据 → 搬记录 → 写 perf.data
```

```plantuml
@startuml
title PMU 溢出 → PMI 采样 时序
participant "CPU Core" as CORE
participant "PMU 计数器" as PMU
participant "LAPIC" as LAPIC
participant "NMI Handler\n(perf_events)" as NMI
participant "mmap 缓冲区" as BUF
CORE -> PMU: 每 cycle 计数器 +1
PMU -> PMU: GPCTR 从 0xFFFF... 到 0x0000...
PMU -> PMU: 溢出! (硬件自动检测)
PMU -> LAPIC: 发 PMI 信号
LAPIC -> NMI: 触发 NMI 中断
NMI -> CORE: 暂存当前 IP+SP
NMI -> BUF: 写 record {IP, 调用栈, 时间戳}
NMI -> PMU: 重置计数器 = -SAMPLE_PERIOD
NMI -> PMU: 清 IA32_PERF_GLOBAL_STATUS
NMI -> CORE: iret 返回被采样代码
@enduml
```

### 5.3 为什么 PMI 走 NMI 通道

PMI 走 NMI（不可屏蔽中断）通道是精心设计的：

| 如果走普通 IRQ | 现实是走 NMI |
|---|---|
| 关中断的代码段（spin_lock_irqsave）里**无法采样** | NMI 可以打断任何代码，不受 CLI 影响 |
| 采样会丢内核锁持有时、中断处理时的热点 | 能看到"关中断期间在干什么" |
| 可能被其他中断延迟 | NMI 优先级最高，立即响应 |

> 详见 [interrupts.md](/concepts/process/interrupts.md) §四，`/proc/interrupts` 中的 NMI 行包含 PMI 计数。

### 5.4 Skid（采样偏移）

PMI 从"计数器溢出"到"NMI handler 读到 IP"之间有 **指令流水线深度 × N 条指令**的延迟。这意味着采到的 IP 通常**不是**真正导致溢出的那条指令——这就是 **skid**：

```bash
真正导致溢出的指令 (第 1000 条)
  → 流水线继续执行 ~5-20 条指令 (skid)
  → PMI 触发
  → NMI handler 读到 IP → 指向第 1005~1020 条
```

`perf report` 的 `--no-skid` 选项或 PEBS（见 §六）可以减轻这个问题。

### 5.5 采样开销：NMI 对 CPU 的性能影响

**会打断 CPU 运行吗？—— 会。** NMI 是最硬的中断，硬件自动保存当前指令流的状态（IP/SP/EFLAGS），跳转到 NMI handler，执行完毕 `iret` 返回。被采样代码在这段时间内**完全停止执行**。

**有性能问题吗？—— 有开销，但通常可忽略。** 开销取决于**采样频率**和**单次 NMI handler 耗时**：

```bash
采样开销(%) = (采样频率 Hz) × (单次 handler 耗时 s) × 100%
perf record -F 99:   99 × ~3μs  = ~0.03%   ← 几乎无感知
perf record -F 999:  999 × ~3μs = ~0.3%    ← 生产环境常用上限
perf record -F 4000: 4000 × ~5μs = ~2%     ← 最大频率，可测量但不大
```

**NMI handler 内部做了什么（耗时来源）**：

| 步骤 | 耗时 | 说明 |
|------|:---:|------|
| 硬件保存现场 + 切换栈 | < 0.5μs | 硬件自动 push SS/RSP/RFLAGS/CS/RIP |
| perf_events NMI callback | ~1-2μs | 读 IP、读 PMU 状态寄存器、判断事件来源 |
| 回退调用栈 (unwind) | ~1-5μs | FP unwind ≤ 1μs，DWARF 可达 ~5μs |
| 写 record 到 mmap 环形缓冲区 | < 0.2μs | 纯内存写入 |
| 重置计数器 + 清溢出位 | < 0.2μs | 几条 MSR 写指令 |
| iret 返回 | < 0.3μs | 硬件恢复现场 |
| **合计** | **~3~8μs** | FP unwind 场景约 3μs，DWARF 约 5~8μs |

> **为什么 `-F 4000` 下 NMI handler 更慢？** 因为采样频率越高，环形缓冲区写入越频繁，用户态 `perf` 进程被唤醒越勤，上下文切换 + cache 污染也会叠加。

**NMI 的隐蔽开销（比 handler 本身更难测量）**：

| 开销类型 | 影响 |
|----------|------|
| **Cache 污染** | NMI handler 代码 + 栈数据会抢占被采样程序的 L1-I / L1-D / L2，导致 `iret` 后出现短暂的 cache miss 潮 |
| **TLB 污染** | handler 的代码页和数据页竞争 dTLB/iTLB 条目，退出后原程序的部分 TLB 映射被挤出 |
| **分支预测器污染** | handler 的分支历史覆盖了原程序的分支预测状态 |
| **NMI 不可嵌套** | 一个 NMI 处理期间，后续 NMI 被硬件屏蔽，高频采样时样本可能被合并或丢弃 |

**实际建议**：

| 场景 | 推荐频率 | 开销量级 |
|------|:---:|:---:|
| 生产环境长期 profiling | `-F 49~99` | < 0.05%，放心用 |
| 常规性能分析 | `-F 99~999` | < 1% |
| 短时热点分析（低流量服务） | `-F 2000~4000` | 1~2%，可接受 |
| 长尾延迟敏感（HFT / 高频交易） | 避免 PMI，用 PEBS + 极低频率 | → §六 |

> **一句话**：`-F 99` 采样 ≈ 每 10ms 停 ~3μs，相当于每 10000μs 停 3μs = 0.03% 开销，比操作系统调度器本身的开销还小一个数量级。

---

## 六、精确采样：Intel PEBS vs AMD IBS

### 6.1 普通 PMI 采样的"不精确"问题

普通 PMI 采样（§五）的 IP 有 skid，这在统计上问题不大（热点函数仍然清晰），但有两个场景很致命：

1. **想知道"哪条 load 指令导致 cache miss"**——skid 偏移后 IP 可能指向下一条、甚至下下条指令
2. **想知道"数据地址是什么"**——PMI handler 拿不到已经消失的 load 地址

### 6.2 Intel PEBS（Precise Event-Based Sampling）

PEBS 的核心思路：**不让 PMI handler 临时去读，而是硬件在事件发生时就把全套信息（IP + 数据地址 + 延迟）自动写入一个内存缓冲区**。

```bash
普通 PMI:  溢出 → NMI → 软件读寄存器 (有 skid)
PEBS:      事件发生 → 硬件写 PEBS buffer (IP+数据地址+延迟)
            → buffer 满了 → PMI → handler 批量取走记录
```

```plantuml
@startuml
title PEBS 采样流程
participant "CPU Core" as CORE
participant "PEBS Buffer\n(内存, per-CPU)" as PEBS
participant "PMU" as PMU
participant "NMI Handler" as NMI
CORE -> PMU: load MISS 事件 (硬件检测)
PMU -> PEBS: 硬件自动写 PEBS record:\n{精确 IP, 数据地址, 延迟, TSC}
note right: 这条记录的 IP 是**精确的**\n就是 miss 的那条 load
PEBS -> PEBS: counter +1
alt PEBS buffer 未满
    CORE -> CORE: 继续执行
else PEBS buffer 满 (counter == threshold)
    PEBS -> PMU: 触发 PMI
    PMU -> NMI: NMI 中断
    NMI -> PEBS: 批量读取 N 条 PEBS records
    NMI -> PEBS: 写入 mmap 环形缓冲区
    NMI -> PEBS: 重置 PEBS index
end
@enduml
```

**PEBS 的关键优势**：

| | 普通 PMI | PEBS |
|---|---|---|
| IP 精度 | 有 skid (~5-20 条指令) | **精确**，就是触发事件的那条指令 |
| 数据地址 | 拿不到（load 已完成） | 硬件记录在 PEBS record 里 |
| 延迟信息 | 无 | 有（L1/L2/L3/memory 命中延迟） |
| 开销 | 低（每个样本一次 PMI） | 更低（N 个样本才一次 PMI） |
| 适用事件 | 所有事件 | 仅 Precision Events（`perf list` 中带 `[Precise]` 标记） |

perf 中开启 PEBS：在事件后加 `:pp`（precise）或 `:ppp`（more precise）：

```bash
perf record -e cycles:pp  ...    # PEBS 精度
perf record -e mem_load_retired.l3_miss:pp ...  # 精确定位哪条 load 导致 L3 miss
```

### 6.3 AMD IBS（Instruction-Based Sampling）

AMD 没有 PEBS，用的是 **IBS（Instruction-Based Sampling）**——机制不同但目标相同：精确采样。

```bash
IBS Fetch 采样:  随机选一条取指的指令 → 记录"取指阶段"的延迟
IBS Op 采样:     随机选一条 μOP → 记录"执行/访存阶段"的延迟 + 数据地址
```

| | Intel PEBS | AMD IBS |
|---|---|---|
| 触发方式 | 事件发生后硬件记录 | 随机选一条指令/μOP |
| 记录内容 | IP + 数据地址 + 延迟 | 分段：Fetch(取指) 和 Op(执行) 两类采样 |
| 统计特性 | 事件驱动（cache miss 多→样本多） | 均匀随机采样（每条指令被采概率相等） |
| 适用场景 | 分析特定微架构事件的热点 | 均匀覆盖所有指令，适合找"整体瓶颈" |

```bash
# AMD IBS 采样
perf record -e ibs_fetch/period=100000/ -e ibs_op/period=100000/ ./a.out
perf report
```

---

## 七、PMU 和虚拟化：vPMU

### 7.1 虚拟机里能用 PMU 吗？

云服务器和容器里跑 `perf stat`，能不能读到硬件 PMU？分三层：

| 环境 | PMU 可用性 | 说明 |
|------|:---:|------|
| **物理机 / 裸金属** | ✅ 完全可用 | 直接操作 MSR |
| **容器** | ✅ 取决于 `perf_event_paranoid` | 不改 `/proc/sys/kernel/perf_event_paranoid` 只能用用户态事件 |
| **KVM 虚拟机** | ⚠️ 需显式开启 | 通过 vPMU 虚拟化，性能有折损 |
| **公有云 VM** | ❌ 通常不可用 | 云厂商默认关闭 vPMU（安全+性能考量） |

### 7.2 vPMU 的原理

虚拟机里的 `perf` 无法直接操作物理 MSR——KVM 需要**拦截并模拟** MSR 读写：

```bash
Guest perf 设 EVTSEL0 → 写 MSR 0x186
        ↓
VM-Exit（KVM 拦截）
        ↓
KVM 判断：这个虚拟机开 vPMU 了吗？
  ├─ 开了 → 映射到物理 PMU 计数器（或软件模拟）
  └─ 没开 → 注入 #GP 异常 → perf_event_open 失败
```

> 关闭 vPMU 是云厂商的常见策略——减少 VM-Exit 开销 + 防止 PMU 侧信道攻击。

---

## 八、PMU 的硬件限制与坑

### 8.1 Counter Aliasing（计数器混叠）

在 §一~§四里看到的情况：`dTLB-load-misses` 计数器在特定 CPU 上值严重偏小（实际数百万次 miss 只计到 831）。

**原因**：某些 CPU 微码（microcode）对特定事件使用同一组内部信号线，当多个事件共享同一个硬件资源时发生 **counter aliasing**——计数器读数不准确。

**对策**：
- 不用有 aliasing 的事件，改用子事件（如用 `stlb_hit` + `miss_causes_a_walk` 代替 `dTLB-load-misses`）
- 用 `perf stat -e xxx:u` 加 `:u` 后缀切换到用户态专用计数器（有时避开 aliasing）

### 8.2 计数器数量不足

当同时监控的事件数超过可编程计数器数量时：

| CPU 代 | 可编程计数器 | 固定计数器 | 同时可用事件数 |
|--------|:---:|:---:|:---:|
| Skylake | 4 | 3 | 7 |
| Ice Lake | 8 | 4 | 12 |
| Sapphire Rapids | 8 | 4 | 12 |
| AMD Zen 3/4 | 6 | 0 | 6 |

perf 的 **multiplexing（时分复用）** 可以在事件数 > 计数器数时轮流计数，但每个事件的精度会下降：

```bash
perf stat -e cycles,instructions,cache-misses,cache-references,branches,branch-misses,L1-dcache-loads,L1-dcache-load-misses ./a.out
# 如果只有 4 个 GPCTR，perf 会自动 multiplexing
# 输出中会出现 "(scaled from xx%)" 表示精度损失
```

### 8.3 Hyper-Threading（SMT）下的 PMU

启用超线程时，两个逻辑核共享同一套物理 PMU 计数器。PMU 硬件可以配置为：

- **Per-thread 计数**：每个逻辑核独立计数（perf 默认行为）
- **Per-core 计数**：两个逻辑核合并计数

大多数情况下 per-thread 是正确选择。但在高负载 SMT 场景下，如果另一个逻辑核也在跑热点代码，**共享硬件资源（L1/L2/TLB）上的事件会被两个线程交叉干扰**，使单个线程的 PMU 数据难以归因。

---

## 九、常见 PMU 事件速查

### 9.1 核心计算类

| 事件 | 含义 | 诊断 |
|------|------|------|
| `instructions` | 退休指令数 | IPC = instructions / cycles |
| `cycles` | 非停机 cycle 数 | 越高越忙 |
| `ref-cycles` | 固定频率 cycle（不受 Turbo 影响） | 判断 CPU 是否 boost 了 |
| `stalled-cycles-frontend` | 前端停顿（等取指/译码） | 高 → I-cache miss / 分支预测差 |
| `stalled-cycles-backend` | 后端停顿（等内存/执行资源） | 高 → cache miss / 内存瓶颈 |

### 9.2 缓存类

| 事件 | 含义 |
|------|------|
| `cache-references` | 最后一级缓存（LLC）访问次数 |
| `cache-misses` | 最后一级缓存未命中次数 |
| `L1-dcache-loads` / `L1-dcache-load-misses` | L1 数据缓存 Load 及 Miss |
| `L1-icache-load-misses` | L1 指令缓存未命中 |
| `dTLB-load-misses` | 数据 TLB miss |
| `l2_rqsts.miss` | L2 缓存 miss |

### 9.3 分支与乱序

| 事件 | 含义 |
|------|------|
| `branches` | 分支指令总数 |
| `branch-misses` | 分支预测失败数 |
| `branch-load-misses` | 间接分支（函数指针/virtual call）预测失败 |
| `uops_retired` | 退休微操作数 |
| `uops_executed` | 执行微操作数（含推测执行） |

### 9.4 Uncore 类（型号依赖）

| 事件 | 含义 |
|------|------|
| `uncore_imc/cas_count_read/` | 内存读次数 |
| `uncore_imc/cas_count_write/` | 内存写次数 |
| `uncore_cbox/event=0x1,umask=0x1/` | L3 命中的 cache line 数 |
| `uncore_upi/tx_flits/` | UPI 发送 flit 数 |

---

## 十、PMU 与 Linux 软件栈的关系

PMU 只是硬件。用户通过 Linux 的 `perf_events` 子系统访问它：

```bash
                          用户空间
┌──────────────────────────────────────────────┐
│  perf 工具     libperf / PAPI    自定义工具   │
│  (stat/record)   (编程接口)    (perf_event_open) │
└──────────────────┬───────────────────────────┘
                   │ perf_event_open(2)
                   │ read() / mmap()
┌──────────────────▼───────────────────────────┐
│         Linux 内核 perf_events 子系统          │
│  · 计数器分组 & 调度 (multiplexing)            │
│  · PMI/NMI handler → 写环形缓冲区              │
│  · 权限控制 (perf_event_paranoid)              │
│  · vPMU 虚拟化支持                             │
└──────────────────┬───────────────────────────┘
                   │ MSR 读写 (rdmsr/wrmsr)
                   │ PMI → 通过 LAPIC → NMI
┌──────────────────▼───────────────────────────┐
│            CPU 硬件 PMU + Uncore PMU           │
│  · 固定计数器 (FIXED_CTR0~3)                   │
│  · 可编程计数器 (GPCTR0~7) + EVTSEL0~7         │
│  · PEBS buffer (Intel) / IBS (AMD)            │
└──────────────────────────────────────────────┘
```

> 软件侧详细机制见 [perf-internals.md](/tools/code/perf-internals.md)。

---

## 十一、一句话总结

PMU 是 CPU 芯片内部的一组硬件性能计数器（固定 + 可编程 × N），通过 Event Code + UMASK 选择要数的事件，计数器溢出触发 NMI 中断实现采样（perf record），Intel 的 PEBS 和 AMD 的 IBS 提供精确 IP 和访存延迟。perf 通过 `perf_event_open(2)` 操作 MSR 来配置 PMU，是硬件观测能力在用户态的唯一标准化入口。

