【问题标题】:How to measure time in milliseconds using ANSI C?如何使用 ANSI C 以毫秒为单位测量时间?
【发布时间】:2010-09-26 13:22:39
【问题描述】:

仅使用 ANSI C,有没有办法以毫秒或更高的精度测量时间?我正在浏览 time.h 但我只找到了秒精度函数。

【问题讨论】:

  • 注意精度和准确度的区别。您可以通过将时间以秒为单位并乘以 1000 来获得毫秒 precision 的时间,但这没有用。 ms 精度函数不一定具有 ms 精度 - 尽管它们通常比 1s 精度更好。
  • 简单的答案是否定的,ANSI C 不支持毫秒精度或更高的精度。更复杂的答案取决于您要做什么 - 坦率地说,整个区域都是一场噩梦 - 即使您允许使用广泛可用的 Posix 功能。您使用“测量”一词,因此我假设您对间隔而不是“挂钟”时间感兴趣。但是您是否尝试测量您的进程的绝对时间段或 CPU 使用率?
  • 只是想对 SOF 说刚刚救了我的培根 ;-)

标签: c portability time-precision


【解决方案1】:

没有提供优于 1 秒时间分辨率的 ANSI C 函数,但 POSIX 函数gettimeofday 提供微秒分辨率。时钟函数仅测量进程执行所花费的时间量,并且在许多系统上并不准确。

你可以这样使用这个函数:

struct timeval tval_before, tval_after, tval_result;

gettimeofday(&tval_before, NULL);

// Some code you want to time, for example:
sleep(1);

gettimeofday(&tval_after, NULL);

timersub(&tval_after, &tval_before, &tval_result);

printf("Time elapsed: %ld.%06ld\n", (long int)tval_result.tv_sec, (long int)tval_result.tv_usec);

这会在我的机器上返回 Time elapsed: 1.000870

【讨论】:

  • 轻微警告:gettimeofday() 不是单调的,这意味着它可以跳来跳去(甚至倒退),例如,如果您的机器试图与网络时间服务器或其他时间保持同步来源。
  • 准确地说:在 ISO C99(我认为这部分与 ANSI C 兼容)中,甚至不能保证 any 时间分辨率。 (ISO C99, 7.23.1p4)
  • 值得注意的是timeval::tv_usec总是不到一秒,它在循环。 IE。为了使时间差大于 1 秒,您应该:long usec_diff = (e.tv_sec - s.tv_sec)*1000000 + (e.tv_usec - s.tv_usec);
  • @Dipstick:但请注意,例如NTP 永远不会让你的时钟向后移动,除非你明确告诉它这样做。
  • @AlexanderMalakhov 时间减法逻辑封装在 timersub 函数中。我们可以按原样使用tval_result 值(tv_sec 和 tv_usec)。
【解决方案2】:

您可能获得的最佳精度是通过使用仅 x86 的“rdtsc”指令,它可以提供时钟级别的分辨率(当然必须考虑 rdtsc 调用本身的成本,这可以是在应用程序启动时轻松测量)。

这里的主要问题是测量每秒的时钟数,这应该不会太难。

【讨论】:

  • 您可能还需要关注处理器关联,因为在某些机器上,您可能会将 RDTSC 调用发送到多个处理器,并且它们的 RDTSC 计数器可能不会同步。
  • 此外,一些处理器没有单调递增的 TSC - 考虑降低 CPU 频率的节能模式。将 RDTSC 用于非常短的本地化时间是一个非常的坏主意。
  • 顺便说一句,@WillDean 提到的核心漂移和使用 rdtsc 计时是许多游戏无法在(早期?)多核 AMD64 CPU 上运行的原因 - 我不得不限制为单核 -对我的 x2 4400+ 的一些标题的核心亲和力。
【解决方案3】:
#include <time.h>
clock_t uptime = clock() / (CLOCKS_PER_SEC / 1000);

【讨论】:

  • 因为它是每秒时钟数,所以它的值无关紧要,clock() / CLOCKS_PER_SEC 的结果值将以秒为单位(至少应该如此)。除以 1000 会将其转换为毫秒。
  • 根据 C 参考手册,clock_t 值可以从大约 36 分钟开始环绕。如果您要测量长时间的计算,则需要注意这一点。
  • 还要注意整数除法 CLOCKS_PER_SEC / 1000 可能不精确,这可能会影响最终结果(尽管根据我的经验,CLOCKS_PER_SEC 一直是 1000 的倍数)。执行(1000 * clock()) / CLOCKS_PER_SEC 不太容易受到除法不精确的影响,但另一方面更容易出现溢出。只是一些需要考虑的问题。
  • 这不是测量 cpu 时间而不是 wall 时间吗?
  • clock() measures Windows 上的挂墙时间和大多数其他流行操作系统上的 CPU 时间。
【解决方案4】:

我总是使用clock_gettime() 函数,从CLOCK_MONOTONIC 时钟返回时间。返回的时间是从过去某个未指定时间点(例如纪元的系统启动)开始的时间量,以秒和纳秒为单位。

#include <stdio.h>
#include <stdint.h>
#include <time.h>

int64_t timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p)
{
  return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) -
           ((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec);
}

int main(int argc, char **argv)
{
  struct timespec start, end;
  clock_gettime(CLOCK_MONOTONIC, &start);

  // Some code I am interested in measuring 

  clock_gettime(CLOCK_MONOTONIC, &end);

  uint64_t timeElapsed = timespecDiff(&end, &start);
}

【讨论】:

  • clock_gettime() 不是 ANSI C。
  • 另外 CLOCK_MONOTONIC 并没有在很多系统(包括很多 Linux 平台)上实现。
  • @PowerApp101 没有好的/强大的 ANSI C 方法可以做到这一点。许多其他答案依赖于 POSIX 而不是 ANCI C。话虽如此,我相信今天。 @Dipstick 今天,我相信大多数现代平台[需要引用] 支持 clock_gettime(CLOCK_MONOTONIC, ...),甚至还有功能测试宏 _POSIX_MONOTONIC_CLOCK
【解决方案5】:

在窗户下:

SYSTEMTIME t;
GetLocalTime(&t);
swprintf_s(buff, L"[%02d:%02d:%02d:%d]\t", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds);

【讨论】:

  • 这是按要求提供的 ansi C 吗?
【解决方案6】:

timespec_get 来自 C11

最多返回纳秒,四舍五入到实现的分辨率。

看起来像是 POSIX'clock_gettime 的 ANSI 盗版。

示例:printf 在 Ubuntu 15.10 上每 100 毫秒完成一次:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static long get_nanos(void) {
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    return (long)ts.tv_sec * 1000000000L + ts.tv_nsec;
}

int main(void) {
    long nanos;
    long last_nanos;
    long start;
    nanos = get_nanos();
    last_nanos = nanos;
    start = nanos;
    while (1) {
        nanos = get_nanos();
        if (nanos - last_nanos > 100000000L) {
            printf("current nanos: %ld\n", nanos - start);
            last_nanos = nanos;
        }
    }
    return EXIT_SUCCESS;
}

C11 N1570 standard draft 7.27.2.5 “timespec_get 函数说”:

如果 base 是 TIME_UTC,则 tv_sec 成员设置为自 实现定义的纪元,截断为一个完整的值,tv_nsec 成员是 设置为整数纳秒,四舍五入为系统时钟的分辨率。 (321)

321) 虽然 struct timespec 对象以纳秒分辨率描述时间,但可用的 分辨率取决于系统,甚至可能大于 1 秒。

C++11 也得到了std::chrono::high_resolution_clock:C++ Cross-Platform High-Resolution Timer

glibc 2.21 实现

可以在sysdeps/posix/timespec_get.c下找到:

int
timespec_get (struct timespec *ts, int base)
{
  switch (base)
    {
    case TIME_UTC:
      if (__clock_gettime (CLOCK_REALTIME, ts) < 0)
        return 0;
      break;

    default:
      return 0;
    }

  return base;
}

很清楚:

【讨论】:

  • 这是2017年的正确答案,连MSVC都有这个功能;在基准测试方面,寻找读取芯片寄存器的东西(具有 PT 扩展的 x86 处理器的较新版本,以及相应的 Linux 内核/perf 的较新版本)
【解决方案7】:

实施便携式解决方案

正如这里已经提到的,对于时间测量问题没有足够精确的适当的 ANSI 解决方案,我想写一下如何获得便携式的方法,如果可能的话,还有一个高分辨率的时间测量解决方案。

单调时钟与时间戳

一般来说时间测量有两种方式:

  • 单调时钟;
  • 当前(日期)时间戳。

第一个使用单调时钟计数器(有时称为滴答计数器),它以预定义的频率对滴答进行计数,因此如果您有滴答值并且频率已知,则可以轻松地将滴答转换为经过的时间。实际上并不能保证单调时钟以任何方式反映当前系统时间,它也可能会计算自系统启动以来的滴答声。但它保证无论系统状态如何,时钟始终以递增的方式运行。通常频率与硬件高分辨率源绑定,这就是它提供高精度的原因(取决于硬件,但大多数现代硬件对高分辨率时钟源没有问题)。

第二种方式提供基于当前系统时钟值的(日期)时间值。它也可能具有较高的分辨率,但它有一个主要缺点:这种时间值会受到不同的系统时间调整的影响,即时区更改、夏令时 (DST) 更改、NTP 服务器更新、系统休眠等在。在某些情况下,您可以获得负的经过时间值,这可能导致未定义的行为。其实这种时间源不如第一种可靠。

因此,时间间隔测量的第一条规则是尽可能使用单调时钟。它通常具有较高的精度,并且设计可靠。

后备策略

在实施便携式解决方案时,值得考虑一种备用策略:如果可用,则使用单调时钟;如果系统中没有单调时钟,则使用时间戳方法。

Windows

在 MSDN 上有一篇名为 Acquiring high-resolution time stamps 的精彩文章,介绍了 Windows 上的时间测量,其中描述了您可能需要了解的有关软件和硬件支持的所有详细信息。要在 Windows 上获取高精度时间戳,您应该:

  • 使用QueryPerformanceFrequency查询计时器频率(每秒滴答数):

    LARGE_INTEGER tcounter;
    LARGE_INTEGER freq;    
    
    if (QueryPerformanceFrequency (&tcounter) != 0)
        freq = tcounter.QuadPart;
    

    定时器频率在系统启动时是固定的,因此您只需获取一次。

  • 使用QueryPerformanceCounter查询当前刻度值:

    LARGE_INTEGER tcounter;
    LARGE_INTEGER tick_value;
    
    if (QueryPerformanceCounter (&tcounter) != 0)
        tick_value = tcounter.QuadPart;
    
  • 将刻度缩放到经过的时间,即微秒:

    LARGE_INTEGER usecs = (tick_value - prev_tick_value) / (freq / 1000000);
    

根据 Microsoft 的说法,在大多数情况下,在 Windows XP 和更高版本上使用这种方法应该不会有任何问题。但您也可以在 Windows 上使用两种备用解决方案:

  • GetTickCount 提供自系统启动以来经过的毫秒数。它每 49.7 天包装一次,因此在测量较长的间隔时要小心。
  • GetTickCount64GetTickCount 的 64 位版本,但从 Windows Vista 及更高版本开始提供。

OS X (macOS)

OS X (macOS) 有自己的马赫绝对时间单位,代表单调时钟。最好的开始方法是 Apple 的文章 Technical Q&A QA1398: Mach Absolute Time Units,它描述了(带有代码示例)如何使用特定于 Mach 的 API 来获取单调刻度。还有一个关于它的本地问题,称为clock_gettime alternative in Mac OS X,最后可能会让您有点困惑如何处理可能的值溢出,因为计数器频率以分子和分母的形式使用。因此,一个简短的示例如何获取经过的时间:

  • 获取时钟频率分子和分母:

    #include <mach/mach_time.h>
    #include <stdint.h>
    
    static uint64_t freq_num   = 0;
    static uint64_t freq_denom = 0;
    
    void init_clock_frequency ()
    {
        mach_timebase_info_data_t tb;
    
        if (mach_timebase_info (&tb) == KERN_SUCCESS && tb.denom != 0) {
            freq_num   = (uint64_t) tb.numer;
            freq_denom = (uint64_t) tb.denom;
        }
    }
    

    你只需要这样做一次。

  • 使用mach_absolute_time查询当前刻度值:

    uint64_t tick_value = mach_absolute_time ();
    
  • 使用先前查询的分子和分母将刻度缩放到经过的时间,即微秒:

    uint64_t value_diff = tick_value - prev_tick_value;
    
    /* To prevent overflow */
    value_diff /= 1000;
    
    value_diff *= freq_num;
    value_diff /= freq_denom;
    

    防止溢出的主要想法是在使用分子和分母之前将刻度按比例缩小到所需的精度。由于初始计时器分辨率以纳秒为单位,我们将其除以1000 以获得微秒。您可以在 Chromium 的 time_mac.c 中找到相同的方法。如果您确实需要纳秒级精度,请考虑阅读How can I use mach_absolute_time without overflowing?

Linux 和 UNIX

clock_gettime 调用是您在任何 POSIX 友好系统上的最佳方式。它可以从不同的时钟源查询时间,我们需要的是CLOCK_MONOTONIC。并非所有具有clock_gettime 的系统都支持CLOCK_MONOTONIC,因此您首先需要检查其可用性:

  • 如果 _POSIX_MONOTONIC_CLOCK 定义为值 &gt;= 0,则表示 CLOCK_MONOTONIC 可用;
  • 如果_POSIX_MONOTONIC_CLOCK被定义为0,则意味着你应该额外检查它是否在运行时工作,我建议使用sysconf

    #include <unistd.h>
    
    #ifdef _SC_MONOTONIC_CLOCK
    if (sysconf (_SC_MONOTONIC_CLOCK) > 0) {
        /* A monotonic clock presents */
    }
    #endif
    
  • 否则不支持单调时钟,您应该使用后备策略(见下文)。

clock_gettime 的用法非常简单:

  • 获取时间值:

    #include <time.h>
    #include <sys/time.h>
    #include <stdint.h>
    
    uint64_t get_posix_clock_time ()
    {
        struct timespec ts;
    
        if (clock_gettime (CLOCK_MONOTONIC, &ts) == 0)
            return (uint64_t) (ts.tv_sec * 1000000 + ts.tv_nsec / 1000);
        else
            return 0;
    }
    

    我在这里将时间缩减到微秒。

  • 用同样的方法计算与上一次收到的时间值的差:

    uint64_t prev_time_value, time_value;
    uint64_t time_diff;
    
    /* Initial time */
    prev_time_value = get_posix_clock_time ();
    
    /* Do some work here */
    
    /* Final time */
    time_value = get_posix_clock_time ();
    
    /* Time difference */
    time_diff = time_value - prev_time_value;
    

最好的后备策略是使用gettimeofday 调用:它不是单调的,但它提供了相当好的分辨率。这个想法与clock_gettime 相同,但要获得时间值,您应该:

#include <time.h>
#include <sys/time.h>
#include <stdint.h>

uint64_t get_gtod_clock_time ()
{
    struct timeval tv;

    if (gettimeofday (&tv, NULL) == 0)
        return (uint64_t) (tv.tv_sec * 1000000 + tv.tv_usec);
    else
        return 0;
}

同样,时间值缩小到微秒。

SGI IRIX

IRIXclock_gettime 调用,但缺少CLOCK_MONOTONIC。相反,它有自己的单调时钟源,定义为CLOCK_SGI_CYCLE,您应该使用它而不是CLOCK_MONOTONICclock_gettime

Solaris 和 HP-UX

Solaris 有自己的高分辨率计时器接口gethrtime,它以纳秒为单位返回当前计时器值。虽然较新版本的 Solaris 可能有 clock_gettime,但如果您需要支持旧的 Solaris 版本,您可以坚持使用 gethrtime

用法很简单:

#include <sys/time.h>

void time_measure_example ()
{
    hrtime_t prev_time_value, time_value;
    hrtime_t time_diff;

    /* Initial time */
    prev_time_value = gethrtime ();

    /* Do some work here */

    /* Final time */
    time_value = gethrtime ();

    /* Time difference */
    time_diff = time_value - prev_time_value;
}

HP-UX 缺少 clock_gettime,但它支持 gethrtime,您应该像在 Solaris 上一样使用它。

BeOS

BeOS 也有自己的高分辨率计时器接口system_time,它返回自计算机启动以来经过的微秒数。

示例用法:

#include <kernel/OS.h>

void time_measure_example ()
{
    bigtime_t prev_time_value, time_value;
    bigtime_t time_diff;

    /* Initial time */
    prev_time_value = system_time ();

    /* Do some work here */

    /* Final time */
    time_value = system_time ();

    /* Time difference */
    time_diff = time_value - prev_time_value;
}

操作系统/2

OS/2 有自己的 API 来检索高精度时间戳:

  • 使用DosTmrQueryFreq(用于 GCC 编译器)查询计时器频率(每单位滴答数):

    #define INCL_DOSPROFILE
    #define INCL_DOSERRORS
    #include <os2.h>
    #include <stdint.h>
    
    ULONG freq;
    
    DosTmrQueryFreq (&freq);
    
  • 使用DosTmrQueryTime查询当前刻度值:

    QWORD    tcounter;
    unit64_t time_low;
    unit64_t time_high;
    unit64_t timestamp;
    
    if (DosTmrQueryTime (&tcounter) == NO_ERROR) {
        time_low  = (unit64_t) tcounter.ulLo;
        time_high = (unit64_t) tcounter.ulHi;
    
        timestamp = (time_high << 32) | time_low;
    }
    
  • 将刻度缩放到经过的时间,即微秒:

    uint64_t usecs = (prev_timestamp - timestamp) / (freq / 1000000);
    

示例实现

您可以查看实现上述所有策略的plibsys 库(有关详细信息,请参阅 ptimeprofiler*.c)。

【讨论】:

  • “对于时间测量问题没有足够精确的适当的ANSI解决方案”:有C11 timespec_get:stackoverflow.com/a/36095407/895245
  • 这仍然是衡量代码执行时间的错误方法。 timespec_get 不是单调的。
【解决方案8】:

接受的答案已经足够了。但是我的解决方案更简单。我只是在Linux中测试,使用gcc(Ubuntu 7.2.0-8ubuntu3.2)7.2.0。

也使用gettimeofdaytv_sec 是秒的一部分,tv_usec微秒,而不是毫秒

long currentTimeMillis() {
  struct timeval time;
  gettimeofday(&time, NULL);

  return time.tv_sec * 1000 + time.tv_usec / 1000;
}

int main() {
  printf("%ld\n", currentTimeMillis());
  // wait 1 second
  sleep(1);
  printf("%ld\n", currentTimeMillis());
  return 0;
 }

打印出来:

1522139691342
1522139692342,正好一秒钟。
^

【讨论】:

    猜你喜欢
    • 2011-12-26
    • 1970-01-01
    • 2011-04-30
    • 2012-04-28
    • 2021-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多