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

Odpowiedz Nowy wątek
2019-05-07 12:20
0

Kiedyś już zakładałem podobny temat, ale teraz trochę inaczej podszedłem do problemu i osiągnąłem pewne efekty, tylko zastanawia mnie ich wiarygodność. Naskrobałem sobie coś takiego:

type
  TCounter = class(TThread)
    procedure Execute; override;
  end;

  TPainter = class(TThread)
    procedure Execute; override;
  end;

var
  Counter: TCounter;
  Painter: TPainter;
  FPS: Integer;

implementation

procedure UpdateFPS;
begin
  Form1.Caption:= IntToStr(FPS);
end;

procedure TCounter.Execute;
var
  Elapsed: Int64;
  Watch: TStopWatch;
begin
  FreeOnTerminate:= True;
  Watch:= TStopWatch.StartNew;
  while not (Terminated = True) and not (Application.Terminated = True) do
    begin
      Elapsed:= Watch.ElapsedMilliseconds;
      if Elapsed >= 1000 then
        begin
          Synchronize(UpdateFPS);
          Elapsed:= 0;
          Watch.Reset;
          Watch.Start;
          FPS:= 0;
        end;
      Sleep(1);
    end;
end;

procedure TPainter.Execute;
begin
  while not (Terminated = True) and not (Application.Terminated = True) do
    begin
      Synchronize(Draw); //Procedurka służąca do generowania obrazka i rysowania go na ekranie przy pomocy TPaintBox
      Inc(FPS, 1);
      Sleep(1);
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Counter.Resume;
  Painter.Resume;
end;

initialization
  Counter:= TCounter.Create(True);
  Painter:= TPainter.Create(True);

finalization
  Counter.Terminate;
  Painter.Terminate;

Od razu zaznaczam: Tak, wiem, nie powinno się używać Suspend i Resume (bo są "przestarzałe"), tylko TMutex i TEvent, ale to tak na szybko, żeby sobie nie komplikować :).

No a właściwie pytanie jest takie: Na ile to co skleciłem jest wiarygodnym miernikiem szybkości? Jestem na etapie testowania różnych algorytmów rasteryzacji i teksturowania, a metoda badawcza "na oko" nie jest wystarczająca, bo muszę mieć twarde dane i wiedzieć, co działa szybciej, a co wolniej.

Druga kwestia: Czym zastąpić Sleep? Znajduje się on w obu wątkach tylko dlatego, że pętle bez niego niemalże mordują procesor. Sam TCounter (którego jedynym zadaniem jest odświeżanie licznika na belce) bez Sleep potrafi wytwarzać nawet 10% obciążenia CPU, a TPainter nawet ponad 20%. Sleep z tego co wiem, nie jest precyzyjne i nie zawsze trwa tyle samo. W przypadku licznika nie stanowi to wielkiego problemu, w końcu nie jest aż tak istotne, czy jego odświeżenie nastąpi raz na 1000 czy tam 1020 milisekund (gołym okiem to nawet niezauważalne), przy rysowaniu "klatek" to już jednak ma znaczenie (i np. bez Sleep(1) uzyskuję 280-290 klatek, z nim jakieś 195-200). Poza tym nawet gdy używam Sleep(1), obciążenie CPU jest dosyć duże, bo wynosi ok. 10%, a CPU mam naprawdę niezły, tj. Intel i7 7700k.

Trzecia sprawa (trochę uboczna): Orientuje się ktoś, czy Direct2D posiada jakieś wbudowane funkcje do mierzenia szybkości renderowania (ilość klatek na sekundę)? Na logikę wydaje się, że tak (bo przecież jest to - nieco uboższy ale jednak - krewniak Direct3D), ale przeszukując neta, na nic takiego nie trafiłem, ani w Delphi, ani w C++.

edytowany 2x, ostatnio: furious programming, 2019-05-07 17:59

Pozostało 580 znaków

2019-05-13 00:39
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. :]


edytowany 7x, ostatnio: furious programming, 2019-05-13 03:31

Pozostało 580 znaków

2019-05-14 14:42
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?

edytowany 1x, ostatnio: Crow, 2019-05-14 14:43

Pozostało 580 znaków

2019-05-14 18:22
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.


edytowany 5x, ostatnio: furious programming, 2019-05-14 21:41
Skorzystałem z TimeGetDevCapsi najmniejszą obsługiwaną wartością było właśnie 1 ms, stąd na potrzeby dema, od razu użyłem stałej :). Natomiast w silniku oczywiście najpierw robię odczyt. Czyli generalnie TimeBeginPeriod i TimeEndPeriod poprawiają precyzję... ale jednak nie aż tak bardzo? To znaczy jak rozumiem ten przyrost dokładności będzie zauważalny, ale nie dla 1 ms, a dla nieco większych interwałów, np. 7-8 ms? Kurde, jakie to jest słabe, że pod Windowsem nie ma sensownego sposobu na precyzyjne odczekiwanie w bezczynności, które oszczędzałoby moc CPU. - Crow 2019-05-14 21:12
Nie poprawią precyzji nie wiadomo jak bardzo, bo Sleep pobiera wartość jako liczbę całkowitą – 0 nic nie robi, a 1 to sensowne minimum. Tyle że jak poda się 1 to program zostanie wstrzymany na 2ms, jak poda się 2 to na 3ms, dla większych wartości precyzja będzie wyższa. No ale w dalszym ciągu jest jakaś niedokładność, którą trzeba niwelować pętlami zjadającymi. - furious programming 2019-05-14 21:35
Linuks ma funkcję nanosleep, która pobiera w parametrach strukturkę zawierającą liczbę całych sekund i nanosekund (jako część kolejnej sekundy) typu timespec, więc można sobie zrobić bardzo precyzyjne odmierzanie czasu, co wykorzystałem w swoim platformerze (port dla Uniksów). I choć wiadomo, że ta funkcja też nie jest ultra-dokładna, to i tak bije na głowę funkcję Sleep z Windows. - furious programming 2019-05-14 21:37
W razie czego, na moim staruszku pętla zjadająca działa z dokładnością do 2μs (około 480 wywołań QPC porzypadających na każdą milisekundę). Na nowszym sprzęcie będzie to więcej wywołań, a więc jeszcze wyższa dokładność. - furious programming 2019-05-15 08:56

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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