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

发布于 2024-08-24 05:13:22 字数 1954 浏览 9 评论 0 原文

微软已经在 GetTickCount 的文档中表示,您永远无法通过比较滴答计数来检查间隔是否已过去。例如:

不正确(伪代码):

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

...

if (GetTickCount > endTime)
   break;

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

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

然后执行检查:

if (GetTickCount > endTime)

立即满足,因为 GetTickCount is 大于 endTime< /code>:

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+}, {$OVERFLOWCHECKS ON}) 时,两个滴答计数相减会生成 EIntOverflow TickCount 翻转时出现异常:

if (0x00000100 - 0xffffff00) > 10000

0x00000100 - 0xffffff00 = 0x00000200

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

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

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

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

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


更新

我问的另一个问题解释了为什么 {$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}

Microsoft already says, in the documentation for GetTickCount, that you could never compare tick counts to check if an interval has passed. e.g.:

Incorrect (pseudo-code):

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

...

if (GetTickCount > endTime)
   break;

The above code is bad because it is suceptable to rollover of the tick counter. For example, assume that the clock is near the end of it's range:

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

Then you perform your check:

if (GetTickCount > endTime)

Which is satisfied immediatly, since GetTickCount is larger than endTime:

if (0xfffffe01 > 0x00002510)

The solution

Instead you should always subtract the two time intervals:

DWORD startTime = GetTickCount;

...

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

Looking at the same math:

if (GetTickCount - startTime) > 10000

if (0xfffffe01 - 0xfffffe00) > 10000

if (1 > 10000)

Which is all well and good in C/C++, where the compiler behaves a certain way.

But what about Delphi?

But when i perform the same math in Delphi, with overflow checking on ({Q+}, {$OVERFLOWCHECKS ON}), the subtraction of the two tick counts generates an EIntOverflow exception when the TickCount rolls over:

if (0x00000100 - 0xffffff00) > 10000

0x00000100 - 0xffffff00 = 0x00000200

What is the intended solution for this problem?

Edit: i've tried to temporarily turn off OVERFLOWCHECKS:

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

But the subtraction still throws an EIntOverflow exception.

Is there a better solution, involving casts and larger intermediate variable types?


Update

Another SO question i asked explained why {$OVERFLOWCHECKS} doesn't work. It apparently only works at the function level, not the line level. So while the following doesn't work:

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

the following does work:

delta := Subtract(GetTickCount, startTime);

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

小耗子 2024-08-31 05:13:22

像这样的简单函数怎么样?

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 天范围,它就可以工作。

How about a simple function like this one?

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;

So you have

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

It will work as long as the time between StartTime and the current GetTickCount is less than the infamous 49.7 days range of GetTickCount.

千里故人稀 2024-08-31 05:13:22

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

使用新的 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()< /code> 返回值并在早期系统上返回真正的 64 位系统滴答计数,如果您至少每隔几天调用 GetSystemTicks() 函数,那么它应该可以很好地工作。 [我似乎记得某处的实现,但不记得它在哪里。 gabr 发布了 链接和实现。]

现在实现这样的函数很简单,

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

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

编辑:

您在评论中写道:

但正如您所说,Windows 2000 和 XP 上的 GetTickCount 回退仍然保留了原来的问题。

您可以轻松解决此问题。首先,您不需要需要回退到GetTickCount() - 您可以使用提供给在旧系统上也计算 64 位滴答计数。 (如果需要,您可以将 timeGetTime() 替换为 GetTickCount)。)

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

I have stopped doing these calculations everywhere after writing a few helper functions that are called instead.

To use the new GetTickCount64() function on Vista and later there is the following new type:

type
  TSystemTicks = type int64;

which is used for all such calculations. GetTickCount() is never called directly, the helper function GetSystemTicks() is used instead:

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.

You could even manually track the wrap-around of the GetTickCount() return value and return a true 64 bit system tick count on earlier systems too, which should work fairly well if you call the GetSystemTicks() function at least every few days. [I seem to remember an implementation of that somewhere, but don't remember where it was. gabr posted a link and the implementation.]

Now it's trivial to implement functions like

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

that will hide the details. Calling these functions instead of calculating durations in-place serves also as documentation of the code intent, so less comments are necessary.

Edit:

You write in a comment:

But like you said, the fallback on Windows 2000 and XP to GetTickCount still leaves the original problem.

You can fix this easily. First you don't need to fall back to GetTickCount() - you can use the code gabr provided to calculate a 64 bit tick count on older systems as well. (You can replace timeGetTime() with GetTickCount) if you want.)

But if you don't want to do that you can just as well disable range and overflow checks in the helper functions, or check whether the minuend is smaller than the subtrahend and correct for that by adding $100000000 (2^32) to simulate a 64 bit tick count. Or implement the functions in assembler, in which case the code doesn't have the checks (not that I would advise this, but it's a possibility).

挖个坑埋了你 2024-08-31 05:13:22

您还可以使用 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 }

You can also use DSiTimeGetTime64 from the DSiWin32:

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 }
蒲公英的约定 2024-08-31 05:13:22

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

var
  Start, Delta : Int64;
begin
  Start := GetTickCount;
  ...
  Delta := GetTickCount - start;
  if (Delta > 10000) then
    ...

You can use the Int64 datatype to avoid overflow:

var
  Start, Delta : Int64;
begin
  Start := GetTickCount;
  ...
  Delta := GetTickCount - start;
  if (Delta > 10000) then
    ...
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文