Jak dobrze zaimplementować busy waiting?

0

Wiem że to dziwne pytanie, ale dla mnie jest istotne. ;)

Mam sobie klasę zegara, który zapewnia stały framerate — może obsługiwać dowolny, choć w projektach używam 60fps oraz 50fps. Działa on w ten sposób, że czas wykonania danej klatki oraz czas do odczekania do kolejnej klatki mierzy z dużą dokładnością (do ~mikrosekundy). Korzystam z timerów multimedialnych, więc Sleep może wstrzymać działanie wątku najmniej o 1ms. Jednak czasem wstrzymuje o milisekundę dłużej niż się mu każe i framerate spada o kilka klatek (z reguły 2-3).

Aby temu zapobiec, wykorzystuję funkcję Sleep do odczekania zawsze o jedną milisekundę mniej, a resztę czasu zjadam busy waitingiem (poniżej milisekundy). Dzięki temu framerate jest super-stabilny, elegancko utrzymuje pożądaną wartość, a niewykorzystaną moc obliczeniową oddaję innym procesom. Obecnie kod oczekujący na kolejną klatkę wygląda tak:

procedure TClock.UpdateFrameAlign();
begin
  while GetCounterValue() < FFrameTicksNext - FTicksPerFrame do
    Sleep(1);

  while GetCounterValue() < FFrameTicksNext do
    ;
end;

W pierwszej pętli sprawdzany jest stan sprzętowego licznika (całość bazuje na wartościach pobranych lub obliczonych na podstawie QueryPerformanceCounter) i jeśli można odczekać milisekundę to jest odczekiwana. I tak w kółko, aż czasu do odczekania zostanie mniej niż milisekunda — wtedy wykonywana jest druga pętla, czyli zwykły busy waiting. No i on mnie zastanawia.

Czy współczesne procesory będą się wkurzać na sukcesywne odczytywanie licznika sprzętowego i nie robienie niczego? Czy może lepiej zająć go jakimiś nic nie znaczącymi instrukcjami, np. nop-ami? Czy to ma w ogóle jakieś znaczenie?

1
procedure TClock.UpdateFrameAlign();
begin
  while GetCounterValue() < FFrameTicksNext - FTicksPerFrame do
    Sleep(1);

  while GetCounterValue() < FFrameTicksNext do
  asm
    pause
  end;
end;

Działa tak jak ma działać, a Intel dodatkowo obiecuje ciut niższe zużycie energii. Ktoś ma jakieś obiekcje co do tej pętli? Robić coś jeszcze czy sam pause wystarczy?


Edit: ostateczna wersja dla SDL-a wygląda tak:

procedure TClock.UpdateFrameAlign();
var
  SleepTime: Single;
begin
  SleepTime := 1000 / FFrameRateLimit * (1 - (FFrameTicksEnd - FFrameTicksBegin) / FTicksPerFrame) - 1;
  SleepTime -= Ord(Round(SleepTime) > SleepTime);
  SleepTime := Max(SleepTime, 0);

  SDL_Delay(Round(SleepTime));

  while GetCounterValue() < FFrameTicksNext do
  asm
    pause
  end;
end;

Wszystko dlatego, że SDL_Delay(1) robi busy waiting samo w sobie (pod Windows), a zwykły Windows.Sleep nie działa w połączeniu z SDL. Dlatego rozwiązaniem jest obliczenie ile sekund trzeba poczekać w zaokrągleniu (odjąć jedną milisekundę od tego), jeśli wyszło mniej niż 0 to ustawić 0 i dopiero wtedy SDL_Delay. Powyższe działa idealnie.

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