Delphi:减法时如何避免EIntOverflow下溢?

Ian*_*oyd 10 delphi integer-overflow delphi-5

微软已经在GetTickCount的文档中说过,你永远无法比较滴答计数来检查是否已经过了一段时间.例如:

不正确(伪代码):

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

...

if (GetTickCount > endTime)
   break;
Run Code Online (Sandbox Code Playgroud)

上面的代码很糟糕,因为它可以对tick计数器进行翻转.例如,假设时钟接近其范围的结尾:

endTime = 0xfffffe00 + 10000
        = 0x00002510; //9,488 decimal
Run Code Online (Sandbox Code Playgroud)

然后你执行检查:

if (GetTickCount > endTime)
Run Code Online (Sandbox Code Playgroud)

这是立刻感到满意,因为GetTickCount 大于endTime:

if (0xfffffe01 > 0x00002510)
Run Code Online (Sandbox Code Playgroud)

解决方案

相反,你应该总是减去两个时间间隔:

DWORD startTime = GetTickCount;

...

if (GetTickCount - startTime) > 10000 //if it's been 10 seconds
   break;
Run Code Online (Sandbox Code Playgroud)

看着同样的数学:

if (GetTickCount - startTime) > 10000

if (0xfffffe01 - 0xfffffe00) > 10000

if (1 > 10000)
Run Code Online (Sandbox Code Playgroud)

在C/C++中,这一点都很好,编译器在某种程度上表现得很好.

但是德尔福呢?

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

if (0x00000100 - 0xffffff00) > 10000

0x00000100 - 0xffffff00 = 0x00000200
Run Code Online (Sandbox Code Playgroud)

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

编辑:我试图暂时关闭OVERFLOWCHECKS:

{$OVERFLOWCHECKS OFF}]
   delta = GetTickCount - startTime;
{$OVERFLOWCHECKS ON}
Run Code Online (Sandbox Code Playgroud)

但减法仍然会引发EIntOverflow异常.

是否有更好的解决方案,包括演员表和更大的中间变量类型?


更新

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

{$OVERFLOWCHECKS OFF}]
   delta = GetTickCount - startTime;
{$OVERFLOWCHECKS ON}
Run Code Online (Sandbox Code Playgroud)

下面工作:

delta := Subtract(GetTickCount, startTime);

{$OVERFLOWCHECKS OFF}]
   function Subtract(const B, A: DWORD): DWORD;
   begin
      Result := (B - A);
   end;
{$OVERFLOWCHECKS ON}
Run Code Online (Sandbox Code Playgroud)

Ken*_*ssa 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;
Run Code Online (Sandbox Code Playgroud)

所以你有了

StartTime := GetTickCount
...
if GetElapsedTime(StartTime) > 10000 then
...
Run Code Online (Sandbox Code Playgroud)

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


mgh*_*hie 5

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

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

type
  TSystemTicks = type int64;
Run Code Online (Sandbox Code Playgroud)

用于所有这些计算.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.
Run Code Online (Sandbox Code Playgroud)

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

现在实现像这样的函数是微不足道的

function GetTicksRemaining(...): TSystemTicks;
function GetElapsedTicks(...): TSystemTicks;
function IsTimeRunning(...): boolean;
Run Code Online (Sandbox Code Playgroud)

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

编辑:

你写评论:

但就像你说的那样,Windows 2000和XP对GetTickCount的回退仍然存在原始问题.

你可以轻松解决这个问题.首先,你不要需要回落到GetTickCount()-你可以使用所提供的代码贾布尔计算一个64位的时间计数在旧系统也是如此.(您可以替换timeGetTime()使用GetTickCount),如果你想要的.)

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