首页 > 解决方案 > 使用 C 时间函数来测量时间:它们是否抗代码重新排序?

问题描述

当我添加一段代码来测量 CPU 执行时间时,例如:

int main()
{
   clock_t init_time = clock(); // (1)
   your_fun(); // (2)
   printf("Seconds: %f", (double)(clock() - init_time) / CLOCKS_PER_SEC); // (3)
}

如果我以完全优化的方式编译那段代码,是否可以对代码进行重新排序,以便在init_time执行初始化之后执行your_fun任何操作?换句话说,是否clock有任何内存屏障机制来保护测量块不被重新排序?

万一clock不抗重新排序:如果我将 (1) 和 (3) 移动到在不同编译单元中实现的新函数,因此编译器在编译时main看不到它们内部的内容,是否可以防止这种重新排序?

我有这个问题有一段时间了,因为我通常会在等待执行的时间(秒)与打印的执行时间(毫秒甚至 0)之间看到相互矛盾的执行时间。

C++ 时钟或本地时间函数呢?他们有类似的问题吗?

标签: c++cperformancetimelanguage-lawyer

解决方案


clock()call 不会为您提供程序使用的 cpu 时间。它为您提供自启动以来经过的滴答数(或最后一次环绕,如果您的正常运行时间较长),这是在旧的遗留 unix 中获得亚秒级计时的唯一方法。但这不是处理时间,而是挂钟时间。

为了说明这一点,我将引用FreeBSD 13.0-STABLEclock(3)发行版手册页中的一段话:

clock()功能符合ISO/IEC 9899:1990(“ISO C90”)。 但是,单一 UNIX 规范(“SUSv2”)的第 2 版需要CLOCKS_PER_SEC定义为一百万。 FreeBSD不符合这个要求;更改该值将引入二进制不兼容,并且一百万在现代处理器上仍然不足。

今天,您可以使用广泛使用的gettimeofday()系统调用(也称为挂钟),它可以为您提供从 unix 时代开始的一天中的时间,具有微秒级分辨率(比时钟调用的滴答声分辨率高得多,而且您不需要需要了解CLOCKS_PER_SEC常量),并且此调用是当今最便携的方式,因为几乎所有可用的 unice 都实现了它,或者更好的是,更新的 POSIX 系统调用clock_gettime(2)和朋友,它允许您使用纳秒分辨率(如果可用)和允许你选择一个进程运行时钟(一个会给你你的 cpu 时间,而不是挂钟时间)

最后一个系统调用并非在任何地方都可用,但如果您的系统声称是 posix,那么您可能会指定时钟的子集。

       int clock_gettime(clockid_t clk_id, struct timespec *tp);

在哪里

struct timespec

充满了参考的时钟时间clk_id

struct timespec结构的字段是:

  • tv_sectime_t表示自 unix 纪元(01/01/1970 00:00:00 UTC 时间)以来的秒数的字段
  • tv_nsec一个long值,表示自上一秒滴答以来的纳秒数。它的范围从 0 到 999999999。

不同时钟 id 的值取自 linux 在线手册(Ubuntu 版本) 未标记为 linux 特定的是 POSIX,因此它们的 id 应该是可移植的(尽管可能不如 linux 实现的精确或快速核心)

  • CLOCK_REALTIME挂钟。
  • CLOCK_REALTIME_COARSE(特定于linux)比以前更快,但不太精确。
  • CLOCK_MONOTONIC 挂钟,但确保不同的呼叫总是会给你上升时间(不管这意味着什么)。正如clock_gettime(2)手册页所说,即使您使系统时钟向后调整到主时钟,它也会增长。
  • CLOCK_MONOTONIC_COARSE(Linux 特定)与上述类似,但单调。
  • CLOCK_MONOTONIC_RAW(特定于 Linux)
  • CLOCK_BOOTTIME(特定于 linux)类似于您使用的时钟,但在 ns 而不是时钟滴答声中。
  • CLOCK_PROCESS_CPUTIME_ID整个进程使用的(特定于 linux 的)进程时间(不是挂钟时间)。
  • CLOCK_THREAD_CPUTIME_ID(特定于linux)线程时间(不是挂钟时间)这是您的线程使用cpu的时间,这是我认为您必须阅读的时钟。

所以,最后你的片段可以得到:

   struct timespec t0, t1;

   int res = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t0);
   // check errors from res.
   your_fun();
   int res = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t1);
   // we'll subtract t0 from t1, so we get the delay in t1.
   if (t1.tv_nsec < t0.tv_nsec) { // carry to the seconds part.
      t1.tv_nsec += 1000000000L - t0.tv_nsec;
      t1.tv_sec--;
   } else {
      t1.tv_nsec -= t0.tv_nsec;
   }
   t1.tv_sec -= t0.tv_sec;
   // no need to convert to double or do floating point arithmetic.
   printf("Seconds: %d.%09d\n", t1.tv_sec, t1.tv_nsec);

您可以将这些调用包装到一个回调函数中,从而为您提供执行时间:

struct timespec *time_wrapper(
        void (*callback)(), // function to be called.
        struct timespec *work) // provide a working space so you don't need to allocate it.
{
    struct timespec t0;
    int res = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t0);
    // check errors from res.
    if (res < 0) return NULL; // check errno.
    callback();
    int res = clock_gettime(CLOCK_THREAD_CPUTIME_ID, work);
    if (res < 0) return NULL; // check errno
    // we'll subtract t0 from *work, so we get the delay in *work.
    if (work->tv_nsec < t0.tv_nsec) { // carry to the seconds part.
        work->tv_nsec += 1000000000L - t0.tv_nsec;
        work->tv_sec--;
    } else {
        work->tv_nsec -= t0.tv_nsec;
    }
    work->tv_sec -= t0.tv_sec;
    return work;
}

您可以将其称为:

#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
   struct timespec delay;

   if (time_wrapper(your_fun, &delay) == NULL) { // some error
      fprintf(stderr, "Error: %s\n", strerror(errno));
      exit(EXIT_FAILURE);
   }

   printf("CPU Seconds: %lu.%09lu",
          (unsigned long) delay.tv_sec,
          (unsigned long) delay.tv_nsec);

   exit(EXIT_SUCCESS);
}

最后一点:编译器优化不能以使您在语句中施加的顺序与您期望的不同的方式对代码进行重新排序。您的代码可能受到优化影响的唯一方式是您在程序中发生了一些未定义的行为,这意味着您的代码不正确。

如果您的代码是正确的,那么编译器中的优化不会影响它。


推荐阅读