Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

性能工程 / Performance Engineering

1. 性能工程全景:从现象到证据 / Performance Engineering Overview

2. 基准测试:把变快变成可信实验 / Benchmarking as Evidence

3. 系统级追踪:用时间线看等待、调度与 I/O / System Tracing

4. CPU、内存、Cache 与 NUMA:把热点解释到硬件行为 / CPU, Memory, Cache and NUMA

5. GPU、Roofline 与异构瓶颈 / GPU, Roofline and Heterogeneous Bottlenecks

6. 优化闭环:从假设、改动到回归守护 / Optimization, Regression and Production

本页目录

CPU、内存、Cache 与 NUMA:把热点解释到硬件行为 / CPU, Memory, Cache and NUMA ​

当时间线告诉你线程确实在 CPU 上运行,下一步才是 CPU Profiling。

CPU Profiling 的第一层答案是“哪段代码占时间”。但真正的工程问题通常还要再问一句:它为什么占时间?

同样一个热点函数,可能慢在算法复杂度、分支预测、指令依赖、缓存未命中、内存带宽、锁原子、分配器、NUMA 远端访问,或者只是调用次数太多。

1. Sampling:先找 CPU 时间 ​

采样 Profiler 会定期打断程序,记录当前指令位置和调用栈。

样本聚合后,可以看到 CPU 时间主要落在哪些函数和调用路径。

text
运行中的程序
  |
  +-- sample 1: main -> parse -> read_token
  +-- sample 2: main -> parse -> read_token
  +-- sample 3: main -> build -> hash_lookup
  +-- sample 4: main -> build -> allocate_node
  |
  v
热点聚合
1
2
3
4
5
6
7
8
9

常用工具:

  • Windows:Visual Studio Profiler、WPA CPU Usage、VTune;
  • Linux:perf、VTune、gprof-ng;
  • macOS:Instruments;
  • AMD 平台:AMD uProf;
  • 通用展示:FlameGraph、Speedscope。

最小 Linux 流程:

bash
perf record -g -- ./app
perf report
1
2

VTune 流程:

bash
vtune -collect hotspots -- ./app
vtune -report hotspots -r r000hs
1
2

2. Inclusive Time 和 Self Time ​

Inclusive time 包含一个函数及其所有子调用。

Self time 只包含函数自身。

text
load_project()
  |
  +-- read_file()
  |
  +-- parse()
  |     |
  |     +-- read_token()
  |
  +-- build_index()
1
2
3
4
5
6
7
8
9

如果 load_project() inclusive time 很高,它可能只是上层入口。

如果 read_token() self time 很高,说明函数自身很值得看。

如果一个函数单次很便宜但调用十亿次,它也可能成为总热点。

看 Profile 时至少同时看:

  • 绝对时间;
  • 百分比;
  • 调用路径;
  • 调用次数;
  • 每次调用成本;
  • 输入规模关系;
  • 线程分布。

百分比很容易误导。优化后某个热点占比下降,可能是它变快了,也可能是别的阶段变慢了。

3. 符号和调用栈质量 ​

没有符号的 Profile 没法有效分析。

你需要保留:

  • 可执行文件;
  • 动态库;
  • 调试符号;
  • Build ID 或 PDB;
  • 源码提交;
  • 编译选项;
  • JIT 符号映射,如果有。

调用栈展开也要可靠。

缺失栈帧常见原因包括:

  • 省略 frame pointer;
  • unwind 信息不完整;
  • 手写汇编;
  • 尾调用;
  • JIT;
  • 栈损坏;
  • 工具权限不足;
  • 内核态符号缺失。

如果 Profile 里大量 [unknown],先修符号和栈,再谈优化。

4. 硬件计数器:从函数到机制 ​

硬件计数器是 CPU 记录事件的寄存器。

它可以计数:

  • cycles:周期;
  • instructions:退休指令;
  • branches:分支;
  • branch-misses:分支预测失败;
  • cache-references:缓存访问;
  • cache-misses:缓存未命中;
  • LLC-load-misses:最后一级缓存读未命中;
  • dTLB-load-misses:数据 TLB 未命中;
  • stalled-cycles:流水线停顿。

Linux 起步:

bash
perf stat ./app
perf stat -e cycles,instructions,branches,branch-misses,cache-misses ./app
1
2

常用派生指标:

text
IPC = instructions / cycles
branch miss rate = branch-misses / branches
cache miss rate = cache-misses / cache-references
1
2
3

但这些指标不能机械解释。

低 IPC 可能来自内存等待,也可能来自分支错误、指令依赖或前端供给不足。

高 Cache miss rate 不一定严重。如果访问次数很少,总时间可能仍然不大。

低 miss rate 也不代表访存没问题。如果总访问量巨大,内存带宽仍可能被打满。

5. Top-Down 微架构分析 ​

现代 CPU 的 Top-Down 方法会把流水线槽位粗分为几类。

text
Pipeline Slots
  |
  +-- Retiring
  +-- Bad Speculation
  +-- Frontend Bound
  +-- Backend Bound
        |
        +-- Core Bound
        +-- Memory Bound
1
2
3
4
5
6
7
8
9

Retiring 高,说明执行的指令大多有效退休。

Bad Speculation 高,常见于分支预测失败或错误路径执行。

Frontend Bound 高,说明取指、解码、指令缓存或微码供给可能不足。

Backend Bound 高,说明执行端口、数据依赖、缓存或内存供给可能限制吞吐。

VTune Microarchitecture Exploration 很适合沿着这条路往下看。

但 Top-Down 是模型,不是最终答案。它告诉你往哪个方向查,而不是自动给出修复方案。

6. Cache:不是越小越快,而是越近越快 ​

CPU 访问寄存器最快,访问内存慢得多。

Cache 是放在 CPU 和内存之间的高速缓存。

text
CPU Core
  |
  v
L1 Cache
  |
  v
L2 Cache
  |
  v
L3 Cache
  |
  v
DRAM
1
2
3
4
5
6
7
8
9
10
11
12
13

程序的访问顺序决定 Cache 是否帮得上忙。

连续数组通常友好,因为硬件预取器能提前加载后续数据。

链表和随机哈希访问通常不友好,因为下一个地址要等当前节点读出来才知道。

常见优化方向:

  • 用连续结构替代指针追逐;
  • 把热字段和冷字段分开;
  • 减少对象大小;
  • 批量处理;
  • 改善循环顺序;
  • 避免不必要的临时对象;
  • 减少跨线程写同一 Cache Line。

7. False Sharing ​

False Sharing 是多线程程序里很隐蔽的性能问题。

两个线程修改不同变量,但这些变量落在同一条 Cache Line 上,CPU 仍然需要在核心之间反复转移这条 Cache Line。

text
Cache Line 64B
+----------------+----------------+
| counter_thread0 | counter_thread1 |
+----------------+----------------+
        ^                  ^
        |                  |
     Core 0             Core 1
1
2
3
4
5
6
7

变量逻辑上不共享,硬件上却共享了缓存行。

表现可能是线程数增加后吞吐不升反降,硬件计数器出现大量缓存一致性流量。

解决方向是填充、对齐、分片计数、每线程局部累加,最后合并。

8. 分配器和内存生命周期 ​

频繁小对象分配会带来:

  • 分配器锁或线程缓存开销;
  • 元数据开销;
  • Cache locality 变差;
  • 内存碎片;
  • Page Fault;
  • 峰值内存升高;
  • 释放延迟;
  • NUMA 放置不可控。

分析工具:

  • heaptrack:Linux C/C++ 分配热点;
  • Massif:Valgrind 的堆快照工具;
  • jemalloc profiling:适合使用 jemalloc 的服务;
  • WPA Memory:Windows 场景;
  • ASan/LSan:正确性和泄漏,不是低扰动性能工具。

优化方向包括对象池、批量分配、Arena、预分配、复用缓冲区、减少临时对象和改变数据结构所有权。

但分配优化要谨慎。对象池可能提升速度,也可能增加峰值内存、复杂化生命周期,并隐藏 use-after-free。

9. NUMA:内存也有远近 ​

NUMA 是 Non-Uniform Memory Access。

它表示 CPU 访问不同内存区域的成本不一样。多路服务器上,每个 CPU Socket 附近有自己的内存通道。线程访问本地内存较快,访问另一个 Socket 的内存较慢。

text
Socket 0 ---- local ---- Memory 0
   |
 remote
   |
Socket 1 ---- local ---- Memory 1
1
2
3
4
5

NUMA 问题常见于:

  • 大内存数据结构;
  • 多线程初始化和多线程计算分离;
  • 线程迁移;
  • 全局队列;
  • 跨 Socket 共享状态;
  • 内存池集中分配;
  • 并行求解器和 HPC 程序。

Linux 可用:

bash
numactl --hardware
numastat -p <pid>
perf stat -e node-load-misses ./app
1
2
3

VTune Memory Access 也能帮助观察远端访问和带宽问题。

基本原则是 First Touch:谁第一次写入页面,页面通常就放到谁所在 NUMA 节点附近。

因此并行初始化和线程亲和性很重要。

10. 内存带宽瓶颈 ​

如果热点每个元素只做少量计算,却要读写大量内存,它可能受内存带宽限制。

例如:

cpp
for (std::size_t i = 0; i < n; ++i) {
    c[i] = a[i] + b[i];
}
1
2
3

每次迭代读取 a[i]、b[i],写入 c[i],计算只有一次加法。

这种代码通常不是缺少更聪明的指令,而是数据搬运成本主导。

优化方向可能是:

  • 减少数组遍历次数;
  • 融合循环;
  • 减少写回;
  • 使用更紧凑的数据类型;
  • 分块提高 Cache 复用;
  • 利用 SIMD;
  • 多线程时避免带宽过早打满。

11. 把证据写成假设 ​

不要写:

text
Cache miss 很高,所以重写数据结构。
1

要写:

text
现象:
  build_index 阶段占端到端 42%。

证据:
  perf record 显示 hash_lookup self time 高;
  perf stat 显示 LLC-load-misses 随输入规模线性增长;
  源码中每个元素会访问 3 个分散节点。

假设:
  指针追逐导致最后一级缓存未命中,限制 build_index。

实验:
  将节点改为连续数组索引,保持算法语义不变。

预期:
  LLC miss 和 build_index 时间下降,峰值内存不显著上升。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这样写,别人可以验证,也可以推翻。

12. 本篇总结 ​

CPU Profiling 先告诉你时间落在哪条调用路径。

硬件计数器和微架构分析再解释这条路径为什么慢。

内存、Cache、分配器和 NUMA 是 C++ 项目非常常见的性能边界,尤其在大数据结构、工程导入、求解器、渲染、并行计算和低延迟服务中。

真正的重点不是背指标,而是把指标和代码机制连起来。

最后更新于:

Pager
上一篇3. 系统级追踪:用时间线看等待、调度与 I/O / System Tracing
下一篇5. GPU、Roofline 与异构瓶颈 / GPU, Roofline and Heterogeneous Bottlenecks

持续记录,持续成长

Copyright © Tidenflow