簡體   English   中英

如何使用 ANSI C 以毫秒為單位測量時間?

[英]How to measure time in milliseconds using ANSI C?

僅使用 ANSI C,有沒有辦法以毫秒或更高的精度測量時間? 我正在瀏覽 time.h,但我只找到了二階精度函數。

沒有提供優於 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在我的機器上。

#include <time.h>
clock_t uptime = clock() / (CLOCKS_PER_SEC / 1000);

我總是使用clock_gettime() 函數,從CLOCK_MONOTONIC 時鍾返回時間。 返回的時間是從過去某個未指定點(例如 epoch 的系統啟動)開始的時間量,以秒和納秒為單位。

#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);
}

實施便攜式解決方案

正如這里已經提到的那樣,對於時間測量問題沒有足夠精確的適當 ANSI 解決方案,我想寫下如何獲得便攜式和高分辨率時間測量解決方案的方法,如果可能的話。

單調時鍾與時間戳

一般而言,時間測量有兩種方式:

  • 單調時鍾;
  • 當前(日期)時間戳。

第一個使用單調時鍾計數器(有時稱為滴答計數器),它以預定義的頻率計算滴答,因此如果您有滴答值並且頻率已知,您可以輕松地將滴答轉換為經過的時間。 實際上並不能保證單調時鍾以任何方式反映當前系統時間,它也可能計算自系統啟動以來的滴答數。 但它保證無論系統狀態如何,時鍾始終以遞增的方式運行。 通常頻率與硬件高分辨率源綁定,這就是它提供高精度的原因(取決於硬件,但大多數現代硬件對高分辨率時鍾源沒有問題)。

第二種方式提供基於當前系統時鍾值的(日期)時間值。 它也可能具有高分辨率,但它有一個主要缺點:這種時間值會受到不同系統時間調整的影響,即時區更改、夏令時 (DST) 更改、NTP 服務器更新、系統休眠等上。 在某些情況下,您可能會得到一個負的經過時間值,這可能會導致未定義的行為。 實際上這種時間源不如第一種可靠。

所以時間間隔測量的第一條規則是盡可能使用單調時鍾。 它通常具有很高的精度,並且在設計上是可靠的。

回退策略

在實現便攜式解決方案時,值得考慮回退策略:如果可用,請使用單調時鍾,如果系統中沒有單調時鍾,則回退到時間戳方法。

窗戶

在 MSDN 上有一篇名為Acquiring high-resolution time stamps on 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);

根據微軟的說法,在大多數情況下,這種方法在 Windows XP 和更高版本上應該不會有任何問題。 但您也可以在 Windows 上使用兩種后備解決方案:

  • GetTickCount提供自系統啟動以來經過的毫秒數。 它每 49.7 天包裝一次,因此在測量更長的間隔時要小心。
  • GetTickCount64是64位版本的GetTickCount ,但它是從Windows Vista及更高版本開始。

OS X (macOS)

OS X (macOS) 有自己的馬赫絕對時間單位,代表單調時鍾。 最好的開始方法是 Apple 的文章Technical Q&A QA1398: Mach Absolute Time Units ,它描述了(帶有代碼示例)如何使用 Mach 特定的 API 來獲取單調滴答聲。 還有一個關於它的本地問題, 在 Mac OS X 中稱為clock_gettime 替代方案,最后可能會讓您有點困惑如何處理可能的值溢出,因為計數器頻率以分子和分母的形式使用。 因此,一個如何獲得經過時間的簡短示例:

  • 獲取時鍾頻率分子和分母:

     #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 中使用的相同方法。 如果您真的需要納秒精度,請考慮閱讀如何使用 mach_absolute_time 而不會溢出? .

Linux 和 UNIX

在任何 POSIX 友好系統上, clock_gettime調用是您最好的方法。 它可以從不同的時鍾源查詢時間,我們需要的是CLOCK_MONOTONIC 並非所有具有clock_gettime系統都支持CLOCK_MONOTONIC ,因此您需要做的第一件事是檢查其可用性:

  • 如果_POSIX_MONOTONIC_CLOCK定義為>= 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_gettimegethrtime如果您需要支持舊的 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也有自己的高分辨率計時器接口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)。

來自 C11 的timespec_get

返回最多納秒,四舍五入到實現的分辨率。

看起來像是來自 POSIX 的clock_gettime ANSI ripoff。

示例:在 Ubuntu 15.10 上每 100 毫秒執行一次printf

#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 標准草案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;
}

這么清楚:

  • 目前僅支持TIME_UTC

  • 它轉發到__clock_gettime (CLOCK_REALTIME, ts) ,這是一個POSIX API: http : __clock_gettime (CLOCK_REALTIME, ts)

    Linux x86-64 有一個clock_gettime系統調用。

    請注意,這不是防故障的微基准測試方法,因為:

    • man clock_gettime表示如果您在程序運行時更改某些系統時間設置,則此度量可能會不連續。 這當然應該是一個罕見的事件,您可能可以忽略它。

    • 這會測量牆上時間,因此如果調度程序決定忘記您的任務,它似乎會運行更長時間。

    由於這些原因, getrusage()可能是更好的 POSIX 基准測試工具,盡管它的微秒最大精度較低。

    更多信息請訪問: 在 Linux 中測量時間 - 時間 vs 時鍾 vs getrusage vs clock_gettime vs gettimeofday vs timespec_get?

您可能獲得的最佳精度是通過使用僅 x86 的“rdtsc”指令,該指令可以提供時鍾級分辨率(當然必須考慮 rdtsc 調用本身的成本,這可以在應用程序啟動)。

這里的主要問題是測量每秒的時鍾數,這應該不會太難。

接受的答案已經足夠了。但我的解決方案更簡單。我只是在 Linux 中測試,使用 gcc (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0。

ALSE使用gettimeofday ,所述tv_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 ,正好一秒鍾。
^

從 ANSI/ISO C11 或更高版本開始,您可以使用timespec_get()獲取毫秒、微秒或納秒時間戳,如下所示:

#include <time.h>

/// Convert seconds to milliseconds
#define SEC_TO_MS(sec) ((sec)*1000)
/// Convert seconds to microseconds
#define SEC_TO_US(sec) ((sec)*1000000)
/// Convert seconds to nanoseconds
#define SEC_TO_NS(sec) ((sec)*1000000000)

/// Convert nanoseconds to seconds
#define NS_TO_SEC(ns)   ((ns)/1000000000)
/// Convert nanoseconds to milliseconds
#define NS_TO_MS(ns)    ((ns)/1000000)
/// Convert nanoseconds to microseconds
#define NS_TO_US(ns)    ((ns)/1000)

/// Get a time stamp in milliseconds.
uint64_t millis()
{
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    uint64_t ms = SEC_TO_MS((uint64_t)ts.tv_sec) + NS_TO_MS((uint64_t)ts.tv_nsec);
    return ms;
}

/// Get a time stamp in microseconds.
uint64_t micros()
{
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    uint64_t us = SEC_TO_US((uint64_t)ts.tv_sec) + NS_TO_US((uint64_t)ts.tv_nsec);
    return us;
}

/// Get a time stamp in nanoseconds.
uint64_t nanos()
{
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    uint64_t ns = SEC_TO_NS((uint64_t)ts.tv_sec) + (uint64_t)ts.tv_nsec;
    return ns;
}

// NB: for all 3 timestamp functions above: gcc defines the type of the internal
// `tv_sec` seconds value inside the `struct timespec`, which is used
// internally in these functions, as a signed `long int`. For architectures
// where `long int` is 64 bits, that means it will have undefined
// (signed) overflow in 2^64 sec = 5.8455 x 10^11 years. For architectures
// where this type is 32 bits, it will occur in 2^32 sec = 136 years. If the
// implementation-defined epoch for the timespec is 1970, then your program
// could have undefined behavior signed time rollover in as little as
// 136 years - (year 2021 - year 1970) = 136 - 51 = 85 years. If the epoch
// was 1900 then it could be as short as 136 - (2021 - 1900) = 136 - 121 =
// 15 years. Hopefully your program won't need to run that long. :). To see,
// by inspection, what your system's epoch is, simply print out a timestamp and
// calculate how far back a timestamp of 0 would have occurred. Ex: convert
// the timestamp to years and subtract that number of years from the present
// year.

有關我的更全面的答案,包括我編寫的整個計時庫,請參見此處: How to get a simple timestamp in C

@Ciro Santilli Путлер 還在此處展示了 C11 的timespec_get() function 的簡明演示,這是我第一次學習如何使用 function 的方式。

我更詳盡的回答中,我解釋說在我的系統上,可能的最佳分辨率是~20ns ,但該分辨率取決於硬件,並且可能因系統而異。

在窗戶下:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM