基准测试与性能分析:精确的代码性能测量
没有客观数据就优化代码会导致错误。一个据说为了改善缓存局部性而重写的哈希函数,却使执行速度慢了 15%。直觉具有欺骗性——需要对时间和处理器事件进行精确测量。
基准测试框架解决了这个问题:多次运行、统计分析、与 perf 集成。这能识别真正的瓶颈。
高精度计时方法
标准的 time() 提供 1 秒分辨率——对于微优化来说不可接受。
clock_gettime()
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
run_test();
clock_gettime(CLOCK_MONOTONIC, &end);
long ns = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
优点: 纳秒分辨率、抗系统时间调整、POSIX 可移植性。
CPU 周期计数器(推荐)
RISC-V:
static inline uint64_t rdcycle(void) {
uint64_t cycles;
asm volatile ("rdcycle %0" : "=r" (cycles));
return cycles;
}
x86_64:
static inline uint64_t rdtsc(void) {
uint32_t lo, hi;
asm volatile ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) | lo;
}
ARM64:
static inline uint64_t rdcycle(void) {
uint64_t val;
asm volatile("mrs %0, pmccntr_el0" : "=r"(val));
return val;
}
分辨率——1 个周期,无系统调用开销。缺点:架构特定,受频率变化影响。
结果的统计分析
由于缓存变化、操作系统中断、分支等因素,单次运行毫无意义。
基本统计
#define ITERATIONS 1000
uint64_t times[ITERATIONS];
// ... fill times ...
uint64_t min = times[0], max = times[0], sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
if (times[i] < min) min = times[i];
if (times[i] > max) max = times[i];
sum += times[i];
}
uint64_t mean = sum / ITERATIONS;
高级统计
中位数对异常值鲁棒,标准差表示稳定性:
qsort(times, ITERATIONS, sizeof(uint64_t), compare_uint64);
uint64_t median = times[ITERATIONS / 2];
double variance = 0;
for (int i = 0; i < ITERATIONS; i++) {
double diff = (double)times[i] - (double)mean;
variance += diff * diff;
}
double stddev = sqrt(variance / ITERATIONS);
关键指标:
- 最小值:最佳情况(热缓存)
- 中位数:典型性能
- 标准差:变异性
- 最大值:最差情况
基准测试框架
通用接口,便于重用:
typedef struct {
const char *name;
void (*setup)(void);
void (*run)(void);
void (*teardown)(void);
} benchmark_t;
void benchmark_run(benchmark_t *bench, int iterations);
实现包括:
- 预热运行
- 多次测量
- 统计报告
示例:
int array[1000];
void setup_array(void) {
for (int i = 0; i < 1000; i++) {
array[i] = i;
}
}
void test_sequential_access(void) {
volatile int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += array[i];
}
}
benchmark_t bench = {
.name = "Sequential Array Access",
.setup = setup_array,
.run = test_sequential_access,
.teardown = NULL
};
benchmark_run(&bench, 1000);
使用 perf 进行缓存分析
perf stat 跟踪硬件计数器:
$ perf stat -e cache-references,cache-misses ./program
1,234,567 cache-references
12,345 cache-misses # 1.00% miss rate
有用事件:
cache-references/misses:所有级别L1-dcache-loads/misses:L1 数据LLC-loads/misses:最后一级
结构比较: 数组——1.2K 次缺失,链表——45K 次缺失(差 37 倍)。
将 perf 集成到框架中
typedef struct {
uint64_t cycles;
uint64_t cache_references;
uint64_t cache_misses;
uint64_t l1_loads;
uint64_t l1_misses;
} perf_counters_t;
自动收集和聚合 perf_event_open 计数器。
常见问题及解决方案
编译器优化
// 错误:循环被消除
int sum = 0;
for (...) sum += array[i];
// 正确
volatile int sum = 0;
冷缓存 vs 热缓存
首次运行较慢。解决方案:预热 + 单独指标。
测量开销
rdcycle() 消耗约 10 个周期。减去或使用长测试。
系统噪声
- 多次迭代
- 使用中位数而非均值
cpupower frequency-set -g performancetaskset -c 0nice -n -20
关键要点
- CPU 计数器: rdtsc/rdcycle——准确性的黄金标准
- 统计: 中位数 + 标准差比均值更可靠
- perf 事件: 缓存缺失揭示局部性问题
- 缓存预热: 真实测量必不可少
- Volatile: 防止死代码优化
— Editorial Team
暂无评论。