时间度量基础: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 需要微架构级优化? 使用 CPU 时钟周期 使用 CPU Time

这个图是我在实际项目中反复调整后总结出来的。你照着走,基本不会选错。

避坑指南:我曾经踩过的三个坑

  • 坑一:用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;

嗯,代码就这么简单。但真正用好它们,需要你理解背后的含义。

最后说一句:别迷信任何一种度量方式。它们都是工具,关键是你知道自己在测什么、想解决什么问题。