返回首页

代码基准测试:精确的 CPU 和缓存测量

章节:创建具有高精度 CPU 时钟周期测量和 perf 计数器的基准测试框架。x86、RISC-V、ARM 示例。统计分析和常见错误。

精确基准测试:从 rdtsc 到 perf 分析
Advertisement 728x90

基准测试与性能分析:精确的代码性能测量

没有客观数据就优化代码会导致错误。一个据说为了改善缓存局部性而重写的哈希函数,却使执行速度慢了 15%。直觉具有欺骗性——需要对时间和处理器事件进行精确测量。

基准测试框架解决了这个问题:多次运行、统计分析、与 perf 集成。这能识别真正的瓶颈。

高精度计时方法

标准的 time() 提供 1 秒分辨率——对于微优化来说不可接受。

Google AdInline article slot

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:

Google AdInline article slot
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 个周期,无系统调用开销。缺点:架构特定,受频率变化影响。

结果的统计分析

由于缓存变化、操作系统中断、分支等因素,单次运行毫无意义。

Google AdInline article slot

基本统计

#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 performance
  • taskset -c 0
  • nice -n -20

关键要点

  • CPU 计数器: rdtsc/rdcycle——准确性的黄金标准
  • 统计: 中位数 + 标准差比均值更可靠
  • perf 事件: 缓存缺失揭示局部性问题
  • 缓存预热: 真实测量必不可少
  • Volatile: 防止死代码优化

— Editorial Team

Advertisement 728x90

继续阅读