介绍 来自百度
我们在衡量一个函数运行时间,或者判断一个算法的时间效率,或者在程序中我们需要一个定时器,定时执行一个特定的操作,比如在多媒
体中,比如在游戏中等,都会用到时间函数。还比如我们通过记录函数或者算法开始和截至的时间,然后利用两者之差得出函数或者算法的运行时间
。编译器和操作系统为我们提供了很多时间函数,这些时间函数的精度也是各不相同的,所以,如果我们想得到准确的结果,必须使用合适的时间函
数。现在介绍 windows 下的几种常用时间函数。
1 : Sleep 函数
使用: sleep(1000) ,在 Windows 和 Linux 下 1000 代表的含义并不相同, Windows 下的表示 1000 毫秒,也就是 1 秒钟;
Linux 下表示 1000 秒, Linux 下使用毫秒级别的函数可以使用 usleep 。
原理: sleep 函数是使调用 sleep 函数的线程休眠,线程主动放弃时间片。当经过指定的时间间隔后,再启动线程,继续执行代码。
Sleep 函数并不能起到定时的作用,主要作用是延时。在一些多线程中可能会看到 sleep(0); 其主要目的是让出时间片。
精度: sleep 函数的精度非常低,当系统越忙它精度也就越低,有时候我们休眠 1 秒,可能 3 秒后才能继续执行。它的精度取决于线程自身优先级、
其他线程的优先级,以及线程的数量等因素。
2 : MFC 下的 timer 事件
使用:
1. 调用函数 SetTimer() 设置定时间隔,如 SetTimer(0,100,NULL) 即为设置 100 毫秒的时间间隔;
2. 在应用程序中增加定时响应函数 OnTimer() ,并在该函数中添加响应的处理语句,用来完成时间到时的操作。
原理:同 sleep 函数一样。不同的是 timer 是一个定时器,可以指定回调函数,默认为 OnTimer() 函数。
精度: timer 事件的精度范围在毫秒级别,系统越忙其精度也就越差。
3 : C 语言下的 Time
使用: time_t t;time(&t);Time 函数是获取当前时间。
原理: time 函数主要用于获取当前时间,比如我们做一个电子时钟程序,就可以使用此函数,获取系统当前的时间。
精度:秒级别
4 : COM 对象中的 COleDateTime , COleDateTimeSpan 类
使用:
COleDateTime start_time = COleDateTime::GetCurrentTime();
COleDateTimeSpan end_time = COleDateTime::GetCurrentTime()-start_time;
While(end_time.GetTotalSeconds() < 2)
{
// 处理延时或定时期间能处理其他的消息
DoSomething()
end_time = COleDateTime::GetCurrentTime-start_time;
}
原理:以上代表延时 2 秒,而这两秒内我们可以循环调用 DoSomething() ,从而实现在延时的时候我们也能够处理其他的函数,或者消息。
COleDateTime,COleDateTimeSpan 是 MFC 中 CTime , CTimeSpan 在 COM 中的应用,所以,上面的方法对于 CTime , CTimeSpa 同样有效
精度:秒级别
5 : C 语言下的时钟周期 clock()
使用: clock_t start = clock();
Sleep(100);
clock_t end = clock();
double d = (double)(start - end) / CLOCKS_PER_SEC;
原理: clock() 是获取计算机启动后的时间间隔。
精度: ms 级别,对于短时间内的定时或者延时可以达到 ms 级别,对于时间比较长的定时或者延迟精度还是不够。
在 windows 下 CLOCKS_PER_SEC 为 1000 。
6 : Windows 下的 GetTickCount()
使用:
DWORD start = GetTickCount();
Sleep(100);
DWORD end = GetTickCount();
原理:
GetTickCount() 是获取系统启动后的时间间隔。通过进入函数开始定时,到退出函数结束定时,从而可以判断出函数的执行时间,这种时间也并
非是函数或者算法的真实执行时间,因为在函数和算法线程不可能一直占用 CPU ,对于所有判断执行时间的函数都是一样,不过基本上已经很准
确,可以通过查询进行定时。 GetTickCount() 和 Clock() 函数是向主板 BIOS 要 real time clock 时间,会有中断产生,以及延迟问题。
精度: WindowsNT 3.5 以及以后版本精度是 10ms ,它的时间精度比 clock 函数的要高, GetTickCount() 常用于多媒体中。
7 : Windows 下 timeGetTime
使用:需要包含 Mmsystem.h , Windows.h ,加入静态库 Winmm.lib.
timeBeginPeriod(1);
DWORD start = timeGetTime();
Sleep(100);
DWORD end = timeGetTime();
timeEndPeriod(1);
原理: timeGetTime 也时常用于多媒体定时器中,可以通过查询进行定时。通过查询进行定时,本身也会影响定时器的定时精度。
精度:毫秒,与 GetTickCount() 相当。但是和 GetTickCount 相比, timeGetTime 可以通过 timeBeginPeriod , timeEndPeriod 设置定时器的最小解析
精度 , timeBeginPeriod,timeEndPeriod 必须成对出现。
8 : windows 下的 timeSetEvent
使用:还记的 VC 下的 Timer 吗? Timer 是一个定时器,而以上我们提到几种时间函数或者类型,实现定时功能只能通过轮训来实现,也就是必须另外创
建一个线程单独处理,这样会影响定时精度,好在 windows 提供了内置的定时器 timeSetEvent ,
函数原型为 MMRESULT timeSetEvent ( UINT uDelay, // 以毫秒指定事件的周期
UINT uResolution, // 以毫秒指定延时的精度,数值越小定时器事件分辨率越高。缺省值为 1ms
LPTIMECALLBACK lpTimeProc, // 指向一个回调函数
WORD dwUser, // 存放用户提供的回调数据
UINT fuEvent ) // 标志参数, TIME_ONESHOT :执行一次; TIME_PERIODIC :周期性执行
具体应用时,可以通过调用 timeSetEvent() 函数,将需要周期性执行的任务定义在 lpFunction 回调函数中 ( 如:定时采样、控制等 ) ,从而完成所
需处理的事件。需要注意的是:任务处理的时间不能大于周期间隔时间。另外,在定时器使用完毕后,应及时调用 timeKillEvent() 将之释放。
原理:可以理解为代回调函数的 timeGetTime
精度:
毫秒, timeSetEvent 可以通过 timeBeginPeriod , timeEndPeriod 设置定时器的最小解析精度 , timeBeginPeriod,timeEndPeriod 必须成对出现
9: 高精度时控函数 QueryPerformanceFrequency , QueryPerformanceCounter
使用: LARGE_INTEGER m_nFreq;
LARGE_INTEGER m_nBeginTime;
LARGE_INTEGER nEndTime;
QueryPerformanceFrequency(&m_nFreq); // 获取时钟周期
QueryPerformanceCounter(&m_nBeginTime); // 获取时钟计数
Sleep(100);
QueryPerformanceCounter(&nEndTime);
cout << (nEndTime.QuadPart-m_nBeginTime.QuadPart)*1000/m_nFreq.QuadPart << endl;
原理: CPU 上也有一个计数器,以机器的 clock 为单位,可以通过 rdtsc 读取,而不用中断,因此其精度与系统时间相当。
精度:计算机获取硬件支持,精度比较高,可以通过它判断其他时间函数的精度范围。
QueryPerformanceCount 计数器,随系统的不同可以提供微秒级的计数。对于实时图形处理、多媒体数据流处理、或者实时系统构造的程序
员,善用 QueryPerformanceCount/QueryPerformanceFrequency 是一项基本功。
10: 小结
以上提到常用的 9 种时间函数,由于他们的用处不同,所以他们的精度也不尽相同,所以
如果简单的延时可以用 sleep 函数,稍微准确的延时可以使用 clock 函数, GetTickCount 函数,更高级的实用 timeGetTime 函数;
简单的定时事件可以用 Timer ,准确地可以用 imeSetEvent ;
或取一般系统时间可以通 time ,或者 CTime ,或者 COleDateTime ,获取准确的时间可以用 clock ,或者 GetTickCount 函数,或者 timeGetTime 函数
而获取准确地系统时间要使用硬件支持的 QueryPerformanceFrequency 函数, QueryPerformanceCounter 函数。
另一种直接利用 Pentium CPU 内部时间戳进行计时的高精度计时手段。以下讨论主要得益于《 Windows 图形编程》一书,第 15 页- 17 页,有兴趣
的读者可以直接参考该书。关于 RDTSC 指令的详细讨论,可以参考 Intel 产品手册。本文仅仅作抛砖之用。
在 Intel Pentium 以上级别的 CPU 中,有一个称为“时间戳( Time Stamp )”的部件,它以 64 位无符号整型数的格式,记录了自 CPU 上电以
来所经过的时钟周期数。由于目前的 CPU 主频都非常高,因此这个部件可以达到纳秒级的计时精度。这个精确性是上述两种方法所无法比拟的。
在 Pentium 以上的 CPU 中,提供了一条机器指令 RDTSC ( Read Time Stamp Counter )来读取这个时间戳的数字,并将其保存在 EDX:EAX
寄存器对中。由于 EDX:EAX 寄存器对恰好是 Win32 平台下 C++ 语言保存函数返回值的寄存器,所以我们可以把这条指令看成是一个普通的函数调用。
像这样:
inline unsigned __int64 GetCycleCount()
{
__asm RDTSC
}
但是不行,因为 RDTSC 不被 C++ 的内嵌汇编器直接支持,所以我们要用 _emit 伪指令直接嵌入该指令的机器码形式 0X0F 、 0X31 ,如下:
inline unsigned __int64 GetCycleCount()
{
__asm _emit 0x0F
__asm _emit 0x31
}
以后在需要计数器的场合,可以像使用普通的 Win32 API 一样,调用两次 GetCycleCount 函数,比较两个返回值的差,像这样:
unsigned long t;
t = (unsigned long)GetCycleCount();
//Do Something time-intensive ...
t -= (unsigned long)GetCycleCount();
《 Windows 图形编程》第 15 页编写了一个类,把这个计数器封装起来。有兴趣的读者可以去参考那个类的代码。作者为了更精确的定时,做了一
点小小的改进,把执行 RDTSC 指令的时间,通过连续两次调用 GetCycleCount 函数计算出来并保存了起来,以后每次计时结束后,都从实际得到的计
数中减掉这一小段时间,以得到更准确的计时数字。但我个人觉得这一点点改进意义不大。在我的机器上实测,这条指令大概花掉了几十到 100 多个
周期,在 Celeron 800MHz 的机器上,这不过是十分之一微秒的时间。对大多数应用来说,这点时间完全可以忽略不计;而对那些确实要精确到
纳秒数量级的应用来说,这个补偿也过于粗糙了。
这个方法的优点是:
1. 高精度。可以直接达到纳秒级的计时精度(在 1GHz 的 CPU 上每个时钟周期就是一纳秒),这是其他计时方法所难以企及的。
2. 成本低。 timeGetTime 函数需要链接多媒体库 winmm.lib , QueryPerformance* 函数根据 MSDN 的说明,需要硬件的支持(虽然我还没有
见过不支持的机器)和 KERNEL 库的支持,所以二者都只能在 Windows 平台下使用(关于 DOS 平台下的高精度计时问题,可以参考《图形程序开发
人员指南》,里面有关于控制定时器 8253 的详细说明)。但 RDTSC 指令是一条 CPU 指令,凡是 i386 平台下 Pentium 以上的机器均支持,甚至没有平
台的限制(我相信 i386 版本 UNIX 和 Linux 下这个方法同样适用,但没有条件试验),而且函数调用的开销是最小的。
3. 具有和 CPU 主频直接对应的速率关系。一个计数相当于 1/(CPU 主频 Hz 数 ) 秒,这样只要知道了 CPU 的主频,可以直接计算出时间。这和
QueryPerformanceCount 不同,后者需要通过 QueryPerformanceFrequency 获取当前计数器每秒的计数次数才能换算成时间。
这个方法的缺点是:
1. 现有的 C/C++ 编译器多数不直接支持使用 RDTSC 指令,需要用直接嵌入机器码的方式编程,比较麻烦。
2. 数据抖动比较厉害。其实对任何计量手段而言,精度和稳定性永远是一对矛盾。如果用低精度的 timeGetTime 来计时,基本上每次计时的结果都是
相同的;而 RDTSC 指令每次结果都不一样,经常有几百甚至上千的差距。这是这种方法高精度本身固有的矛盾。
关于这个方法计时的最大长度,我们可以简单的用下列公式计算:
自 CPU 上电以来的秒数 = RDTSC 读出的周期数 / CPU 主频速率( Hz )
64 位无符号整数所能表达的最大数字是 1.8 × 10^19 ,在我的 Celeron 800 上可以计时大约 700 年(书中说可以在 200MHz 的 Pentium 上计时 117 年 ,
这个数字不知道是怎么得出来的,与我的计算有出入)。无论如何,我们大可不必关心溢出的问题。
下面是几个小例子,简要比较了三种计时方法的用法与精度
//Timer1.cpp 使用了 RDTSC 指令的 Timer 类
//KTimer 类的定义可以参见《 Windows 图形编程》 P15
// 编译行: CL Timer1.cpp /link USER32.lib
#include <tstdio.h>
#include "KTimer.h"
main()
{
unsigned t;
KTimer timer;
timer.Start();
Sleep(1000);
t = timer.Stop();
printf("Lasting Time: %d/n",t);
}
//Timer2.cpp 使用了 timeGetTime 函数
// 需包含 <mmsys.h> ,但由于 Windows 头文件错综复杂的关系
// 简单包含 <windows.h> 比较偷懒:)
// 编译行: CL timer2.cpp /link winmm.lib
#include <twindows.h>
#include <tstdio.h>
main()
{
DWORD t1, t2;
t1 = timeGetTime();
Sleep(1000);
t2 = timeGetTime();
printf("Begin Time: %u/n", t1);
printf("End Time: %u/n", t2);
printf("Lasting Time: %u/n",(t2-t1));
}
//Timer3.cpp 使用了 QueryPerformanceCounter 函数
// 编译行: CL timer3.cpp /link KERNEl32.lib
#include <windows.h>
#include <stdio.h>
main()
{
LARGE_INTEGER t1, t2, tc;
QueryPerformanceFrequency(&tc);
printf("Frequency: %u/n", tc.QuadPart);
QueryPerformanceCounter(&t1);
Sleep(1000);
QueryPerformanceCounter(&t2);
printf("Begin Time: %u/n", t1.QuadPart);
printf("End Time: %u/n", t2.QuadPart);
printf("Lasting Time: %u/n",( t2.QuadPart- t1.QuadPart));
}
// 以上三个示例程序都是测试 1 秒钟休眠所耗费的时间
file:// 测 / 试环境: Celeron 800MHz / 256M SDRAM
// Windows 2000 Professional SP2
// Microsoft Visual C++ 6.0 SP5
以下是 Timer1 的运行结果,使用的是高精度的 RDTSC 指令
Lasting Time: 804586872
以下是 Timer2 的运行结果,使用的是最粗糙的 timeGetTime API
Begin Time: 20254254
End Time: 20255255
Lasting Time: 1001
以下是 Timer3 的运行结果,使用的是 QueryPerformanceCount API
Frequency: 3579545
Begin Time: 3804729124
End Time: 3808298836
Lasting Time: 3569712