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

2021-09-25 45点热度 0人点赞 0条评论

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

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

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

  1. 代价 - 这可能是一个高频操作,获取时间的代价不能太高。
  2. 精度 - 至少是微秒级别。
  3. 稳定 - 如果使用的时钟抖动误差很大,那测量结果往往是不可靠的。

gettimeofday

gettimeofday(2) 这个函数可以获得微秒级别的时间戳。该函数获得的时间是使用墙上时间 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) 支持获取纳秒级别的时间戳。同时可以通过参数 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 是 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 对上面几种方式进行简单的 benchmark。 编译参数:GCC 10.1 -std=c++20 -O3 结果如下:

img

  1. rdtscp 只需要执行一条 CPU 指令读取寄存器,性能上秒杀其他方式,速度大概是其他方式的 45~50 倍。可惜目前还无法解决不同 CPU 之间的 TSC 寄存器的同步问题。
  2. gettimeofday 的性能略优于 clock_gettime,但是差距不是,大概百分之十。
  3. clock_gettime 的 CLOCK_REALTIME 和 CLOCK_MONOTONIC 性能上几乎一样。

测试代码

可以在 Quick C++ Benchmark 运行。

```c++
#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;
<strong>asm</strong> <strong>volatile</strong>("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);

```

v
转载 from https://zhuanlan.zhihu.com/p/305173545 by FOCUS

订阅博客,及时获取文章更新邮件通知

close

订阅博客,及时获取文章更新邮件通知

kerbal

这个人很懒,什么都没留下

文章评论

您需要 登录 之后才可以评论