UML软件工程组织

如何获得十亿分之一秒的时钟精度
作者:俞伟明 发文时间:2003.11.19
要想真正有效地测试、优化程序性能——特别是为Windows服务器开发的多线程程序,操作系统提供的标准时钟是不够的,必须使用解析度更高的时钟。本文介绍了如何访问处理器的十亿分之一秒级别的时钟,极大地提高代码性能测试的速度和精度。

一、获取计时数据

和其他Windows服务器一样,在Windows 2003 Server上最能发挥性能优势的是多线程程序。Windows 2003 Server支持各种多处理器系统,同时也能在单处理器的P4系统上运行。对于单处理器P4系统,Windows 2003 Server将发挥出Intel超线程技术提供的各种硬件线程执行引擎的优势。

开发服务器应用的人都知道,之所以要开发并行程序,真正的原因只有一个——性能。然而,众所周知,性能改善是一个比较模糊的目标,因为多线程代码的性能通常只能靠经验估计。在单线程程序中,性能改进程度一般可以精确地预知,例如减少了多少指令和延迟较高的操作,但多线程代码不同,Windows平台中线程调度是不确定的,也就是说,在Windows中应用程序可以要求调度程序运行线程,但调度程序何时(是否)运行线程则远远超出了应用程序代码的控制范围。

在测试性能时,开发者很快会遇到一个问题,这就是Windows内建的标准时钟实在不够精确,其可靠测量事件时间的解析度很难高于一秒,这样,要确定一个代码片段是否真正得到优化就很困难了。如果一定要用Windows的标准时钟进行测试,必须利用循环让代码运行几百万次,才能获得有效的时间数据。绝大多数情况下,使用这类循环意味着修改应用程序。

其实,还有更好的办法,这就是Win32高解析度时钟,涉及的函数有两个:QueryPerformanceCount(),QueryPerformanceFrequency()。在Intel系统中,从P II开始,这些函数依赖于Pentium芯片内建的一个计数器。当一个Intel系统启动时,一个64位的寄存器跟踪着消逝的时钟周期,这个计数器提供了解析度极高的计时设备。

整个64位寄存器都要用到。32 bit的整数大约能计数20亿,对于当前每秒运行20-30亿个周期的处理器,32 bit的计数器会在一秒或更少的时间内溢出,64 bit的计数器则能容纳这些秒数的20亿倍,按20亿秒计算就是约63年——可以相信,这已经远远超出测量任何程序的要求了。

要对一个事件进行计时,只需获得事件开始之前、结束之后的时钟计数。下面的代码不依赖于Win32(即,从C/C++直接访问),稍后我们再看看操作系统提供的函数。我们首先定义一个数据结构,然后再来看填写该结构的代码:


typedef  struct _BinInt32
{
__int32 i32[2];
} BigInt32;

typedef  struct _BigInt64
{
    __int64 i64;
} BigInt64;

typedef union _bigInt
{
    BigInt32 int32val;
    BigInt64 int64val;
} BigInt;


下面的代码从操作系统获得时钟计数器的高位和低位,分别填写__int64数据的两个32 bit部分:


BigInt start_ticks, end_ticks;
_asm
{
    RDTSC
    mov start_ticks.int32val.i32[0], eax
    mov start_ticks.int32val.i32[4], edx
}


这段代码能够在Visual Studio .NET 2003中顺利运行,在以前的C/C++编译器中也应该没有问题。RDTSC(ReaD Time Stamp Counter)是一个汇编指令,它的功能是把时间戳计数器的内容装入EAX和EDX寄存器。执行上述代码后,start_ticks就包含了完整的时钟计数。再次调用上面的代码,把start_ticks替换成end_ticks,再从end_ticks减去start_ticks,就得到了两次调用期间流逝的时钟周期。

要输出这个_int64值,可以使用下面的printf()掩码:


printf ( "Function used %I64Ld ticks\n", end_ticks.int64val.i64 -
 start_ticks.int64val.i64 );


Win32函数QueryPerformanceCounter()的功能也大致相似,它唯一的参数是一个指向计数器变量的长指针。如果函数调用失败,它返回0(实际上是FALSE)。然而,上面提供的代码突破了Windows调用的黑箱,即使在非Windows的Intel系统上,也能发挥同样的功能。

二、使用计时数据

如果要把时钟计数转换成时间,只要把时钟计数除以CPU的时钟频率就可以了。不过,芯片上标准的GHz数据往往与实际运行的速度不同。如果要测试芯片的实际速度,除了Win32调用QueryPerformanceFrequency(),还有几种非常好的工具软件。这里要推荐两种工具,首先是Intel自己的Processor Frequency ID Utility,可以从http://support.intel.com/support/

processors/tools/FrequencyID/FreqID.htm免费下载,它还能提供有关处理器的许多其他信息。另一个工具提供的信息更多,它就是wCPUID,可以从http://www.h-oda.com/免费下载。

这两种工具都能够测出精确的时钟速度,用前面获得的时钟计数除以速度,就可以得到高精度的时间计数。QueryPerformanceFrequency()函数也只有一个长指针参数,出现错误时返回0或FALSE。

在如此高的时钟解析度下,许多平常看不到的现象会显现出来。最令人莫名其妙的是,多次测试同一段代码,结果会出现很大的波动。

大范围波动的主要原因在于读取操作,特别是第一、二两次读取与从缓冲区读取的差异。当代码第一次执行时,一般需要把它装入到缓冲区,代码所操作的数据也一样。用时钟周期来度量,这一缓冲装入过程是相当耗时的。不过,当代码和数据放入了缓冲区(多次运行代码之后的结果),装入缓冲区操作所带来的失真渐渐消失。因此,实际测试时,应当抛弃前几次的数据,只计算结果稳定下来之后的平均值。

然而,即使在看起来比较稳定的结果集中,仍会突然出现一些突变,这是由于操作系统切换线程所导致的。由于时钟计数器总是不停地累加,它的计数不能反映出代码的一部分执行时间已经用于休眠。要解决这个问题,必须将线程设置成Windows最高的优先级,即实时(对应的符号是REALTIME_PRIORITY_CLASS),防止测试期间线程被切换掉。

但是,采用这种解决办法时必须谨慎。如果让一大段代码用这个优先级运行,可能会阻塞其他线程。因此,如果要用这种办法测试大段代码,应当确信系统暂时不作它用。另外,必须记住的是,测试完成后要把代码恢复成标准优先级——注意,是在测试完成后立即恢复,否则的话,可能带来许多风险,例如,可能直到部署应用程序时也不能再想起需要恢复优先级,由此带来的问题可能使用户久久难忘——当你的应用程序开始运行时,其他代码都好像停止运行了。

当然,这是一个可以暂且不管的话题。无论怎样,现在我们已经有了一个高度精确的时钟,它能够在大多数当前的Intel处理器上运行,适合从Windows 95开始的所有Windows操作系统。好好享受吧!

 

版权所有:UML软件工程组织