6性能之巅:系统、企业与云可观测性(第2版)

CPU

架构、调度器与性能分析

IPCCPU缓存调度器perfCPU火焰图

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 CPU

架构、调度器与性能分析

章节导读

CPU 是计算机的引擎,也是性能分析中"最熟悉又最陌生"的组件。说熟悉,因为 top 里的 CPU 使用率人人会看;说陌生,因为绝大多数工程师无法回答:为什么我的程序 IPC 只有 0.4?缓存未命中到底浪费了多少时间?CFS 调度器如何决定下一个运行谁?%system 高到底意味着什么?本章从微架构到调度器完整拆解 CPU 性能,目标是让你看到任何 CPU 指标都能说出背后的物理机制。

先建立微架构模型。现代 CPU 是深度流水线(15-20 级)的超标量处理器:每个时钟周期同时发射多条指令(4-6 宽),乱序执行(out-of-order)引擎重排指令以掩盖延迟,分支预测器(准确率 95-99%)投机执行路径,多级缓存(L1 约 1ns/4-5 周期、L2 约 4ns、L3 约 15-40ns、主存约 80-100ns)构成存储层次。这套机器的效率用 IPC(每周期指令数)衡量:理论峰值 4-6,科学计算等缓存友好的负载可达 2-3,而典型的商业应用(指针追逐、分支密集、大工作集)往往只有 0.5-1.5。IPC 低意味着 CPU 在"空转等待"——等内存(缓存未命中,每次 L3 未命中到主存浪费约 200 周期,相当于执行 200 条指令的时间)、等分支预测失败的流水线冲刷(约 15-20 周期)、等锁与依赖链。这就是"CPU 使用率 100% 但性能很差"的微观解释:使用率只说明核在忙,IPC 才说明忙得有效率。

CPU 性能计数器(PMC)是微架构的观测窗口。每个核有若干可编程计数器(Intel 通常 4-8 个通用 PMC),可配置为计数任意硬件事件:cycles、instructions、cache-references、cache-misses、branch-misses、TLB 未命中、流水线停顿原因等。perf stat 是读取接口。计数器多路复用:事件数超过物理计数器时,perf 分时轮转并外推(输出中的百分比表示实际测量时间占比,误差随之增大)。云虚拟机中 PMU 可能未暴露(%d 显示 not supported),近年主流云已支持(AWS 的 PMU 虚拟化、GCP 默认开启)。方法论上,PMC 分析的标准流程是 USE 的 CPU 细化:先看 IPC 定性(计算密集 vs 内存密集),再用 Top-Down 方法(Intel 的 TMA,Top-down Microarchitecture Analysis)分解流水线槽位:retiring(有效工作)、bad speculation(预测失败浪费)、frontend bound(取指/解码瓶颈)、backend bound(执行资源或内存瓶颈),逐层钻取到具体原因。

缓存是应用性能的第一硬件因素。缓存以 64 字节缓存行为单位,局部性(时间局部性:刚访问的会再访问;空间局部性:邻居会被访问)决定命中率。性能杀手有三个:(1)大工作集随机访问——超过 L3 容量(通常 32-64MB)后每次访问都是主存延迟;(2)伪共享(false sharing)——两个核修改同一缓存行的不同变量,缓存一致性协议(MESI)导致该行在两核间反复失效-传输(ping-pong),性能可劣化 10 倍,典型场景是数组元素按线程分片但元素小于 64 字节、或结构体中热点字段与冷字段同行,解法是填充对齐(alignas(64)、@Contended);(3)缓存不友好的数据结构——链表与树的指针追逐使预取器失效,同样数据量下数组遍历可比链表快 10-100 倍。观测:perf stat -e cache-misses,cache-references 得命中率,perf c2c 专门诊断伪共享(基于 PEBS 采样记录缓存行的争用地址),perf mem 分析访存延迟分布。

CPU 调度器决定"谁在什么时候跑在哪个核上"。CFS(完全公平调度器)用红黑树按 vruntime 排序,总是选最"亏欠"的任务,nice 值(-20 到 19)映射为权重比例分配 CPU。关键性能参数:sched_latency_ns(目标调度延迟,所有 runnable 任务轮转一次的周期,默认 6-24ms 视核数)与 sched_min_granularity_ns(最小时间片,防止切换过频,默认 0.75-3ms)。当 runnable 任务数很多时,实际时间片 = max(min_granularity, sched_latency / nr_running)——线程超卖的应用会感受到毫秒级的调度等待。调度延迟(runqueue latency:任务从被唤醒到真正上 CPU 的时间)是延迟敏感服务的隐形杀手,观测用 BCC runqlat(直方图)。6.6 内核起 EEVDF(Earliest Eligible Virtual Deadline First)取代 CFS,引入虚拟截止时间与滞后(lag)概念,对交互式任务响应更好,sched_latency 等参数消失。实时调度类(SCHED_FIFO/RR)优先级绝对高于普通任务,用于硬实时场景,误用会饿死系统(RT throttling 默认保留 5% CPU 给普通任务兜底)。

多核拓扑与亲和性。现代服务器 CPU 是多插槽 × 多核 × SMT(超线程,每核 2 个逻辑 CPU 共享执行单元)的层次结构,跨核共享 L3(同 socket),跨 socket 走 UPI 总线(延迟约 2 倍于本地内存,带宽受限)。性能实践:延迟敏感服务绑核(taskset/cgroup cpuset)避免迁移与跨 NUMA 访问;SMT 的取舍——吞吐量负载开启 SMT 通常提升 20-30%(两个逻辑核共享缓存与执行单元),延迟敏感或安全敏感(侧信道)负载可能关闭;中断亲和与 RPS/RFS 把网络处理分散到多核(第 9 章)。观测拓扑:lscpu、lstopo(hwloc 套件)、/sys/devices/system/cpu/cpu*/topology/。

CPU 性能分析的工具矩阵按 USE 组织。利用率:mpstat(每核 %usr/%sys/%soft/%steal/%idle)、pidstat(每进程)、top/htop。饱和度:vmstat r 列、runqlen/runqlat(eBPF)、PSI cpu、调度延迟。错误:perf -e machine-checks、dmesg 的 MCE 记录、温度降频(turbostat 显示实际频率——过热降频与功耗墙是"CPU 使用率正常但变慢"的隐蔽原因)。剖析:perf record + 火焰图(热点函数)、perf stat + PMC(微架构效率)、perf annotate(指令级热点)。这一矩阵的每个格子本章都会给出实战命令。

最后强调 CPU 分析的两个现代议题。其一是虚拟化下的 CPU:steal time(Hypervisor 分给别的租户的时间)、vCPU 调度延迟(宿主机的调度器决定你的 vCPU 何时上物理核)、CPU 配额限流(cgroup cfs_quota 造成的周期性 throttle,第 10 章详述)——云环境的"CPU 问题"一半在实例内看不到。其二是频率的复杂性:turbo boost(单核高频多核降频)、功耗墙(RAPL 限制)、调频调度器(intel_pstate 的 powersave 默认策略其实含 turbo 逻辑)——比较性能必须确认频率状态(turbostat、/proc/cpuinfo 的 MHz 实时值),"同样的代码慢 20%"可能只是频率差异。带着这些认知进入实验:你将亲手测量 IPC、缓存未命中的代价、伪共享的破坏力与调度延迟的分布。

核心概念

  • IPC
  • CPU缓存
  • 调度器
  • perf
  • CPU火焰图

要点精析

IPC 与 Top-Down 分析方法。IPC = instructions / cycles,perf stat 直接输出。判读基准:>2.0 计算密集且缓存友好(视频编码、压缩、科学计算);1.0-2.0 健康的商业负载;0.5-1.0 内存受限或分支密集(典型 Java/Go 服务);<0.5 严重的缓存未命中、锁争用或深度依赖链。IPC 低时的分解路径(Intel TMA 四层模型):Level 1 把每个流水线槽位归类为 Retiring(有效)、Bad Speculation(分支预测失败/机器清空)、Frontend Bound(取指解码供不上:icache 未命中、代码体积过大)、Backend Bound(后端阻塞:Core Bound 执行端口争用/依赖链,Memory Bound 访存等待);Level 2/3 继续细分(Memory Bound 再分 L1/L2/L3/DRAM Bound)。perf 支持 --topdown 直接输出 L1 分解(perf stat --topdown -a sleep 5),pmu-tools 的 toplev 脚本做多层分解。实战意义:DRAM Bound 占比高 → 优化数据布局与局部性(第 7 章);Bad Speculation 高 → 减少不可预测分支(查表替代分支、__builtin_expect);Frontend Bound 高 → 减小热路径代码体积(内联控制、LTO、BOLT 重排)。

缓存层次与访问成本的量化。典型服务器 CPU:L1d 32-48KB/核(4-5 周期,约 1ns)、L2 1-2MB/核(12-15 周期,约 4ns)、L3 32-64MB 共享(40-60 周期,约 15-25ns)、DRAM 80ns(200+ 周期)、跨 NUMA 远端内存 130-200ns、NVMe SSD 10-100μs(比 DRAM 慢千倍)。缓存行 64 字节,硬件预取器检测顺序/跨步访问模式提前加载——顺序遍历数组时预取器可把有效延迟压到接近 L2,随机访问则每次都是全延迟。一个直观计算:遍历 1GB 内存的链表(每节点 64 字节、随机分布),1600 万次访问 × 80ns ≈ 1.3 秒;同样数据的数组顺序遍历约 0.05 秒(带宽 20GB/s)——26 倍差距全部来自缓存与预取。观测工具:perf stat -e cache-references,cache-misses(LLC 命中率)、-e L1-dcache-load-misses、perf mem record 采样访存延迟分布、perf c2c 找伪共享。应用启示:数据结构选择(数组 > 链表)、热冷字段分离(结构体布局)、分块处理(tiling,让工作集适配 L2/L3)、大页减少 TLB 压力(第 7 章)。

伪共享(false sharing)的机制与诊断。缓存一致性协议 MESI 以缓存行为粒度:一个核写入某行前必须使其他核的该行副本失效(Invalidate)。若两个核频繁写同一行的不同变量(如线程各自的计数器在数组中相邻),该行在两核的缓存间反复弹跳(ping-pong),每次写入都触发跨核一致性流量(约 40-100ns,且污染 L3),性能可劣化 5-20 倍——而代码逻辑上两个线程毫无共享!高发场景:per-thread 统计数组、对象池的空闲标记、结构体中的热点写字段相邻。诊断:perf c2c record -a sleep 30 && perf c2c report——它用 PEBS 采样加载/存储的线性地址,按缓存行聚合"争用行",输出 HITM(Hit Modified,读到其他核刚修改的行)排行与行内偏移,直接定位到变量;bpftrace 也可采样但 c2c 是专用工具。修复:填充到缓存行边界——C++ alignas(64)、Java @jdk.internal.vm.annotation.Contended(需 -XX:-RestrictContended)、Go 结构体插入 [64]byte 填充、Python 无解(GIL 之下无此问题)。验证:修复后 perf c2c 的 HITM 计数应降 90%+,吞吐相应提升。

CFS/EEVDF 调度器行为与调度延迟分析。CFS 核心量:vruntime(按权重折算的虚拟运行时间),调度决策 = 选 vruntime 最小的任务,公平性由红黑树 O(log n) 维护。时间片计算:nr_running 个任务分享 sched_latency_ns(默认 6ms × log2 核数修正),每任务片长 = max(latency/nr, min_granularity)。含义:4 核机器跑 40 个 CPU 饥饿线程,每个线程每 24ms 才轮到 2.4ms——应用感受到的"卡顿"其实是调度周期。调度延迟(wakeup 到 on-CPU)的健康值:空载 <10μs,中等负载 <100μs,出现毫秒级说明 CPU 饱和或优先级配置问题。观测:runqlat(eBPF 直方图,按进程可细分)、runqlen(队列长度时序)、perf sched record/timehist(每次调度的延迟明细,含唤醒者)。EEVDF(6.6+)用虚拟截止时间替代纯 vruntime:任务积累 lag(应得而未得的 CPU),lag 为正者获得更早的截止时间,交互式任务(短突发)天然受益。调优入口:nice/cgroup weight 调权重、chrt 调实时类(慎用)、cpuset 隔离、sched_migration_cost_ns 影响迁移决策。

%usr/%sys/%soft/%steal/%iowait 的精确语义与诊断指向。%usr:用户态代码执行——高是常态,配合火焰图找热点。%sys:内核态执行(系统调用、中断处理的下半部之外部分)——异常高(>20-30%)指向:系统调用风暴(syscount 查)、内核锁争用(perf 看 native_queued_spin_lock_slowpath)、频繁缺页(sar -B 查)、大量小 I/O。%soft:软中断——高指向网络包处理(NET_RX,查 pps 与中断分布)或块 I/O 完成(BLOCK)。%steal:Hypervisor 未分给本 vCPU 的时间——非零即宿主机超售/争抢,云环境性能问题的第一嫌疑(第 10 章)。%iowait:CPU 空闲且有未完成 I/O——注意它是"空闲的变体"而非"I/O 消耗",多核下语义模糊,高 iowait + 低利用率 = 任务在等 I/O(去查存储),高 iowait + 高利用率 = 正常繁忙。%guest:跑虚拟机的时间(宿主机视角)。诊断口诀:usr 高查应用热点,sys 高查系统调用与内核路径,soft 高查网络,steal 高找云厂商,iowait 高查存储。

上下文切换与 CPU 迁移的缓存代价。进程切换的隐性成本是缓存与 TLB 状态丢失:新任务的代码与数据不在 L1/L2,TLB 无其页表项,切换后经历"预热期"(数十 μs 内 IPC 显著低)。CPU 迁移(任务从核 A 移到核 B)更糟:L1/L2 是私有的,迁移后完全冷启动,且 L3 若不同(跨 socket)也丢失。调度器的迁移决策权衡负载均衡与缓存亲和(wake_affine 逻辑倾向把任务放在唤醒者附近)。观测:vmstat 的 cs 列(总切换)、pidstat -w(按进程分自愿/非自愿)、perf stat -e cpu-migrations(迁移次数)、perf sched timehist 的 migrations。优化:延迟敏感服务用 cpuset/taskset 绑核(消除迁移)、线程数 ≈ 核数(减少切换)、SO_REUSEPORT 多进程各绑一核(网络服务的标准架构)。数据参考:切换率每秒 10 万 + 每次隐性成本 20μs = 2 个核的等效开销——切换风暴本身就是性能问题。

CPU 频率与功耗管理的性能影响。现代 CPU 的频率是动态的:空闲降频省电(C-states 深度睡眠,唤醒有延迟——C6 退出约 50-100μs,对超低延迟场景是问题,可用 cpuidle.off 或 tuning profile 限制)、负载升频(turbo boost:单核可达基频 1.5-2 倍,全核负载时回落到基频甚至以下)、功耗墙(RAPL 限制封装功耗,密集 AVX-512 负载触发降频)、温度墙(散热不足降频)。后果:同一程序的性能随"机器上还有什么负载""散热状态""BIOS 设置"波动 20-50%——基准测试必须监控频率(turbostat 显示每核实际频率与功耗)。调频驱动:intel_pstate 默认 powersave 模式(名字误导,实际含 turbo 逻辑,active 模式响应更快)、schedutil(随负载调频)。延迟敏感场景的实践:BIOS 关 C-states 与 turbo 波动(固定频率)、cpupower frequency-set -g performance、隔离核(isolcpus)跑关键线程。观测"频率导致的慢":turbostat 的 Bzy_MHz 列 + IPC 正常但吞吐低 = 频率问题。

SMT(超线程)的性能经济学。SMT 让一个物理核呈现两个逻辑 CPU,共享执行单元、缓存与 TLB:当两个逻辑核的任务都不能单独填满流水线(多数商业负载 IPC <2),SMT 提升总吞吐 15-30%;但当任务已高效利用执行单元,第二个逻辑核只能分到剩余资源,单任务延迟变长。决策矩阵:吞吐量优先的批处理/云服务 → 开启 SMT(同样的机器多干活);延迟优先(交易、实时)→ 考虑关闭或绑核时避免两个关键任务共享物理核(lscpu -e 的 CORE 列识别兄弟逻辑核);安全敏感 → 关闭(L1TF/MDS 等侧信道跨逻辑核泄漏);licensing 按核计费的商业软件 → 关闭 SMT 可能省钱(物理核数减半)。观测 SMT 收益:同负载下开/关 SMT 对比吞吐与 P99。云注意:多数云厂商的 vCPU = 逻辑 CPU(半个物理核),"2 vCPU"的并行能力弱于 2 物理核,基准对比时要对齐口径。

perf 的 PMC 实战命令集与多路复用陷阱。基础:perf stat -a sleep 10(全系统 cycles/instructions/IPC/切换/缺页);细化:perf stat -e cache-references,cache-misses,branch-instructions,branch-misses,dTLB-load-misses -p PID sleep 10;记录模式:perf record -e cycles -c 1000000 -g -p PID(按周期数采样,热点更准)与 perf record -e cache-misses -c 10000(按缓存未命中采样,直接定位"谁在打爆缓存"——火焰图的计数语义变为未命中数)。多路复用:一次指定超过 PMC 数量的事件时 perf 轮转测量并缩放外推,输出末尾的百分比(如 62.5%)是实际测量占比——低于 50% 时误差显著,应分批测量或用 perf stat -M 的 metricgroup。虚拟机限制:PMU 未虚拟化时硬件事件不可用(显示 not counted),退化方案是软件事件(cpu-clock)+ 采样剖析。内核权限:perf_event_paranoid 控制级别(2=仅用户态,1=含内核,0=含 CPU 级事件,-1=全开)。

CPU 火焰图的深度判读技巧。基础读法之外(宽度=占比、找平顶),进阶技巧:(1)看栈底——从 entry_SYSCALL_64 或中断入口开始的宽塔是内核路径开销(系统调用、软中断、定时器),应用直接调用栈之外的这些"税"常被忽视;(2)搜索特定帧——SVG 中 Ctrl+F 搜 memcpy(拷贝开销)、futex(锁)、GC/JVM 符号(运行时税)、schedule(调度)量化各类开销占比;(3)对比内核/用户占比——火焰图颜色无意义但帧名有:内核符号([k] 或特定前缀)占比 >30% 通常异常,指向系统调用风暴或中断压力;(4)差分火焰图验证优化——修复前后各采 30 秒,difffolded 渲染,目标函数的红色收缩即量化收益;(5)off-CPU 对照——CPU 火焰图只解释 on-CPU 时间,若 CPU 利用率低,宽延迟在 off-CPU 图里(第 4 章)。常见意外发现:序列化/反序列化(JSON 编解码常占 10-30%)、日志(同步日志的格式化+写盘)、正则回溯、内存分配(malloc/free 及其锁)、内核的 copy_user(网络/文件拷贝)。

CPU 饱和的完整诊断流程(实战剧本)。症状:延迟升高。第一步 mpstat -P ALL 1:全核 %usr 高 → 计算饱和,走剖析路径;单核 100% 其余空闲 → 单线程瓶颈或中断集中(查该核上的进程 pidstat 与 /proc/interrupts);%sys 高 → 系统调用/内核路径(syscount + perf 火焰图看内核栈);%soft 高 → 网络包处理(sar -n DEV 的 rxpck/s + 中断分布);%steal 高 → 虚拟化争抢(第 10 章);利用率都不高但延迟高 → 非 CPU 饱和,查调度延迟(runqlat,可能 cgroup 限流)或 off-CPU 等待。第二步(计算饱和):perf record 火焰图定位热点函数 → 判断热点性质:业务逻辑(算法优化)、库函数(换实现/升级版本)、运行时(GC/JIT,第 5 章)、内核(系统调用模式优化)。第三步验证:优化后同负载重测 QPS/P99/IPC/火焰图差分。整个流程的原则:每一步用数据排除一半假设空间,10 分钟内到达"热点函数"级别。

CPU 相关的内核参数与隔离技术速查。调度:kernel.sched_min_granularity_ns / sched_latency_ns(CFS 时代)、kernel.sched_autogroup(桌面特性,服务器上按会话分组可能干扰 cgroup 语义,通常关闭)、kernel.sched_migration_cost_ns(迁移惰性阈值)。隔离:isolcpus 启动参数(把核从调度域摘出,配合 taskset 专用)、nohz_full(隔离核上关闭周期 tick,减少干扰,需 rcu_nocbs 配合)、cgroup cpuset(容器化隔离的标准手段)、IRQ 亲和把中断赶离隔离核。实时:kernel.sched_rt_runtime_us(RT throttling,默认 950000/1000000 即 RT 最多占 95%)、chrt 设置 SCHED_FIFO。频率:cpupower frequency-set -g performance、intel_pstate=no_active 启动参数。这些参数的共同主题是为延迟敏感任务创造"确定性环境":独占核、无 tick 中断、无频率波动、无抢占。极端场景(高频交易、DPDK)用 isolcpus + nohz_full + 绑核 + 关 irqbalance 的完整组合,把调度延迟从毫秒级压到微秒级。

代码示例

perf stat -a sleep 10
perf stat -e cycles,instructions,cache-references,cache-misses,branch-instructions,branch-misses -p $(pgrep -f myapp) sleep 10
perf stat --topdown -a sleep 5
/* L1 Top-Down 分解:retiring/bad speculation/frontend/backend */
perf stat -M MemoryBandwidth -a sleep 10
/* 内存带宽 metric(视 CPU 支持) */
PMC 分析的标准序列。第一条给出全系统基线:重点读 insn per cycle(IPC)与 context-switches、cpu-migrations、page-faults 的每秒速率。第二条对目标进程测微架构效率:cache-misses/cache-references 得 LLC 未命中率(>5% 值得优化数据布局,>20% 说明工作集远超 L3 或随机访问严重);branch-misses/branch-instructions 得预测失败率(>2% 说明有不可预测分支——数据相关的 if 是重灾区,可改无分支代码)。第三条 --topdown 输出四大类槽位占比(需要较新内核与 CPU):Backend Bound 中 Memory Bound 高 → 访存优化;Bad Speculation 高 → 分支优化;Retiring 高而性能仍不足 → 算法层面减少总工作量。第四条用 metricgroup 测内存带宽(uncore 事件,接近带宽上限时加核也不提速——内存墙)。使用注意:事件数超过 PMC 数量时看输出的复用百分比,低于 50% 分批测;虚拟机中硬件事件可能 not counted,先 perf list 确认 pmu 可用性;测量窗口内负载要稳定(压测中测,不要空闲测)。这些数字是"优化方向选择器":先定性(计算/访存/分支受限),再动手改代码,避免盲目优化。
mpstat -P ALL 1
pidstat -u 1
top -H -p $(pgrep -f myapp)
/* -H 显示线程级 CPU */
taskset -cp $(pgrep -f myapp)
/* 查看进程的 CPU 亲和掩码 */
taskset -cp 2,3 $(pgrep -f myapp)
/* 绑定到 CPU2、3 */
chrt -f -p 50 $(pgrep -f myapp)
/* 设置 SCHED_FIFO 实时优先级 50(慎用) */
CPU 利用率的分层定位工具链:系统级(mpstat -P ALL 每核分解,发现单核热点与 %soft/%steal 异常)→ 进程级(pidstat -u 或 top 按 P 排序)→ 线程级(top -H 或 ps -T -p PID 看每线程 %CPU,Java 场景把线程 ID 转十六进制后 jstack 对照定位代码)。taskset 系列管理亲和性:-cp 查看/设置 CPU 掩码,绑核的收益是消除迁移(缓存局部性)与隔离干扰(配合 isolcpus),风险是绑错核数导致自饱和(绑 2 核跑 8 线程计算负载)——绑定后必须重测利用率与延迟。chrt 设置实时调度类:SCHED_FIFO 优先级 1-99,同优先级不轮转(跑满为止),SCHED_RR 有时间片轮转;实时任务不被普通任务抢占,适合硬延迟场景(音频、控制、DPDK 轮询线程),但 bug(死循环)会独占核——RT throttling(默认 95%)是最后防线,生产设置前先确认 kernel.sched_rt_runtime_us。方法论提示:绑核与实时类是"环境确定性"手段,先用 runqlat 确认调度延迟真是瓶颈(毫秒级长尾)再动用,否则是无效复杂度。
perf record -F 99 -p $(pgrep -f myapp) -g -- sleep 30
perf report --sort=dso,symbol
perf annotate --stdio hot_function
/* 指令级热点:哪条汇编指令采样最多 */
perf record -e cache-misses -c 10000 -p $(pgrep -f myapp) -g -- sleep 30
perf report
/* 此火焰图/报告的宽度语义 = 缓存未命中数 */
从函数级到指令级的热点钻取。perf record -F 99 采样后,perf report --sort=dso,symbol 先按动态库聚合(判断开销在应用、libc、JVM 还是内核),再按符号细看。perf annotate 是最后一公里:它把采样点映射回汇编指令(有 -g 调试信息时并排显示源码行),输出每条指令的采样占比——经典发现:热点循环中某条 load 指令占 60%(缓存未命中的停顿被记在消费该值的指令上,即"skid"现象,精确归因需 PEBS:perf record -e cycles:pp);或某条 call 到 memcpy 占比高(结构体拷贝过大)。第二个 perf record 变体展示"事件驱动采样"的威力:-e cache-misses -c 10000 表示每 1 万次缓存未命中采一个栈,生成的火焰图宽度语义是"未命中分布"——直接看到哪个数据结构、哪行代码在打爆缓存,比 cycles 采样更聚焦访存问题。同理可换 branch-misses(找不可预测分支)、page-faults(找缺页热点,mmap 大文件首次访问)、major-faults(找换页)。方法论:cycles 火焰图回答"时间在哪",专项事件火焰图回答"特定硬件事件在哪",两者互补;-c(周期计数)比 -F(频率)在事件采样下更精确。
/usr/share/bcc/tools/runqlat 10 5
/* 调度延迟直方图:任务从唤醒到上 CPU 的等待 */
/usr/share/bcc/tools/runqlen 1 10
/* 每秒采样运行队列长度,共 10 次 */
perf sched record -- sleep 10
perf sched timehist
/* 每次上下文切换明细:运行时长、等待时长、唤醒者 */
cat /sys/fs/cgroup/cpu.stat 2>/dev/null || cat /sys/fs/cgroup/cpu/mygroup/cpu.stat
/* nr_throttled 与 throttled_usec:cgroup 限流证据 */
调度延迟四件套,诊断"CPU 不高但任务等 CPU"的关键。runqlat 输出 2 的幂微秒直方图:健康系统 99% 的延迟在 1-64μs 桶;出现 1ms+ 的桶说明 CPU 供给紧张(runnable 任务多于核)或被限流;-p 参数可按进程细分(哪个应用在挨饿)。runqlen 给队列长度时序,与 vmstat r 列互证。perf sched timehist 是最详细的调度日志:每行一次切换,含任务名、运行时长(RUNTIME)、等待时长(DELAY)、唤醒者(WAKER)——等待时长大的行直接指出"谁被谁拖住",唤醒者链能还原因果(如 kswapd 唤醒说明内存回收路径)。cpu.stat 的 nr_throttled(被限流次数)与 throttled_usec(累计限流时长)是容器 CPU 配额问题的铁证:nr_throttled 持续增长 = cfs_quota 太小,应用在每个 100ms 周期内提前用完配额被冻结到周期结束——表现为规律性的 100ms 级延迟毛刺(第 10 章深入)。判读组合:runqlat 长尾 + cpu.stat 限流增长 → 加配额或降线程数;runqlat 长尾 + 无限流 + 宿主机 %steal 高 → 云平台争抢;runqlat 正常但延迟高 → 不是调度问题,转 off-CPU 分析。
turbostat --interval 1 --num_iterations 10
/* 每核实际频率(Bzy_MHz)、C-state 驻留、功耗 */
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
cpupower frequency-info
lscpu -e
/* CPU 拓扑:SOCKET CORE CPU 映射,识别 SMT 兄弟核 */
lstopo --of console
/* hwloc 的拓扑树:NUMA、L3、核、PU */
频率与拓扑的诊断工具组。turbostat 揭示"名义性能"与"实际性能"的差距:Bzy_MHz 是忙碌时的实际频率——若基频 2.5GHz 的 CPU 实测 Bzy_MHz 只有 1.8GHz,说明触发功耗墙或温度墙(PkgWatt、CoreTmp 列佐证);CPU%c1/c6/c7 列显示空闲核陷入的深度睡眠占比,C6 驻留高而唤醒延迟敏感的场景需要限制 C-state;Avg_MHz 是含空闲的平均频率。cpupower/scaling_governor 检查调频策略:performance(顶格频率)vs powersave(intel_pstate 默认,含 turbo 但响应保守)vs schedutil(随调度负载调)。lscpu -e 的拓扑表在绑核时必不可少:CPU0 与 CPU16 可能是同一物理核的 SMT 兄弟(CORE 列相同)——把两个延迟敏感线程绑到兄弟逻辑核等于让它们抢执行单元,正确做法是绑到不同 CORE 的核上。lstopo 输出 NUMA 节点与 L3 域层次,跨 NUMA 的线程通信与内存访问是第 7 章的主题。实战剧本:"优化后没变快"的排查顺序:先 turbostat 确认频率没变(排除环境因素),再对比火焰图(排除代码因素),最后查 steal/throttle(排除平台因素)——环境、代码、平台三因素分离是 CPU 性能对比的方法论纪律。

实验笔记

实验六:CPU 微架构与调度器行为实测。

实验目标:(1)亲手测量缓存未命中、伪共享、分支预测失败的性能代价,建立硬件直觉;(2)观测调度延迟分布并理解 CFS 行为;(3)完成一次"IPC 定性 → 火焰图定位 → 优化 → PMC 验证"的完整闭环。

环境准备:Linux 虚拟机或物理机(物理机效果更佳,虚拟机的 PMU 支持视平台而定;确认 perf list 中 hw cache 事件可用),gcc、perf、BCC tools。若虚拟机 PMC 不可用,伪共享与缓存实验仍可做(用时间测量),PMC 相关步骤记录"环境受限"即可。

步骤一(缓存代价实测):编写 C 程序 cache_bench.c:分配 256MB 数组,实现两种遍历——顺序访问(stride=1)与随机访问(预先生成随机置换索引链),各遍历多次计时,输出每次访问的平均纳秒数。再变化工作集大小(1MB、16MB、64MB、256MB)重测顺序遍历,画出"工作集大小-每元素耗时"曲线。预期:工作集在 L2 内时每元素约 1-2ns,超过 L3(256MB)后跳到 60-100ns——曲线的阶梯就是缓存层次的形状。随机访问 256MB 应稳定在 80-120ns(每次都是 DRAM 且预取失效)。用 perf stat -e cache-misses 验证两种模式的未命中数差异(应差两个数量级)。

步骤二(伪共享实测):编写 false_sharing.c:N 个线程(N=核数)各自对数组 counters[i] 做 1 亿次自增,线程 i 只写 counters[i](逻辑上零共享)。版本 A:counters 为紧凑 long 数组(相邻元素同行);版本 B:每个计数器填充到 64 字节(struct { long v; char pad[56]; })。对比两版本总耗时——预期 A 比 B 慢 3-15 倍。用 perf c2c record -a ./false_sharing_A 采集并 report,找到 HITM 最高的缓存行及其偏移,确认就是 counters 数组所在行。这个实验的震撼效果让它成为硬件性能课的经典保留节目。

步骤三(分支预测实测):编写 branch_bench.c:对 32K 个随机整数数组做"if (x >= 128) sum += x"的遍历。版本 A:数组未排序(分支结果随机,预测失败率约 50%);版本 B:数组排序(前半全 false 后半全 true,预测近乎完美)。对比耗时(经典结果:排序版快 5-10 倍——同样的工作量!),perf stat -e branch-misses 验证。进阶:用无分支写法(int t = (x - 128) >> 31; sum += ~t & x;)重写版本 A,验证其耗时与排序版相当,理解"消除不可预测分支"的优化手法。

步骤四(调度延迟观测):空载运行 runqlat 10 1 记录基线直方图(预期集中 1-16μs)。然后 stress-ng --cpu $((4 * $(nproc))) 制造 4 倍超订,再采 runqlat——观察直方图右移到百 μs 甚至 ms 级。同时 vmstat 1 看 r 列(应约 4×核数)。计算验证 CFS 语义:r 列 / 核数 × sched_min_granularity 约等于观测到的典型调度延迟。若有 cgroup 环境,创建 cpu.max 为 "20000 100000"(0.2 核配额)的 cgroup 跑一个 4 线程负载,观察 cpu.stat 的 nr_throttled 暴涨与 runqlat 的规律性长尾——这是容器限流的完整复现(第 10 章预告)。

步骤五(综合闭环):编译一个"性能差"的靶场程序(如低效字符串处理的 C++ 程序,或直接用 sysbench cpu),走完整流程:perf stat 测 IPC 定性 → perf record + 火焰图定位热点函数 → perf annotate 看指令级 → 实施一项优化(算法/编译选项 -O2 -march=native/数据布局)→ 重测 IPC 与吞吐验证收益 → 差分火焰图可视化变化。写报告记录每步数据。

预期结果:三组硬件代价实测数据(缓存阶梯曲线、伪共享倍数、分支预测倍数)、调度延迟直方图对比(空载/超订/限流)、一份完整的优化闭环报告。

进阶挑战:(1)用 perf mem 或 mlc(Intel Memory Latency Checker)测本机内存延迟与带宽矩阵(本地 NUMA vs 远端);(2)lscpu -e 找出 SMT 兄弟核,把步骤二的两线程分别绑到"兄弟核"与"不同物理核"对比伪共享效应之外的 SMT 争用;(3)turbostat 观察步骤一至五全程的频率与功耗变化,解释全核负载时 Bzy_MHz 低于单核 turbo 的原因;(4)阅读 EEVDF 的设计文档(LWN 系列文章),在 6.6+ 内核上对比 sched_latency 参数消失后 runqlat 的行为差异。

延伸阅读

  • 《Systems Performance》第 2 版第 6 章 CPUs 原文。原书覆盖 CPU 架构(流水线、缓存、TLB)、PMC、调度器(含 Solaris 与 Linux 对照)、性能分析工具与调优,并给出大量 perf/mpstat/pidstat 的判读示例。特别推荐其中关于"CPU 使用率的误导性"与调度延迟分析的段落,与本章要点互为印证。
  • 《深入理解计算机系统》(CSAPP)第 5 章程序性能优化与第 6 章存储器层次结构。第 6 章是缓存机制的最佳教学文本(局部性、缓存组织、伪共享的原始论述),第 5 章的"程序性能优化"从循环展开到内存山(memory mountain)实验,与本章实验一步骤一同构。两章合读能为 CPU 微架构性能打下最扎实的计算机科学基础。
  • Intel 64 and IA-32 Architectures Optimization Reference Manual(Intel 官方,免费 PDF)。微架构优化的权威手册:Top-Down 方法的原始出处(第 2 章)、缓存与预取器行为、伪共享规避、分支优化技巧。附录的 PMC 事件表是 perf list 输出的语义字典。AMD 有对应的 PPR 与 optimization guide。工具层面配合 pmu-tools(github.com/andikleen/pmu-tools)的 toplev 使用。
  • Brendan Gregg 的 CPU 相关博文与图表:《CPU utilization is wrong》(论述使用率掩盖 IPC 与频率问题)、《Linux perf Examples》的 CPU 章节、《Threaded CPU Cores》可视化。他的博客用大量实测图表解释每个指标的物理含义,是本章内容的"图解版"。另推荐其演讲《Performance Analysis Methodology》中 CPU 部分的案例。
  • Agner Fog 的优化手册系列(agner.org/optimize,五卷免费 PDF),特别是《The microarchitecture of Intel, AMD and VIA CPUs》与《Optimizing software in C++》。Fog 的手册以实测数据著称:每条指令的延迟/吞吐表、各代微架构的流水线细节、C++ 优化的硬件依据。比 Intel 手册更贴近程序员视角,是写高性能原生代码的案头书。
  • LWN.net 的调度器系列文章:《EEVDF scheduler》系列(2023,理解 6.6+ 的调度器变革)、《CFS bandwidth control》(cgroup CPU 配额的实现与 throttle 行为)、Ingo Molnar 的 sched-design-CFS.rst 原始设计文档。调度器是内核中演进最快的子系统之一,LWN 是跟踪其语义变化的最佳渠道,判读 runqlat/cpu.stat 数据前值得先读对应内核版本的设计文档。
  • Martin Thompson《Memory Barriers: a Hardware View for Software Hackers》演讲与 Ulrich Drepper《What Every Programmer Should Know About Memory》(LWN 连载,2007)。前者用一小时讲透缓存一致性协议(MESI)与 store buffer 如何决定并发代码性能,是理解伪共享的最短路径;后者是内存层次对软件影响的百科全书(虽年代较早,缓存行、NUMA、TLB 的论述依然成立)。两者共同构成"机械同情心"的硬件基础。
6/13CPU
0%