Delphi: Как избежать потери значения EIntOverflow при вычитании? - PullRequest
9 голосов
/ 10 марта 2010

Microsoft уже говорит в документации для GetTickCount, что вы никогда не сможете сравнить количество тиков, чтобы проверить, прошел ли интервал. e.g.:

Неправильно (псевдокод):

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+}, {$OVERFLOWCHECKS ON}), вычитание двух отсчетов тиков генерирует исключение EIntOverflow, когда TickCount переворачивается:

if (0x00000100 - 0xffffff00) > 10000

0x00000100 - 0xffffff00 = 0x00000200

Каково решение этой проблемы?

Редактировать: я пытался временно отключить OVERFLOWCHECKS:

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

Но вычитание все равно вызывает исключение EIntOverflow.

Есть ли лучшее решение, включающее приведение типов и большие промежуточные типы переменных?


Обновление

Другой вопрос, который я задал, объяснил, почему {$OVERFLOWCHECKS} не работает. По-видимому, он работает только на уровне function , а не на уровне line . Так что пока следующее не не работает:

{$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 ]

6 голосов
/ 17 марта 2010

Как насчет простой функции, подобной этой?

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 будет меньше, чем печально известный диапазон 49,7 дней GetTickCount.

5 голосов
/ 10 марта 2010

Я перестал выполнять эти вычисления везде после написания нескольких вспомогательных функций, которые вызываются вместо этого.

Для использования новой функции GetTickCount64() в Vista и более поздних версиях существует следующий новый тип:

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;

, что скроет детали. Вызов этих функций вместо расчета длительности на месте служит также для документирования намерений кода, поэтому требуется меньше комментариев.

Edit:

Вы пишете в комментарии:

Но, как вы сказали, откат на Windows 2000 и XP для GetTickCount все еще оставляет исходную проблему.

Вы можете это легко исправить. Во-первых, вам не нужно , чтобы вернуться к GetTickCount() - вы можете использовать код gabr, предоставленный для , и рассчитать 64-битное число тиков на старых системах . (Вы можете заменить timeGetTime() на GetTickCount), если хотите.)

Но если вы не хотите этого делать, вы также можете отключить проверки диапазона и переполнения в вспомогательных функциях или проверить, меньше ли вычитаемое значение, чем вычитаемое, и исправьте его, добавив $ 100000000 (2 ^ 32) имитировать 64-битный отсчет тиков. Или реализуйте функции в ассемблере, и в этом случае код не имеет проверок (не то, чтобы я советовал это, но это возможно).

3 голосов
/ 10 марта 2010

Также можно использовать DSiTimeGetTime64 из 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 }
1 голос
/ 10 марта 2010

Вы можете использовать тип данных Int64, чтобы избежать переполнения:

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