C 语言精确计时与基准测试
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。想优化性能,先得测得准。但 C 里计时函数有好几种,精度天差地别——clock可能根本测不出你的优化效果,rdtsc能数到 CPU 周期。本文用实测讲清怎么选计时函数、怎么写可信的基准测试。
本文要回答的问题
clock、clock_gettime、rdtsc各有多准?怎么选?CLOCK_MONOTONIC和CLOCK_REALTIME有什么区别?- 为什么"测出来快"的代码,实际可能没快?基准测试有哪些陷阱?
一、四种计时方式对比
实测同一个"100 万次空操作"循环:
c
// 1. clock(粗粒度,进程 CPU 时间)
clock_t a = clock();
for (int i = 0; i < 1000000; i++) { volatile int x = i; }
clock_t b = clock();
// 2. clock_gettime(纳秒级)
struct timespec t1, t2;
clock_gettime(CLOCK_MONOTONIC, &t1);
/* ... */
clock_gettime(CLOCK_MONOTONIC, &t2);
// 3. rdtsc(CPU 周期级)
uint64_t c1 = rdtsc();
/* ... */
uint64_t c2 = rdtsc();实测结果:
text
clock_gettime: 313069 ns ← 纳秒级,可靠
rdtsc: 1655524 个时钟周期 ← 周期级,最精细
clock: 0 ticks ← 精度太粗,测不出!clock() 返回 0——它的粒度(ticks)太粗,100 万次空操作都测不出来。这就是"用错计时函数导致优化测不出效果"的典型。
二、计时函数选型
| 函数 | 精度 | 测量对象 | 适用 |
|---|---|---|---|
clock() | 粗(µs~ms) | 进程 CPU 时间 | 粗粒度、秒级 |
gettimeofday() | µs 级 | 墙上时钟(会跳变) | 一般场景 |
clock_gettime() | ns 级 | 单调/实时时钟 | 推荐默认 |
rdtsc | CPU 周期 | 周期数 | 极精细、单函数级 |
推荐 clock_gettime(CLOCK_MONOTONIC, ...):纳秒精度 + 单调时钟,是基准测试的标准选择。
2.1 CLOCK_MONOTONIC vs CLOCK_REALTIME
| 时钟 | 特点 | 问题 |
|---|---|---|
CLOCK_REALTIME | 墙上时钟,可被 NTP/用户调整 | 测时间会跳变(回拨) |
CLOCK_MONOTONIC | 单调递增,不受时间调整影响 | 无 |
基准测试必须用 CLOCK_MONOTONIC——用 REALTIME 测时间,系统校时一发生,你的测量就乱了。
三、rdtsc:数 CPU 周期
c
static inline uint64_t rdtsc(void) {
unsigned int lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}rdtsc 读 CPU 时间戳计数器,精度到单个周期,是剖析单条指令、单次函数调用开销的利器。
但注意:
- 乱序执行下,rdtsc 前后的指令可能重排,需
mfence/lfence夹住 - 多核下不同核的 TSC 可能不同步(现代 CPU 一般同步)
- 频率变化(turbo)下,周期数 ≠ 真实时间
四、基准测试的四大陷阱
| 陷阱 | 现象 | 对策 |
|---|---|---|
| 编译器优化掉被测代码 | 空循环被消除,测得"0 时间" | 用 volatile 或让结果有副作用 |
| 没预热 | 第一次跑含缓存冷启动 | 先跑一次预热,再正式测 |
| 单次测量噪声大 | 结果忽快忽慢 | 多次运行取中位数/平均 |
| 测的时间太短 | 低于计时精度,测不准 | 循环放大(跑 100 万次) |
五、可信的基准测试模板
c
#include <time.h>
#include <stdio.h>
#define ITER 1000000
static inline double now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1e9 + ts.tv_nsec;
}
double measure(void (*fn)(void)) {
fn(); // 预热
double best = 1e18;
for (int r = 0; r < 10; r++) { // 跑 10 轮取最小
double t = now_ns();
for (int i = 0; i < ITER; i++) fn();
double e = now_ns() - t;
if (e < best) best = e;
}
return best / ITER; // 单次平均
}要点:预热 + 多轮取最小(排除 GC、调度噪声)+ 放大循环 + CLOCK_MONOTONIC。
六、与本站主线衔接
- 内联汇编:rdtsc 的内联汇编实现,见 内联汇编入门。
- 编译优化:编译器消除空循环导致测不准,见 编译优化行为。
- 缓存:预热与缓存冷启动对测量的影响,见 缓存与 C 程序性能。
- perf:perf 与手动计时的配合,见 perf 剖析 C 程序。
一句话总结
计时要选对函数:clock 太粗(实测测出 0)、clock_gettime(CLOCK_MONOTONIC) 纳秒级是标准、rdtsc 精确到周期;基准测试要防"编译器优化掉代码、没预热、单次噪声、测太短"四大陷阱,用预热 + 多轮取最小 + 单调时钟才可信。