时间度量基础:CPU时钟周期、Wall-Clock Time、CPU Time
做算子性能优化,第一件事就是搞清楚「时间」到底怎么量。
你可能觉得这有什么好说的?不就是掐个秒表吗?
嗯,我当年也这么想。直到有一次,我优化了一个算子,跑出来快了30%,结果换台机器测,反而慢了10%。当时我就懵了。后来才发现,我用的时间度量方式压根就不对。
三种时间度量,你选对了吗?
咱们先看三个最基础的概念。搞懂了它们,你才算真正入了性能优化的门。
| 度量方式 | 含义 | 典型场景 |
|---|---|---|
| Wall-Clock Time | 墙上时钟时间,也叫「挂钟时间」 | 用户实际等待时间 |
| CPU Time | 程序在CPU上实际执行的时间 | 算法本身的效率 |
| CPU时钟周期 | CPU执行一条指令的最小时间单位 | 微架构级别的调优 |
Wall-Clock Time:用户感知的「真实时间」
说白了,就是你拿手机秒表从程序开始按到结束的时间。
它包含了所有东西:CPU计算、内存访问、磁盘I/O、网络延迟、甚至操作系统调度的时间片。
什么时候用?
当你在意「用户体验」的时候。比如一个推理服务,用户从发请求到拿到结果,这就是Wall-Clock Time。
我有个教训。之前做视频编码算子优化,我盯着CPU Time优化了半天,快了50%。结果上线后用户反馈说「没感觉快啊」。一查,瓶颈在磁盘写入,CPU根本就在等I/O。Wall-Clock Time几乎没变。
CPU Time:算法本身的「纯干活时间」
这个只统计CPU在用户态和内核态执行代码的时间。它不管你在等磁盘还是等网络。
为什么需要它?
- 你想知道「这个算法到底快不快」——用CPU Time
- 你想对比两个算子的计算效率——用CPU Time
- 你想排除系统干扰——用CPU Time
我的习惯:做算子微调时,我只看CPU Time。因为它稳定、可复现。Wall-Clock Time受系统负载影响太大,跑十次可能十个结果。
CPU时钟周期:最底层的「心跳」
这是CPU内部的时间单位。一个3GHz的CPU,每秒有30亿个时钟周期。
为什么搞这么底层?因为有些优化,你只看毫秒级的时间根本看不出区别。
举个例子:
// 场景:循环展开优化
// 优化前
for (int i = 0; i < 1024; i++) {
a[i] = b[i] + c[i];
}
// 优化后(4次展开)
for (int i = 0; i < 1024; i += 4) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
a[i+2] = b[i+2] + c[i+2];
a[i+3] = b[i+3] + c[i+3];
}
用Wall-Clock Time测,可能都是0.1ms,看不出差别。但用CPU时钟周期测,优化前可能用了3000个周期,优化后用了2200个周期。差距就出来了。
注意:CPU时钟周期在现代CPU上并不完全可靠。因为CPU有动态调频、睿频、休眠状态。同一个程序,跑在2.0GHz和跑在4.0GHz,时钟周期数可能不一样。
如何选择合适的时间度量?
我总结了一个简单的决策流程,画成了图,你看看:
这个图是我在实际项目中反复调整后总结出来的。你照着走,基本不会选错。
避坑指南:我曾经踩过的三个坑
- 坑一:用Wall-Clock Time对比算法效率。 我曾经在笔记本上跑两个算子的对比测试,一个跑的时候刚好系统在后台做Windows更新,结果慢了3倍。我差点把这个算子给否了。后来换成CPU Time,发现两个算子其实差不多。
- 坑二:忽略CPU频率变化。 有次我在服务器上做性能测试,发现同一个算子,上午和下午跑出来的时钟周期数差了20%。查了半天,原来是服务器散热不行,下午温度高了,CPU自动降频了。
- 坑三:在多线程场景下用CPU Time。 CPU Time是所有线程时间的总和。如果你开了8个线程,CPU Time可能是Wall-Clock Time的8倍。这时候看CPU Time反而会误导你。
我的建议:
- 做算子微调时,用CPU时钟周期(配合perf stat)
- 做算法选型时,用CPU Time(配合getrusage或clock_gettime)
- 做系统级优化时,用Wall-Clock Time(配合chrono或gettimeofday)
- 永远跑三次取中位数,别取平均值
代码示例:三种时间度量的获取方式
#include <chrono>
#include <sys/resource.h>
#include <unistd.h>
// 1. Wall-Clock Time
auto start = std::chrono::high_resolution_clock::now();
// ... 你的算子 ...
auto end = std::chrono::high_resolution_clock::now();
auto wall_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
// 2. CPU Time
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
long cpu_us = usage.ru_utime.tv_sec * 1000000 + usage.ru_utime.tv_usec;
// 3. CPU时钟周期(x86)
unsigned int lo, hi;
__asm__ volatile ("rdtsc" : "=a" (lo), "=d" (hi));
// ... 你的算子 ...
__asm__ volatile ("rdtsc" : "=a" (lo), "=d" (hi));
unsigned long long cycles = ((unsigned long long)hi << 32) | lo;
嗯,代码就这么简单。但真正用好它们,需要你理解背后的含义。
最后说一句:别迷信任何一种度量方式。它们都是工具,关键是你知道自己在测什么、想解决什么问题。