23、chrono时间库:时间点与时间段、时钟类型、duration转换、实战:性能基准测试框架

时间处理,在C++里一直是个让人头疼的事。早些年我还在用clock()gettimeofday()的时候,跨平台兼容性问题没少踩坑。直到C++11引入了chrono库,这才算真正有了一个统一、类型安全的时间处理方案。

说实话,我刚接触chrono时也觉得它有点绕——什么durationtime_pointclock,一堆模板参数看得眼花。但用顺手之后,你会发现它其实非常优雅。今天我们就把它彻底讲透。

23.1 核心概念:时间点与时间段

chrono库的核心就两个东西:时间段(duration)时间点(time_point)。你想想看,时间本质上不就是「某个时刻」和「两个时刻之间的间隔」吗?

23.1.1 duration:时间段的表示

duration是一个模板类,定义在头文件中。它的原型长这样:

template<class Rep, class Period = ratio<1>>
class duration;
  • Rep:存储时间的数值类型,比如intlong longdouble
  • 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::nanosecondsduration<int64_t, nano>纳秒
std::chrono::microsecondsduration<int64_t, micro>微秒
std::chrono::millisecondsduration<int64_t, milli>毫秒
std::chrono::secondsduration<int64_t>
std::chrono::minutesduration<int64_t, ratio<60>>分钟
std::chrono::hoursduration<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是单调递增的,永远不会被调整。它最适合用来测量时间间隔。

核心区别:system_clock可能往回跳(比如你手动改了系统时间),但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_clocksteady_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 秒

还有floorceil,用法类似。我在做性能统计时经常用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时间库核心结构 system_clock steady_clock high_resolution_clock time_point(时间点) duration(时间段) duration_cast / round / floor / ceil 转换

从图中可以看到,三种时钟都可以产生时间点,时间点相减得到时间段,而时间段之间可以通过各种转换函数进行单位转换。这就是chrono库的全部秘密。

23.6 总结

这一章我们聊了:

  • duration:时间段的表示,注意区分tick数和实际时间。
  • time_point:时间点,依赖时钟,可以相减。
  • 三种时钟:system_clock看日历,steady_clock做测量,high_resolution_clock……嗯,随缘。
  • duration转换:用duration_cast截断,用round四舍五入。
  • 性能测试框架:预热、多次迭代、用steady_clock,这三个要点记住了。

说实话,chrono库刚出来那会儿,很多老程序员都不太愿意用。但一旦你习惯了这种类型安全的设计,就再也回不去了。至少我现在写任何和时间打交道的代码,第一反应就是std::chrono


公众号:蓝海资料掘金营,微信deep3321