23、chrono时间库:时间点与时间段、时钟类型、duration转换、实战:性能基准测试框架
时间处理,在C++里一直是个让人头疼的事。早些年我还在用clock()和gettimeofday()的时候,跨平台兼容性问题没少踩坑。直到C++11引入了chrono库,这才算真正有了一个统一、类型安全的时间处理方案。
说实话,我刚接触chrono时也觉得它有点绕——什么duration、time_point、clock,一堆模板参数看得眼花。但用顺手之后,你会发现它其实非常优雅。今天我们就把它彻底讲透。
23.1 核心概念:时间点与时间段
chrono库的核心就两个东西:时间段(duration)和时间点(time_point)。你想想看,时间本质上不就是「某个时刻」和「两个时刻之间的间隔」吗?
23.1.1 duration:时间段的表示
duration是一个模板类,定义在头文件中。它的原型长这样:
template<class Rep, class Period = ratio<1>>
class duration;
- Rep:存储时间的数值类型,比如
int、long long、double。 - Period:时间单位,默认是
ratio<1>,也就是秒。
举个简单的例子:
#include <chrono>
#include <iostream>
int main() {
// 5秒
std::chrono::duration<int> five_seconds(5);
// 3毫秒
std::chrono::duration<int, std::milli> three_ms(3);
// 2.5微秒(用double存储)
std::chrono::duration<double, std::micro> two_point_five_us(2.5);
std::cout << "five_seconds: " << five_seconds.count() << " ticks\n";
std::cout << "three_ms: " << three_ms.count() << " ticks\n";
return 0;
}
不过在实际项目中,我们很少直接写duration<int, std::milli>这种长串。标准库提供了一些常用的类型别名:
| 类型别名 | 定义 | 单位 |
|---|---|---|
std::chrono::nanoseconds | duration<int64_t, nano> | 纳秒 |
std::chrono::microseconds | duration<int64_t, micro> | 微秒 |
std::chrono::milliseconds | duration<int64_t, milli> | 毫秒 |
std::chrono::seconds | duration<int64_t> | 秒 |
std::chrono::minutes | duration<int64_t, ratio<60>> | 分钟 |
std::chrono::hours | duration<int64_t, ratio<3600>> | 小时 |
23.1.2 time_point:时间点的表示
time_point表示一个具体的时间点。它依赖于某个时钟,并且从该时钟的纪元(epoch)开始计算。说白了,就是「从某个起点到现在过了多久」。
template<class Clock, class Duration = typename Clock::duration>
class time_point;
获取当前时间点很简单:
auto now = std::chrono::system_clock::now();
两个时间点可以相减,得到的是duration:
auto start = std::chrono::steady_clock::now();
// ... 做一些事情 ...
auto end = std::chrono::steady_clock::now();
auto elapsed = end - start; // 类型是 duration
std::cout << "耗时: " << elapsed.count() << " ticks\n";
.count()得到的是时钟的tick数,不是秒!你需要知道时钟的周期才能换算成人类可读的时间。后面我们会讲如何转换。
23.2 三种时钟类型
chrono库提供了三种时钟,各有各的用途。我当年刚学时经常搞混,这里帮你理清楚。
23.2.1 system_clock:系统时钟
system_clock代表系统范围内的实时时钟。它有两个特点:
- 可以转换为
time_t,从而得到日历时间。 - 可能会被用户或系统调整(比如NTP同步)。
适合用来获取「当前日期和时间」:
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::cout << "当前时间: " << std::ctime(&now_c);
23.2.2 steady_clock:单调时钟
steady_clock是单调递增的,永远不会被调整。它最适合用来测量时间间隔。
auto start = std::chrono::steady_clock::now();
// 被测代码
auto end = std::chrono::steady_clock::now();
auto diff = end - start;
23.2.3 high_resolution_clock:高精度时钟
这个时钟通常只是system_clock或steady_clock的别名。在我的经验里,它并不一定比steady_clock精度更高。所以我个人建议:直接使用steady_clock,别纠结。
23.3 duration的转换
不同单位的duration之间可以互相转换。但要注意:从高精度到低精度可能会丢失数据。
auto ms = std::chrono::milliseconds(1500);
auto s = std::chrono::duration_cast<std::chrono::seconds>(ms);
std::cout << s.count() << " 秒\n"; // 输出: 1 秒(截断,不是四舍五入)
如果你想要四舍五入,可以用round:
auto s_round = std::chrono::round<std::chrono::seconds>(ms);
std::cout << s_round.count() << " 秒\n"; // 输出: 2 秒
还有floor和ceil,用法类似。我在做性能统计时经常用round,因为多次测试取平均值后,四舍五入更合理。
duration_cast把纳秒转成毫秒,结果因为截断导致统计结果偏小。后来改用round才解决问题。如果你的场景对精度敏感,记得选对转换方式。
23.4 实战:性能基准测试框架
好了,理论讲完了。我们来写一个实用的性能测试框架。这个框架我一直在用,结构简单但功能完整。
#include <chrono>
#include <iostream>
#include <functional>
#include <string>
class Benchmark {
public:
using Clock = std::chrono::steady_clock;
using Duration = std::chrono::duration<double, std::milli>;
static void run(const std::string& name,
std::function<void()> func,
int iterations = 1000) {
// 预热
func();
auto start = Clock::now();
for (int i = 0; i < iterations; ++i) {
func();
}
auto end = Clock::now();
Duration total = end - start;
double avg = total.count() / iterations;
std::cout << "[" << name << "]\n"
<< " 总耗时: " << total.count() << " ms\n"
<< " 平均耗时: " << avg << " ms\n"
<< " 迭代次数: " << iterations << "\n\n";
}
};
// 使用示例
void test_vector_push() {
std::vector<int> v;
for (int i = 0; i < 1000; ++i) {
v.push_back(i);
}
}
int main() {
Benchmark::run("vector push_back", test_vector_push, 10000);
return 0;
}
这个框架有几个要点:
- 预热:先跑一次,避免冷启动影响结果。
- 多次迭代:单次测试误差太大,跑几千次取平均才可靠。
- 使用steady_clock:保证时间单调递增。
- 输出单位统一:我用毫秒,你也可以改成微秒。
23.5 知识体系总览
下面这张图帮你把本章的核心逻辑串起来:
从图中可以看到,三种时钟都可以产生时间点,时间点相减得到时间段,而时间段之间可以通过各种转换函数进行单位转换。这就是chrono库的全部秘密。
23.6 总结
这一章我们聊了:
- duration:时间段的表示,注意区分tick数和实际时间。
- time_point:时间点,依赖时钟,可以相减。
- 三种时钟:system_clock看日历,steady_clock做测量,high_resolution_clock……嗯,随缘。
- duration转换:用
duration_cast截断,用round四舍五入。 - 性能测试框架:预热、多次迭代、用steady_clock,这三个要点记住了。
说实话,chrono库刚出来那会儿,很多老程序员都不太愿意用。但一旦你习惯了这种类型安全的设计,就再也回不去了。至少我现在写任何和时间打交道的代码,第一反应就是std::chrono。