【问题标题】:Why QueryperformanceCounter timed different from wall clock?为什么 QueryperformanceCounter 的计时与挂钟不同?
【发布时间】:2023-03-23 12:38:01
【问题描述】:

您好,我正在使用 QueryperformanceCounter 在 Delphi 中对一段代码进行计时。出于某种原因, 我使用 QueryPerformanceCounter 获得的毫秒数与我使用秒表获得的挂钟时间完全不同。例如秒表给我大约 33 秒,如果不准确,这似乎是正确的,但使用 QueryPerofomanceCounter 会给我一个像 500 毫秒的数字。

当单步执行我的代码时,我可以看到 QueryPerformanceFrequency 为我的 CPU 提供了正确的 CPU 频率,Core2 E6600 为 2.4G。因此,如果刻度数正确,(tick number / Freq) * 1000 应该为我正在计时的代码提供正确的执行时间,但为什么不呢?

我知道对于我尝试计时的代码,QeuryPerformanceCounter 可能会过度杀戮,因为它需要几秒钟而不是 MillionSeconds,但我更感兴趣的是了解挂钟和 QueryPerormanceCounter 之间时间差的原因。

如果相关,我的硬件是 E6600 Core2,操作系统是 Windows 7 X64。

unit PerformanceTimer;

interface

uses Windows, SysUtils, DateUtils;

type TPerformanceTimer = class
  private
    fFrequency : TLargeInteger;
    fIsRunning: boolean;
    fIsHighResolution: boolean;
    fStartCount, FstopCount : TLargeInteger;
    procedure SetTickStamp(var lInt : TLargeInteger) ;
    function GetElapsedTicks: TLargeInteger;
    function GetElapsedMiliseconds: TLargeInteger;
  public
    constructor Create(const startOnCreate : boolean = false) ;
    procedure Start;
    procedure Stop;
    property IsHighResolution : boolean read fIsHighResolution;
    property ElapsedTicks : TLargeInteger read GetElapsedTicks;
    property ElapsedMiliseconds : TLargeInteger read GetElapsedMiliseconds;
    property IsRunning : boolean read fIsRunning;
end;

implementation

constructor TPerformanceTimer.Create(const startOnCreate : boolean = false) ;
begin
  inherited Create;

  fIsRunning := false;

  fIsHighResolution := QueryPerformanceFrequency(fFrequency) ;
  if NOT fIsHighResolution then
    fFrequency := MSecsPerSec;

  if startOnCreate then
    Start;
end;

function TPerformanceTimer.GetElapsedTicks: TLargeInteger;
begin
  result := fStopCount - fStartCount;
end;

procedure TPerformanceTimer.SetTickStamp(var lInt : TLargeInteger) ;
begin
  if fIsHighResolution then
    QueryPerformanceCounter(lInt)
  else
    lInt := MilliSecondOf(Now) ;
end;

function TPerformanceTimer.GetElapsedMiliseconds: TLargeInteger;
begin
  result := (MSecsPerSec * (fStopCount - fStartCount)) div fFrequency;
end;

procedure TPerformanceTimer.Start;
begin
  SetTickStamp(fStartCount) ;
  fIsRunning := true;
end;

procedure TPerformanceTimer.Stop;
begin
  SetTickStamp(fStopCount) ;
  fIsRunning := false;
end;

end.

【问题讨论】:

  • 我猜,显示代码会有所帮助,因为数量级相差 10^3^2(错误乘以 1000)
  • 刚刚做了一个小测试,单个算术加法运算甚至不会花费一半时间。 50 个加法操作的循环将产生 1 或 2 个刻度。那么使用 QueryPerformanceCounter 有什么问题。
  • @pstar: 50 次添加操作并不算多。 QueryPerformanceCounter 肯定没有错。请同时显示您实际测量的代码。
  • 您在使用秒表计时时的错误将大于 queryPerformanceCounter 中的错误。当然,您犯的编码错误可能比这更大。欢迎来到调试计时的可爱世界。
  • @Warren,除非 Pstar 的反应非常缓慢,否则我认为停止秒表需要超过 30 秒的时间,所以虽然这个时间确实会有更多的错误空间,但它不会'不是 32.5 秒' 的错误。 Pstar,您正在计时的代码是否真的只需要半秒钟才能运行,并且结果显示在屏幕上需要很长时间,以便您可以注意到并按下秒表上的按钮?

标签: delphi performancecounter


【解决方案1】:

这段代码对我有用,也许你可以试试:

  var
    ifrequency, icount1, icount2: Int64;
    fmsec: Double;
  begin
    QueryPerformanceFrequency(ifrequency);
    QueryPerformanceCounter(icount1);
    Sleep(500);
    QueryPerformanceCounter(icount2);
    fmsec := 1000 * ((icount2 - icount1) / ifrequency);
  end;

fmsec 大约是 499.6 或类似的值。

注意:不要依赖 Now 或 TickCount 来处理小数字:它们有大约 10 毫秒的间隔(取决于 Windows 版本)!因此,如果您使用 Now 和 DateUtils.MillisecondsBetween,“sleep(10)”的持续时间可以为 0ms

注意 2:不要长时间依赖 QueryPerformanceCounter,因为它的时间会在一天中慢慢消失(每分钟大约 1 毫秒差异)

【讨论】:

  • 找出 QueryPerformanceCounter 本身是否导致我要说的问题的最佳解决方案。但正如我在我的 cmets 中所说,QueryPerformanceFrequency 确实返回了正确的频率数。因此,正如我测试的那样,您的代码将正常工作。有趣的是,您的代码将始终返回平均非常接近 499.6 的数字,并且它也发生在我的机器上。你知道原因吗?
【解决方案2】:

如果您的硬件支持动态频率缩放,则意味着 QueryPerformanceFrequency 不能返回一个静态值来连续描述动态变化的值。每当计算量大的东西开始时,适应的 CPU 速度都会阻止精确测量。

至少,在我的笔记本上体验过 - 当它更改为更高的时钟频率时,基于 QueryPerformanceCounter 的测量被搞砸了。

因此,无论提供更高的精度,我仍然在大多数情况下使用 GetTickCount 来实现此类目的(但基于 DateTime 的测量也可以,如前所述,除非可能发生时区切换),并带有一些“温暖- up”代码段开始消耗 CPU 功率,因此当相关代码段开始执行时,CPU 速度处于其(恒定)最大值。

【讨论】:

  • 有些芯片组即使在禁用 CPU 速度自适应功能时也不会返回准确甚至线性的时序信息,即使在某些台式机上也是如此。甚至
  • @brezinczky 所有现代 CPU 都将支持动态频率缩放,至少我知道 Pentium 4 及更高版本支持,当然这取决于 BIOS 或用户设置来启用/禁用它。但我的结论仍然是,除非使用动态变化的 QueryPerformanceFrequency,否则使用具有静态值的 QueryPerformanceCounter 进行计时大部分时间都不会起作用,使其无用。但是使用计数器本身作为分析器的原因仍然有用。
【解决方案3】:

你应该发布一个代码 sn-p 来演示这个问题......但我会假设你有一个错误:

Milliseconds := 1000 * ((StopCount - StartCount) / Frequency);

如果您要与秒表进行比较,您可能会采用更简单的方法,只需捕获之前和之后的 TDateTime(使用 Now()),然后使用 DateUtils MilliSecondSpan () 计算差值的方法:

var
  MyStartDate:TDateTime;
  MyStopDate:TDateTime;
  MyTiming:Double;
begin
  MyStartDate := Now();
  DoSomethingYouWantTimed();
  MyStopDate := Now();
  MyTiming := MilliSecondSpan(MyStopDate, MyStartDate);
  DoSomethingWithTiming(MyTiming);
end;

【讨论】:

  • 你的假设是正确的!我不得不承认我测量的代码块是不完整的。所以简而言之,代码的其他部分导致了我没有想到的长时间延迟,并且使用 Now() 我得到了与使用 QueryPerformanceCounter 完全相同的时间跨度。你赢得了我的选票,因为在我的特定情况下,我的硬件工作正常,我测量时间的代码正在工作,但我没有测量正确的代码路径。 QueryPerformanceCounter 不起作用的原因可能有很多,在我的情况下不应该按比例计算:半秒与大约 30 秒。
【解决方案4】:

我使用 NTP 服务器定期同步 PC 时钟,长时间同步 PC 时钟以调整 QueryPerformanceCounter“滴答”时间,并使用校准的 QueryPerformanceCounter 时间进行精确的时间测量。在时钟漂移较低的良好服务器上,这意味着我在时间段内的准确性远低于一毫秒,并且我所有机器的时钟时间同步在一两毫秒内。下面附上部分相关代码:

function NowInternal: TDateTime;
const
  // maximum time in seconds between synchronising the high-resolution clock
  MAX_SYNC_TIME = 10;
var
  lPerformanceCount: Int64;
  lResult: TDateTime;
  lDateTimeSynchronised: Boolean;
begin
  // check that the the high-resolution performance counter frequency has been
  // initialised
  fDateTimeCritSect.Enter;
  try
    if (fPerformanceFrequency < 0) and
       not QueryPerformanceFrequency(fPerformanceFrequency) then
      fPerformanceFrequency := 0;

    if fPerformanceFrequency > 0 then begin
      // get the return value from the the high-resolution performance counter
      if (fWindowsStartTime <> CSI_NULL_DATE_TIME) and
         QueryPerformanceCounter(lPerformanceCount) then
        lResult := fWindowsStartTime +
                   lPerformanceCount / fPerformanceFrequency / SecsPerDay
      else
        lResult := CSI_NULL_DATE_TIME;

      if (MilliSecondsBetween(lResult, Now) >= MAX_CLOCK_DIFF) or
         (SecondsBetween(Now, fLastSyncTime) >= MAX_SYNC_TIME) then begin
        // resynchronise the high-resolution clock due to clock differences or
        // at least every 10 seconds
        lDateTimeSynchronised := SyncDateTime;

        // get the return value from the the high-resolution performance counter
        if (fWindowsStartTime <> CSI_NULL_DATE_TIME) and
           QueryPerformanceCounter(lPerformanceCount) then
          lResult := fWindowsStartTime +
                     lPerformanceCount / fPerformanceFrequency / SecsPerDay;

      end else
        lDateTimeSynchronised := False;

      if MilliSecondsBetween(lResult, Now) >= (MAX_CLOCK_DIFF * 2) then
        // default the return value to the standard low-resolution value if
        // anything has gone wrong
        Result := Now
      else
        Result := lResult;

    end else begin
      lDateTimeSynchronised := False;

      // default the return value to the standard low-resolution value because
      // we cannot use the high-resolution clock
      Result := Now;
    end;
  finally
    fDateTimeCritSect.Leave;
  end;

  if lDateTimeSynchronised then
    CsiGlobals.AddLogMsg('High-resolution clock synchronised', CSI_LC_CLOCK);
end;

function SyncDateTime: Boolean;
var
  lPriorityClass: Cardinal;
  lThreadPriority: Integer;
  lInitTime: TDateTime;
  lNextTime: TDateTime;
  lPerformanceCount: Int64;
  lHighResCurrentTime: TDateTime;
  lLowResCurrentTime: TDateTime;
begin
  // synchronise the high-resolution date/time structure (boost the thread
  // priority as high as possible during synchronisation)
  lPriorityClass := CsiGetProcessPriorityClass;
  lThreadPriority := CsiGetCurrentThreadPriority;
  try
    CsiSetProcessPriorityClass(REALTIME_PRIORITY_CLASS);
    CsiSetCurrentThreadPriority(THREAD_PRIORITY_TIME_CRITICAL);

    // loop until the low-resolution date/time value changes (this will load the
    // CPU, but only for a maximum of around 15 milliseconds)
    lInitTime := Now;
    lNextTime := Now;
    while lNextTime = lInitTime do
      lNextTime := Now;

    // adjust the high-resolution performance counter frequency for clock drift
    if (fWindowsStartTime <> CSI_NULL_DATE_TIME) and
       QueryPerformanceCounter(lPerformanceCount) then begin
      lHighResCurrentTime := fWindowsStartTime +
                             lPerformanceCount / fPerformanceFrequency /
                             SecsPerDay;
      lLowResCurrentTime := Now;
      if MilliSecondsBetween(lHighResCurrentTime, lLowResCurrentTime) <
         (MAX_CLOCK_DIFF * 2) then
        fPerformanceFrequency := Round((1 +
                                       (lHighResCurrentTime -
                                        lLowResCurrentTime) /
                                       (lLowResCurrentTime - fLastSyncTime)) *
                                       fPerformanceFrequency);
    end;

    // save the Windows start time by extrapolating the high-resolution
    // performance counter value back to zero
    if QueryPerformanceCounter(lPerformanceCount) then begin
      fWindowsStartTime := lNextTime -
                           lPerformanceCount / fPerformanceFrequency /
                           SecsPerDay;
      fLastSyncTime := Now;
      Result := True;

    end else
      Result := False;
  finally
    CsiSetCurrentThreadPriority(lThreadPriority);
    CsiSetProcessPriorityClass(lPriorityClass);
  end;
end;

【讨论】:

  • 我不确定这个答案如何解决秒表测量半分钟时为什么 Pstar 的计时器测量半秒的问题。
  • 不管与我的问题相关,似乎是一般计时器问题的一个很好的解决方案,我喜欢使用 PC 时钟时间调整 QueryPerformanceFrequency 的想法。但我猜如果频率没有改变,精度会低于 QueryPerformanceCounter?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-31
相关资源
最近更新 更多