10

微软已经在 GetTickCount 的文档中说过,你永远不能比较滴答计数来检查是否已经过了一个间隔。例如:

不正确(伪代码):

DWORD endTime = GetTickCount + 10000; //10 s from now

...

if (GetTickCount > endTime)
   break;

上面的代码很糟糕,因为它很容易发生刻度计数器的翻转。例如,假设时钟接近其范围的末端:

endTime = 0xfffffe00 + 10000
        = 0x00002510; //9,488 decimal

然后你执行你的检查:

if (GetTickCount > endTime)

立即满足,因为GetTickCount 大于endTime

if (0xfffffe01 > 0x00002510)

解决方案

相反,您应该始终减去两个时间间隔:

DWORD startTime = GetTickCount;

...

if (GetTickCount - startTime) > 10000 //if it's been 10 seconds
   break;

看同样的数学:

if (GetTickCount - startTime) > 10000

if (0xfffffe01 - 0xfffffe00) > 10000

if (1 > 10000)

在 C/C++ 中,这一切都很好,编译器以某种方式运行。

但是德尔福呢?

但是当我在 Delphi 中执行相同的数学运算时,在 ( {Q+}, ) 上进行溢出检查时,两个滴答计数的减法会在TickCount翻转{$OVERFLOWCHECKS ON}时生成 EIntOverflow 异常:

if (0x00000100 - 0xffffff00) > 10000

0x00000100 - 0xffffff00 = 0x00000200

这个问题的预期解决方案是什么?

编辑:我试图暂时关闭OVERFLOWCHECKS

{$OVERFLOWCHECKS OFF}]
   delta = GetTickCount - startTime;
{$OVERFLOWCHECKS ON}

但是减法仍然会引发EIntOverflow异常。

有没有更好的解决方案,涉及强制转换和更大的中间变量类型?


更新

我问的另一个 SO 问题解释了为什么{$OVERFLOWCHECKS}不起作用。它显然只适用于功能级别,而不是级别。因此,虽然以下内容不起作用

{$OVERFLOWCHECKS OFF}]
   delta = GetTickCount - startTime;
{$OVERFLOWCHECKS ON}

以下确实有效:

delta := Subtract(GetTickCount, startTime);

{$OVERFLOWCHECKS OFF}]
   function Subtract(const B, A: DWORD): DWORD;
   begin
      Result := (B - A);
   end;
{$OVERFLOWCHECKS ON}
4

4 回答 4

6

像这样一个简单的功能怎么样?

function GetElapsedTime(LastTick : Cardinal) : Cardinal;
var CurrentTick : Cardinal;
begin
  CurrentTick := GetTickCount;
  if CurrentTick >= LastTick then
    Result := CurrentTick - LastTick
  else
    Result := (High(Cardinal) - LastTick) + CurrentTick;
end;

所以你有了

StartTime := GetTickCount
...
if GetElapsedTime(StartTime) > 10000 then
...

只要 StartTime 和当前 GetTickCount 之间的时间小于臭名昭著的 GetTickCount 49.7 天范围,它就会起作用。

于 2010-03-17T15:08:51.203 回答
5

在编写了一些被调用的辅助函数之后,我已经停止在任何地方进行这些计算。

要在 Vista 及更高版本上使用新GetTickCount64()功能,有以下新类型:

type
  TSystemTicks = type int64;

用于所有此类计算。GetTickCount()从不直接调用,GetSystemTicks()而是使用辅助函数:

type
  TGetTickCount64 = function: int64; stdcall;
var
  pGetTickCount64: TGetTickCount64;

procedure LoadGetTickCount64;
var
  DllHandle: HMODULE;
begin
  DllHandle := LoadLibrary('kernel32.dll');
  if DllHandle <> 0 then
    pGetTickCount64 := GetProcAddress(DllHandle, 'GetTickCount64');
end;

function GetSystemTicks: TSystemTicks;
begin
  if Assigned(pGetTickCount64) then
    Result := pGetTickCount64
  else
    Result := GetTickCount;
end;

// ...

initialization
  LoadGetTickCount64;
end.

您甚至可以手动跟踪返回值的环绕,GetTickCount()并在早期系统上返回真正的 64 位系统滴答计数,如果您GetSystemTicks()至少每隔几天调用一次该函数,这应该可以很好地工作。[我似乎记得某处的实现,但不记得它在哪里。gabr 发布了一个链接和实现。]

现在实现这样的功能很简单

function GetTicksRemaining(...): TSystemTicks;
function GetElapsedTicks(...): TSystemTicks;
function IsTimeRunning(...): boolean;

这将隐藏细节。调用这些函数而不是就地计算持续时间也可以作为代码意图的文档,因此需要较少的注释。

编辑:

你在评论中写道:

但是就像你说的,在 Windows 2000 和 XP 上回退到 GetTickCount 仍然留下了最初的问题。

您可以轻松解决此问题。首先,您不需要退GetTickCount()- 您也可以使用提供的代码 gabr 来计算旧系统上的 64 位滴答计数。(如果需要,可以替换timeGetTime()GetTickCount)。)

但是,如果您不想这样做,您也可以在辅助函数中禁用范围和溢出检查,或者检查被减数是否小于减数并通过添加 $100000000 (2^32) 来模拟64 位滴答计数。或者在汇编程序中实现这些函数,在这种情况下,代码没有检查(不是我建议这样做,但这是一种可能性)。

于 2010-03-10T15:41:54.723 回答
3

您还可以使用 DSiWin32 中的DSiTimeGetTime64

threadvar
  GLastTimeGetTime: DWORD;
  GTimeGetTimeBase: int64;

function DSiTimeGetTime64: int64;
begin
  Result := timeGetTime;
  if Result < GLastTimeGetTime then
    GTimeGetTimeBase := GTimeGetTimeBase + $100000000;
  GLastTimeGetTime := Result;
  Result := Result + GTimeGetTimeBase;
end; { DSiTimeGetTime64 }
于 2010-03-10T16:50:43.680 回答
1

您可以使用 Int64 数据类型来避免溢出:

var
  Start, Delta : Int64;
begin
  Start := GetTickCount;
  ...
  Delta := GetTickCount - start;
  if (Delta > 10000) then
    ...
于 2010-03-10T15:31:40.440 回答