# 基准测试:设计基准测试、常见基准测试框架 ## 为什么要做基准测试? 说白了,基准测试就是给代码“体检”。 我见过太多团队,优化全靠感觉。有人觉得“这里慢”,就闷头改。改完一跑,嘿,快了。但到底快了多少?不知道。更可怕的是,另一处代码变慢了,谁也没察觉。 基准测试就是你的“照妖镜”。它能告诉你: - 这次改动到底是优化了还是退化了 - 性能瓶颈到底在哪 - 你的SDK在用户设备上能不能抗住压力 ## 设计基准测试 ### 核心原则 设计基准测试,说白了就一句话:**测试能复现的问题,复现测试的结果**。 我以前踩过一个坑。给某个SDK做基准测试,第一次跑出来3ms,第二次变成8ms。我纳闷了半天...后来发现,第一次跑的时候CPU在降频,第二次刚跑完别的进程,cache是热的。你想想看,这种数据根本没法用。 所以,设计基准测试要遵守这几个原则:
原则 说明 我的建议
可重复性 同样的代码、同样的环境,跑出来结果应该基本一致 至少跑5次取中位数
隔离性 被测代码不要受其他进程干扰 关掉后台程序、设置CPU亲和性
代表性 测试场景要贴近真实使用 别只测最理想的情况
可比较性 不同版本之间能直接对比 保持测试条件一致
### 测试维度 基准测试不是光测“快不快”就完了。我个人习惯从这几个维度入手: 1. **延迟**:单次操作耗时 2. **吞吐量**:单位时间能处理多少请求 3. **CPU开销**:用户态/内核态时间比 4. **内存消耗**:堆、栈、缓存使用情况 5. **稳定性**:多次测试的方差 ### 常见陷阱 **陷阱一:只测最优情况** 举个例子,你写了个排序算法。测试数据是已经排好序的数组...那当然快。但你想想,用户的数据会这么规整吗? **陷阱二:忽视预热** JIT编译器、CPU缓存、内存分配器...这些东西都需要“热身”。我第一次做Java SDK基准测试时,结果忽高忽低。后来才知道,JVM需要跑几万次才会把热点代码编译好。 **陷阱三:测试代码本身有开销** 我记得有次测试一个字符串拼接函数。我写了这样的代码:
// 错误示范
auto start = high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
    result = result + "a";
}
auto end = high_resolution_clock::now();
能发现问题吗?循环里每次都要创建新字符串,测的是字符串拷贝的性能,根本不是拼接函数的性能。这就是典型的“测试代码污染了测试结果”。 ## 常见基准测试框架 ### Google Benchmark 这个框架我用了好几年。它最方便的地方是——**你只需要关注被测代码,调度、统计、报告这些脏活它全包了**。 先看个例子:
#include <benchmark/benchmark.h>

static void BM_StringCreation(benchmark::State& state) {
    for (auto _ : state) {
        std::string empty_string;
    }
}
BENCHMARK(BM_StringCreation);

static void BM_StringCopy(benchmark::State& state) {
    std::string x = "hello";
    for (auto _ : state) {
        std::string copy(x);
    }
}
BENCHMARK(BM_StringCopy);

BENCHMARK_MAIN();
这段代码测的是空字符串创建和字符串拷贝的开销。Google Benchmark会自动: - 计算每次迭代的时间 - 排除循环本身的开销 - 输出统计信息(均值、方差等) **常用功能**
功能 用法 说明
参数化 BENCHMARK(BM_test)->Arg(8)->Arg(64) 测试不同参数组合
多线程 BENCHMARK(BM_test)->Threads(4) 模拟多线程并发
自定义标签 state.SetLabel("label") 给测试结果加说明
跳过迭代 state.SkipWithError("reason") 条件不满足时跳过测试
**避坑指南** 我曾经遇到一个问题:测试结果特别稳定,但上线后性能差别很大。 后来发现,Google Benchmark默认会在`for (auto _ : state)`循环里反复执行被测代码,直到它认为数据稳定为止。这对于计算密集型的场景没问题。但如果你的代码涉及IO、网络、内存分配,那就要小心了——循环次数太多,可能会触发GC或者内存回收,反而把平均耗时拉高了。
我的习惯:对于有外部依赖的测试,使用Iterations(100)手动控制迭代次数,然后再做统计分析。
### C++ Benchmark的其他选择 除了Google Benchmark,还有几个值得关注: | 框架 | 特点 | 适用场景 | | --- | --- | --- | | **Nonius** | 轻量、单头文件 | 快速原型测试 | | **Celero** | 支持图像化输出 | 需要可视化报告 | | **hayai** | 专注微基准测试 | 毫秒级以下耗时 | 不过说实话,现在Google Benchmark基本是事实标准。除非你项目实在用不了CMake,不然我建议直接上Google Benchmark。 ### 其他语言场景 SDK开发不只有C++。我在iOS和Android上也做过不少基准测试: **iOS (Swift)**
import XCTest

class SDKBechmarks: XCTestCase {
    func testEncodingPerformance() {
        measure {
            // 被测代码
            encode(data)
        }
    }
}
`XCTest`的`measure`方法会自动采集耗时,并且支持基线对比。你可以在Xcode里设置“性能测试基线”,一旦新版本慢于基线,测试就会失败。这个功能在CI/CD里特别好用。 **Android (Java/Kotlin)**
@RunWith(AndroidJUnit4::class)
class SdkBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun testDecode() {
        benchmarkRule.measureRepeated {
            runWithTiming { decoder.decode(data) }
        }
    }
}
Android的`BenchmarkRule`会自动处理预热、隔离、统计。不过要注意的是,这些测试要在真机上跑。模拟器的CPU/内存模型和真机差距很大,测出来的数据基本没用。 ### 基准测试的核心流程

核心流程:明确假设 → 选择测试用例 → 准备测试环境 → 编写基准代码 → 执行测试 → 分析结果 → 报告与决策

警告:基准测试的结果永远是“当时当地”的。换了CPU、换了操作系统、甚至只是后台进程变了,结果都可能不一样。不要迷信单次测试的数字,要看趋势。
## 知识体系:基准测试框架结构 基准测试知识体系 基准测试框架 C++ 基准测试 Google Benchmark Nonius / Celero 参数化/多线程支持 移动端基准测试 iOS: XCTest measure Android: BenchmarkRule 真机 vs 模拟器差异 方法论与实践 延迟/吞吐量/方差 预热/隔离/预热次数 结果统计与对比 输出:可复现的性能基线 ## 实践建议 ### 如何选择框架? 1. **如果你写C++**:无脑上Google Benchmark。社区活跃,工具链成熟,文档齐全。 2. **如果你做iOS开发**:用XCTest自带的`measure`就够了。不够的话,考虑Swift Benchmark Suite。 3. **如果你做Android开发**:用`BenchmarkRule`。记得在真机上跑。 ### 什么时候不该用基准测试? 这个问题我问过自己很多次。其实只有一个答案: **当你的代码还在频繁改逻辑的时候,别急着做基准测试。** 基准测试的投入产出比,取决于被测代码的稳定性。你改一行逻辑,所有数据都要重跑。那还不如先把功能调稳了,再开始做性能基线。