Odmierzanie czasu pomiędzy operacjami, obciążenie procesora

1

Dodałem jeszcze małe usprawnienie, dzięki któremu obniżyłem zużycie mocy CPU o 5-15%.

Skoro Round może zaokrąglić w górę i nakazać procedurze Sleep poczekać nieco więcej, to można tę drugą milisekundę odjąć tylko wtedy, gdy ten zaokrągli w górę. W przeciwnym razie – gdy zaokrągli w dół – dodatkowa milisekunda nie jest odejmowana.

SleepTime := 1000 / FFramerateLimit * (1 - (FFrameTicksEnd - FFrameTicksBegin) / FTicksPerFrame) - 1;
SleepTime -= Ord(Round(SleepTime) > SleepTime);
SleepTime := Max(SleepTime, 0);

Sleep(Round(SleepTime));

NextFrameTicks := FFrameTicksBegin + FTicksPerFrame;

repeat
  CurrentTicks := GetHardwareCounterValue();
until CurrentTicks >= NextFrameTicks;

W pierwszej linijce odejmujemy milisekundę, co zabezpiecza przed niedokładnością Sleep. W drugiej linijce odejmujemy drugą milisekundę, jeśli wynik zaokrąglenia jest większy niż bez zaokrąglenia – to skraca czas do zjedzenia w końcowej pętli, oszczędzając moc procesora. W trzeciej linijce przerabiamy wynik na liczbę nieujemną, bo tylko taką obsługuje Sleep – to zabezpiecza przed wywaleniem wyjątku gdy nastąpi lag, czyli gdy czas generowania klatki będzie dłuższy niż czas przydzielony na jej obsługę.

2

@Crow: ostatecznie potwierdzam prawidłowe działanie powyższego kodu.

Sprawdziłem jak mój platformer działa na trzech komputerach: z WinXP, Win7 oraz z Win10 i okazuje się, że użycie funkcji timeBeginPeriod i timeEndPeriod jest konieczne. Niżej w tabelce znajdują się dane z pomiarów na różnych systemach dotyczące dwóch wersji zegara – używającego tych funkcji oraz nieużywającego.

system Windows XP Windows 7 Windows 10
z funkcjami 60fps 60fps 60fps
bez funkcji 60fps 57fps 41fps

Dlatego też w konstruktorze klasy zegara wołam timeBeginPeriod(1), a w destruktorze timeEndPeriod(1). Te funkcje znajdują się w module MMSystem jakby co. Choć oczywiście najlepiej jest użyć funkcji timeGetDevCaps w celu pobrania zakresu rozdzielczości pracy zegara i do funkcji timeBeginPeriod przekazać tę minimalną. No i zapamiętać tę wartość, aby na koniec podać ją do funkcji timeEndPeriod.

Czyli, podsumowując, systemowy timer ustawić w ten sposób:

var
  FTimerResolution: Integer;

{..}

var
  TimerResolution: TTimeCaps;
begin
  if TimeGetDevCaps(@TimerResolution, SizeOf(TTimeCaps)) = TIMERR_NOERROR then
  begin
    FTimerResolution := TimerResolution.wPeriodMin;
    TimeBeginPeriod(FTimerResolution);
  end
  else
    FTimerResolution := -1;

i na koniec przywrócić jego poprzedni stan w ten sposób:

if FTimerResolution <> -1 then
  TimeEndPeriod(FTimerResolution);

To tyle – miłej zabawy. :]

0

Wszystko spoko (zrozumiałem swój błąd i poprawiłem - dzięki! :]), tylko jedna rzecz coś mi tu nie działa...

procedure Test;
var
  I: Integer;
  Before, After: Int64;
begin
  TimeBeginPeriod(1);
  for I:= 0 to 99 do
    begin
      QueryPerformanceCounter(Before);
      Sleep(1);
      QueryPerformanceCounter(After);
      Form1.Memo1.Lines.Add(IntToStr(After - Before));
    end;
  TimeEndPeriod(1);
end;

Proste demko, powtarza pętlę 100 razy, w której wrzuca do Memo1 obliczony w tickach, rzeczywisty czas snu. Wydaje mi się, że dobrze wywołuję (MSDN też tak podpowiada) TimeBeginPeriod(1) i TimeEndPeriod(1), ale wpływu na wynik nie ma to żadnego, to znaczy jest on nadal ogromnie rozstrzelony i sięga nawet 3000 ticków. Robię coś źle?

1

Wszystko wygląda dobrze, natomiast niedokładność pomiaru (owy rozstrzał) jest spowodowana niedokładnością procedury Sleep i tego nie poprawisz. Dlatego właśnie mechanizm odmierzania czasu musi robić ”coś jeszcze”, aby niwelować tego typu wahania. No i właśnie do tego celu służy dodatkowa pętla, która w kółko odczytuje stan licznika (za pomocą QPC), aby takie wyrównanie przeprowadzić. Sam odczyt stanu licznika jest na tyle krótkotrwały, że w ten sposób można zjadać porcje czasu wynoszące nawet mikrosekundy.

Wykorzystanie funkcji TimeBeginPeriod i TimeEndPeriod nie wzięło się znikąd. Te funkcje są używane w timerach multimedialnych, aby móc odmierzać przerwy z milisekundową precyzją, nie wykorzystując pełnej mocy procesora. Niestety większej precyzji na Windows nie da się uzyskać, jeśli chodzi o procedurę Sleep.

Zmiana rozdzielczości za pomocą tych funkcji jest konieczna, dlatego że domyślnie system może wykorzystywać mniej precyzyjny zegar. Z tego właśnie powodu moja gra na Win10 osiągała zaledwie 41fps (patrz: tabelka) – minimum dla Sleep wynosiło 16ms, stąd tak ogromne opóźnienia. Na Win7 rozdzielczość też nie była wysoka, dlatego nie dało się uzyskać pełnych 60fps. I to właśnie zmienia się za pomocą TimeBeginPeriod – nakazuje się systemowi pracować z wyższą dokładnością.

Pamiętaj tylko, aby nie podawać na pałę wartości 1, bo taka precyzja może nie być obsługiwana – skorzystaj z funkcji TimeGetDevCaps i przekaż minimum, tak jak pokazałem w poprzednim poście. Teoretycznie ta wartość zawsze jest możliwa do wykorzystania, ale pewności nie ma(m). W każdym razie na moim starym laptopie dozwolony przedział wynosi od 1ms do 1000000ms, na nowszych będzie podobnie. Jeśli jednak Twoja gra ma być kompatybilna z jak największą liczbą systemów Windows, to pasuje mieć to na uwadze.

1 użytkowników online, w tym zalogowanych: 0, gości: 1