计算线程执行某项任务消耗的时间时,许多开发人员会调用GetTickCount/GetTickCount64编写如下的代码:
// Get the current time (start time)
ULONGLONG qwStartTime = GetTickCount64();
// Perform complex algorithm here
// Subtract start time from current time to get duration
ULONGLONG dwElapsedTime = GetTickCount64() - qwStartTime;
这段代码假设当前线程不会被中断。然而在Windows这样的基于优先级的操作系统中,开发人员无法得知线程被调度的准确时间。当线程在执行任务中被中断时,使用上面的方法根本无法获得线程所消耗的时间。我们需要一个可以返回线程消耗的CPU时间(既被调度时间)的函数,幸运的是,在Vista之前的操作系统已经提供了GetThreadTimes做到这一点:
BOOL GetThreadTimes(HANDLE hThread,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime);
GetThreadTime函数会将线程相关的时间信息写入为其传递的PFILETIME参数中,各个参数返回值的含义如下表所示:
参数 | 意义 |
---|---|
pftCreationTime | 从1601年1月1号凌晨开始到指定线程被创建时的时间,以100纳秒为单位 |
pftExitTime | 从1601年1月1号凌晨开始到指定线程结束所花的时间,以100纳秒为单位,假如线程尚未终止,则该值未定义 |
pftKernelTime | 线程在内核模式下运行所花的时间,以100纳秒为单位 |
pftUserTime | 线程在用户模式下运行所花的时间,以100纳秒为单位 |
使用GetThreadTimes可以计算线程所消费的CPU时间,比如下面的代码:
__int64 FileTimeToQuadWord(PFILETIME pft) {
return (Int64ShllMod32(ptf->dwHighDateTime, 32) | ptf->dwLowDateTime);
}
void PerformLongOperation() {
FILETIME ftKernelTimeStart, ftKernelTimeEnd;
FILETIME ftUserTiimeStart, ftUserTimeEnd;
FILETIME ftDummy;
__int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed;
// Get starting times
GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeStart, &ftUserTimeStart);
// Perform complex algorithm here
...
// Get the ending times
GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeEnd, &ftUserTimeEnd);
// Get the elapsed kernel and user times by converting the start
// and the times from FILETIMEs to quad words, and then subtract
// the start times from the end times.
qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) -
FileTimeToQuadWord(&ftKernelTimeStart);
qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) -
FileTimeToQuadWord(&ftUserTimeStart);
// Get total time duration by adding the kernel and user times.
qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;
}
函数GetProcessTimes用来返回进程中所有线程累计的时间信息:
BOOL GetProcessTimes(
HANDLE hProcess,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime
);
GetProcessTimes的返回值是进程中所有线程(包括已终止线程)的累计信息,比如,pftKernelTime参数返回进程中所有线程在内核模式下执行的所有时间之和。
上面讨论的方法适合Vista及之前的系统,但在Vista中,查询线程CPU时间的方法有些变化。Vista不再依赖间隔约为10~15毫秒的系统内部定时器,而是使用处理器的时间戳计数器(Time Stamp Counter,TSC),该计数器使用64位的值记录系统启动以来的CPU周期数。在目前广泛使用的GHz级处理器上,这种方法显然要比毫秒值精确的多。
当线程被停止调度时,系统会计算当前TSC和线程开始调度时TSC的差值,并将该值累加到线程消耗的CPU周期数中。QueryThreadCycleTime函数和QueryProcessCycleTime函数返回指定线程消耗的CPU周期数或指定进程中所有线程的消耗的CPU周期数之和。此外,可以使用ReadTimeStampCounter返回自上次重置以来系统的TSC值,ReadTimeStampCounter是定义在WinNT.h中的宏,指向C++编译器提供内置函数__rdtsc。
对于精度要求较高的分析,GetThreadTimes可能无法胜任,为此Windows提供了以下两个高精度性能分析函数:
BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);
BOOL QueryPerformanceCounter(LARGE_INTEGER* pliCount);
函数QueryPerformanceFrequency返回当前硬件平台的高精度性能计数器(High-resolution Performance Counter,HRPC)的频率,注意该值并不是CPU的主频。假如当前硬件平台不支持HRPC,则函数返回0,否则返回非0值。QueryPerformanceCounter返回当前HRPC的值,若当前硬件平台不支持HRPC,函数返回0,否则返回非0值。要注意这两个函数假设调用者线程不会被抢占,不过大多数高精度的分析是在小段代码块内完成的,因此这一点不用担心。下面是我使用这些函数包装的一个C++类,可以很方便的用来进行时间性能分析:
class CStopwatch {
public:
CStopwatch() {
QueryPerformanceFrequency(&m_liPerfFreq);
Start();
};
void Start() {
QueryPerformanceCounter(&m_liPerfStart);
}
__int64 Now() const { // 返回自Start调用以来的毫秒数
LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return (liPerfNow.QuadPart - liPerfStart.QuadPart)*1000/m_liPerfFreq.QuadPart;
}
__int64 NowInMicro() const { // 返回自Start调用以来的微秒数
LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return (liPerfNow.QuadPart - liPerfStart.QuadPart)*1000000/m_liPerfFreq.QuadPart;
}
private:
LARGE_INTEGER m_liPerfFreq; // HSPC的频率
LARGE_INTEGER m_liPerfStart; // HSPC的初始值
};
下面是一个使用CStopwatch类的例子:
CStopwatch stopwatch;
// 在此处执行待测试的代码...
// 获得代码执行时间
__int64 qwElapsedTime = stopwatch.Now();
除了测试代码执行时间,还可以使用上面的函数估算当前计算机系统CPU的主频,代码如下:
DWORD GetCpuFrequencyInMHz() {
// change the priority to ensure the thread will have more chances
// to be scheduled when Sleep() ends
int currentPriority = GetThreadPriority(GetCurrentThread());
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
// Keep track of the elapsed time with the other timer
__int64 elapsedTime = 0;
// Create a stopwatch timer which defaults to the current time
__int64 perfCountStart = stopwatch.NowInMicro();
// get the current number of cycles
unsigned __int64 cyclesOnStart = ReadTimeStampCounter();
// wait for about 1 second
Sleep(1000);
// get the number of cycles after about 1 second
unsigned __in64 numberOfCycles = ReadTimeStampCounter() - cyclesOnStart;
// Get how much time has elapsed with greater precision
elpasedTime = stopwatch.NowInMicro() - perfCountStart;
// Restore the thread priority
SetThreadPriority(GetCurrentThread(), currentPriority);
// Compute the frequency in MHz
return (DWORD)(numberOfCycles/elaspedTime);
}
上面代码的含义比较清晰,只是要注意用这种方法得到的只是CPU频率的估计值,因为Sleep函数的调用效果大多数情况下不可能精确到指定的1秒,且上述方法假设CPU不具备自动调频的能力。