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

本页目录

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

性能优化的第一步不是打开 Profiler,而是让“慢”变成可重复观察的实验。

如果基准不可信,后面的火焰图、计数器和优化结论都会跟着漂。你以为自己优化了代码,实际可能只是输入变小、缓存变热、后台进程停了,或者编译器把没有被使用的计算消掉了。

1. 什么是基准测试 ​

基准测试是用固定工作负载和固定口径衡量系统表现。

它不只是计时。

它要回答:

  • 测的是哪个用户路径;
  • 输入规模和数据分布是什么;
  • 运行环境是否记录清楚;
  • 结果是否正确;
  • 波动范围有多大;
  • 新版本是否真的超过噪声。

一个好的基准像一把尺子。尺子本身如果会伸缩,就不能拿来判断代码长短。

2. 结果指标和诊断指标 ​

结果指标直接对应目标。

诊断指标解释原因。

text
结果指标
  +-- 用户等待了多久
  +-- 每秒完成多少任务
  +-- 峰值内存多少
  +-- 单个任务成本多少

诊断指标
  +-- CPU 利用率
  +-- Cache miss
  +-- 锁等待
  +-- I/O 队列
  +-- GPU Occupancy
1
2
3
4
5
6
7
8
9
10
11
12

结果指标决定优化是否成功。

诊断指标帮助判断下一步该查哪里。

例如 CPU 利用率从 40% 到 90% 不一定是好事。如果吞吐没变、延迟变差,它可能只是忙等或无效重算。反过来 CPU 利用率下降也不一定是坏事,如果吞吐相同且成本下降,可能正是优化成果。

3. 工作负载决定结论边界 ​

工作负载是基准里最容易被低估的部分。

微基准可以研究一个函数、一种数据结构、一段循环。

组件基准可以研究解析器、求解器、渲染器、数据库访问层。

端到端基准可以研究真实用户路径。

text
微基准
  -> 机制清楚,容易复现,但可能脱离真实调用方式

组件基准
  -> 保留模块边界,适合工程优化

端到端基准
  -> 最接近用户价值,但归因更难
1
2
3
4
5
6
7
8

同一个项目至少保留三类输入:

  • 小输入:用于快速回归;
  • 典型输入:代表常见真实场景;
  • 压力输入:暴露内存、I/O、并发或算法极限。

对工程软件、CAE、可视化、编译器、数据导入这类项目,还要记录输入文件的校验和。否则“同名文件”后来被改过,历史性能数据就失去意义。

4. 冷启动和稳态不能混在一起 ​

冷启动包含首次加载、动态库初始化、页面缓存、JIT、GPU Context、Shader 编译、连接建立等成本。

稳态运行更多反映核心路径的持续性能。

text
冷启动
  = 进程启动
  + 动态库加载
  + 初始化
  + 首次打开资源
  + 第一次有用结果

稳态
  = 已初始化系统
  + 重复执行核心工作
  + 输出结果
1
2
3
4
5
6
7
8
9
10
11

如果用户抱怨“第一次打开项目慢”,你不能预热后只测稳态。

如果目标是长时间求解吞吐,你也不能把一次性初始化混进每一步迭代。

报告时把两者分开写,别用一个平均值糊过去。

5. 噪声与样本 ​

性能数据有噪声。

噪声来自 CPU 动态频率、温度、后台进程、操作系统调度、磁盘缓存、网络状态、虚拟化、内存布局、地址随机化、分支预测历史和输入随机性。

最低限度要多次运行。

text
baseline:  101 ms, 105 ms, 99 ms, 104 ms, 100 ms
candidate: 96 ms,  98 ms,  97 ms, 99 ms,  96 ms
1
2

这种结果比“某一次从 101 ms 到 96 ms”更可信。

如果基线自然波动有 8%,候选版本快 3%,结论应该是证据不足。工程上可以继续观察,但不要宣布优化成功。

6. A/B 交错运行 ​

长时间测试容易遇到环境漂移。

例如机器温度升高、后台索引开始、远端服务变慢。此时先跑完 A 再跑 B,可能把时间漂移误判成版本差异。

更稳妥的方法是交错:

text
A B A B A B A B
1

每轮都记录原始样本。

如果结果对环境很敏感,可以在报告中说明“本实验只能证明这个机器和这个负载下的差异”。

7. 正确性护栏 ​

基准不能奖励错误行为。

常见虚假优化包括:

  • 编译器消除了未使用结果;
  • 跳过输入校验;
  • 缓存了本该重新计算的数据;
  • 降低浮点精度或收敛标准;
  • 少写日志但破坏审计;
  • 把工作延迟到基准计时之后;
  • 丢弃慢请求;
  • 峰值内存暴涨但只看时间。

C++ 微基准里尤其要避免死代码消除。

Google Benchmark 通常这样写:

cpp
#include <benchmark/benchmark.h>
#include <vector>

static void BM_sum(benchmark::State& state) {
    std::vector<int> data(state.range(0), 1);

    for (auto _ : state) {
        long long sum = 0;
        for (int value : data) {
            sum += value;
        }
        benchmark::DoNotOptimize(sum);
    }
}

BENCHMARK(BM_sum)->Arg(1024)->Arg(1024 * 1024);
BENCHMARK_MAIN();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

benchmark::DoNotOptimize 告诉编译器这个结果对观察者有意义,不能简单删除。

但它不等于正确性测试。真实项目还要检查输出、误差、校验和、异常路径和资源释放。

8. 编译模式要诚实 ​

性能分析通常应使用 Release 构建,并保留符号。

Debug 构建会改变内联、优化、断言、迭代器检查、运行库和内存布局。它适合调试正确性,不适合推断最终性能。

推荐形态是:

text
Release 优化
  + 调试符号
  + 可复现构建信息
  + 关闭不属于生产的诊断开关
1
2
3
4

在 CMake 中,常见选择是 RelWithDebInfo。

如果开启 Sanitizer、日志、统计计数、Trace 宏,也要在报告里写清楚。它们可能改变性能。

9. 基准记录模板 ​

一条可复查的基准记录可以这样写:

text
目标:
  大模型导入 P95 低于 12 s。

工作负载:
  case-large-01.bdf,5.2 GB,冷启动,校验和 0x...

环境:
  Windows 11,i9-13900K,64 GB,NVMe SSD,电源高性能。

构建:
  commit abc123,MSVC 19.xx,RelWithDebInfo,LTO off。

步骤:
  每轮重启进程,运行 15 次,丢弃首次失败样本但保留记录。

正确性:
  节点、单元、材料、属性数量一致;错误日志一致。

结果:
  baseline median 14.8 s, P95 18.4 s
  candidate median 11.1 s, P95 12.7 s

结论:
  P95 接近目标但峰值内存增加 420 MB,需要进入下一轮内存分析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这个模板让别人能复现,也能反驳。

10. 微基准什么时候会骗人 ​

微基准最大的问题是上下文太干净。

真实程序里,一个函数可能被不同输入、不同线程、不同缓存状态、不同分配器、不同调用频率包围。微基准单独跑得快,不代表端到端变快。

它常见的误导包括:

  • 数据一直在 L1/L2 Cache,真实工作集却远大于 Cache;
  • 分支模式过于规律;
  • 分配器没有并发压力;
  • 编译器看到更多常量;
  • 没有系统调用和 I/O;
  • 没有锁竞争;
  • 没有 NUMA 远端访问;
  • 没有真实错误路径。

微基准的正确用法是验证机制。

例如“连续数组比链表遍历更容易被缓存和预取器利用”,微基准很合适。要证明产品导入变快,还要回到组件基准和端到端基准。

11. 什么时候进入 Profiler ​

当基准已经能稳定复现问题,就进入归因阶段。

text
基准稳定
  |
  +-- 不知道慢在哪:系统追踪
  |
  +-- CPU 忙:采样 Profiler
  |
  +-- 热点原因不明:硬件计数器
  |
  +-- 线程在等:Off-CPU / 锁 / I/O
  |
  +-- GPU 相关:Nsight Systems -> Nsight Compute
1
2
3
4
5
6
7
8
9
10
11

如果你还不知道慢在哪个阶段,先用系统级时间线。

如果你知道 CPU 正在忙,使用 CPU 采样。

如果热点明确但原因不明,使用硬件计数器。

如果线程空洞很多,查调度、锁、I/O 和 Off-CPU。

如果 GPU 空洞很多,先查 CPU/GPU 流水线,再查单个 Kernel。

12. 本篇总结 ​

基准测试的核心不是“测一次时间”,而是让性能结论具有证据资格。

你要固定工作负载,区分冷启动和稳态,保存原始样本,记录环境,校验正确性,并承认噪声边界。

只有这样,后面的 WPR、WPA、VTune、perf、Nsight 才知道该解释什么。

最后更新于:

Pager
上一篇1. 性能工程全景:从现象到证据 / Performance Engineering Overview
下一篇3. 系统级追踪:用时间线看等待、调度与 I/O / System Tracing

持续记录,持续成长

Copyright © Tidenflow