实施便携式解决方案
正如这里已经提到的,对于时间测量问题没有足够精确的适当的 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 上使用两种备用解决方案:
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,因此您首先需要检查其可用性:
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
IRIX 有clock_gettime 调用,但缺少CLOCK_MONOTONIC。相反,它有自己的单调时钟源,定义为CLOCK_SGI_CYCLE,您应该使用它而不是CLOCK_MONOTONIC 和clock_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)。