测量一段代码的执行时间的常见方法

微信扫一扫,分享到朋友圈

测量一段代码的执行时间的常见方法

本文主要适用于 x86-64 体系结构下的 Linux C/C++ 服务器程序。

程序运行的时候,我们经常需要测量某一段代码的执行时间。最简单的做法,自然就是在代码开始的地方获取当前时间 begin_time,在代码结束的地方获取当前时间 end_time,然后计算 end_time – begin_time 即可。

测量代码执行时间的时候需要考虑以下几个问题:

  1. 代价 – 这可能是一个高频操作,获取时间的代价不能太高。

  2. 精度 – 至少是微秒级别。

  3. 稳定 – 如果使用的时钟抖动误差很大,那测量结果往往是不可靠的。

gettimeofday

gettimeofday(2) [1] 这个函数可以获得微秒级别的时间戳。该函数获得的时间是使用墙上时间 xtime 和 jiffies 处理得到的。

墙上时间其实就是实际时间,从 UTC 1970-01-01 00:00:00 开始算起,它是由主板电池供电的 RTC 芯片存储提供。这样即使机器断电了,时间也不用重设。

jiffies 是 Linux 内核启动后的节拍数,在 x86-64 的体系结构下,Linux 内核的节拍频率是 1000,即一个节拍的时间是 1s / 1000 = 1ms。也就是说,jiffies 这个全局变量存储了操作系统启动以来共经历了多少毫秒。

仅仅靠 xtime 和 jiffies 是无法达到微秒级别的精度的。在 Linux 内核中,高精度定时器 hrtimer 模块也会对 xtime 进行修正的,这个模块能够支持纳秒级别的时间精度。关于 hrtimer,我这里就不多介绍了,有兴趣的可以在网上查阅相关资料。

在 x86-64 的系统中,gettimeofday 不是系统调用,其调用成本和普通的用户态函数基本一致。内核采用了“同时映射一块内存到用户态和内核态,数据由内核态维护,用户态拥有读权限”的方式使得该函数调用不需要陷入内核去获取数据。

clock_gettime

clock_gettime(2) [2] 支持获取纳秒级别的时间戳。同时可以通过参数 clk_id 指定获取的时间类型,主要有:

  1. CLOCK_REALTIME – 墙上时间,即纳秒级精度的墙上时间。作用了 gettimeofday 类似。

  2. CLOCK_MONOTONIC – 从系统启动起开始计时的运行时间。

  3. CLOCK_PROCESS_CPUTIME_ID – 本进程执行到当前代码时系统CPU花费的时间。

  4. CLOCK_THREAD_CPUTIME_ID – 本线程执行到当前代码时系统CPU花费的时间。

rdtsc/rdtscp

rdtsc/rdtscp [3] 是 x86 CPU 的指令,含义是 read TSC(Time Stamp Counter) 寄存器。TSC 寄存器在每个 CPU 时钟信号到来时加 1。所以这个数值的递增速度和 CPU 的主频相关。比如,主频为 1M Hz 的 CPU,这个寄存器每秒就递增 1 000 000 次。服务器 x86-64 的 CPU 主频一般都在 1G Hz 以上,所以通过这个指令,我们可以获得纳秒级别的时间精度。

使用 rdtsc 存在一些问题:

  1. CPU 乱序执行使得指令会影响代码执行时间的测量(乱序执行之后,无法保证 rdtsc 指令的执行一定是在业务代码执行的之前和之后)。这个问题可以用 rdtscp 指令来解决。

  2. CPU的运行频率(降频、超频)可能会变化。这个可以在 /proc/cpuinfo 查看 CPU 的 TSC 相关特性,主要是 constant_tsc 和 nonstop_tsc。简单说,有这两个特性的 CPU 就可以认为 TSC 寄存器的时钟信号频率是不变的。

    1. constant_tsc: TSC ticks at a constant rate.

    2. nonstop_tsc: TSC does not stop in C states.

  3. 无法保证每个 CPU 核心的 TSC 寄存器是同步的。比如,线程在 CPU1 获得了代码开始的 begin tick,中间发生上下文切换,恢复后线程在 CPU2 获得了代码结束的 end tick。此时计算用 end tick – begin tick 计算出来的时间是不准确的。这个问题暂时没有好办法可以解决。根据具体的代码逻辑,这个问题可能没什么影响,也可能导致测量结果不准确。

小结

使用 Quick C++ Benchmark [4] 对上面几种方式进行简单的 benchmark。编译参数:GCC 10.1 -std=c++20 -O3 结果如下:

  1. rdtscp 只需要执行一条 CPU 指令读取寄存器,性能上秒杀其他方式,速度大概是其他方式的 45~50 倍。可惜目前还无法解决不同 CPU 之间的 TSC 寄存器的同步问题。

  2. gettimeofday 的性能略优于 clock_gettime,但是差距不是,大概百分之十。

  3. clock_gettime 的 CLOCK_REALTIME 和 CLOCK_MONOTONIC 性能上几乎一样。

测试代码

可以在 Quick C++ Benchmark [5] 运行。

#include <sys/time.h>
static void GetTimeOfDay(benchmark::State& state) {
for (auto _ : state) {
struct timeval tv;
gettimeofday(&tv, nullptr);
}
}
BENCHMARK(GetTimeOfDay);
static void ClockGetTime_REAL(benchmark::State& state) {
for (auto _ : state) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
}
}
BENCHMARK(ClockGetTime_REAL);
static void ClockGetTime_MONOTONIC(benchmark::State& state) {
for (auto _ : state) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
}
}
BENCHMARK(ClockGetTime_MONOTONIC);
inline uint64_t GetTickCount() {
uint32_t lo, hi;
uint64_t o;
__asm__ __volatile__("rdtscp" : "=a"(lo), "=d"(hi) : : "%ecx");
o = hi;
o <<= 32;
return (o | lo);
}
static void GetTickCount_rdtscp(benchmark::State& state) {
for (auto _ : state) {
uint64_t ticks = GetTickCount();
benchmark::DoNotOptimize(ticks);
}
}
BENCHMARK(GetTickCount_rdtscp);

参考资料

[1]

gettimeofday(2): https://man7.org/linux/man-pages/man2/gettimeofday.2.html

[2]

clock_gettime(2): https://man7.org/linux/man-pages/man2/clock_gettime.2.html

[3]

rdtsc/rdtscp: https://en.wikipedia.org/wiki/Time_Stamp_Counter

[4]

Quick C++ Benchmark: https://quick-bench.com/q/DxnMEl4tUb5bAXRqxI1gSmWvUqM

[5]

Quick C++ Benchmark: https://quick-bench.com/q/DxnMEl4tUb5bAXRqxI1gSmWvUqM

Elasticsearch Top 51 重中之重面试题及答案

上一篇

TiDB 数据库的 4 大应用场景分析

下一篇

你也可能喜欢

测量一段代码的执行时间的常见方法

长按储存图像,分享给朋友