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-07 12:58
1

Ja do mierzenia czasu trwania procesów używam GetTickCount

procedure Tmain.btn1Click(Sender: TObject);
var
  start: Integer;
begin
  start := GetTickCount;
  Sleep(1000);
  ShowMessage(IntToStr(GetTickCount - start));
end;

Pozostało 580 znaków

2019-05-07 14:42
0

TStopWatch jest bardziej precyzyjny od GetTickCount, ale to nie o to chodzi. Ja się raczej zastanawiam, czy wątki skonstruowane w taki sposób dają miarodajny wynik, czy nie. No i nadal nie wiem, czym mógłbym zastąpić nieprecyzyjny Sleep.

Pozostało 580 znaków

2019-05-07 15:44
1

a po co Ci drugi wątek? Nie możesz tego policzyć w tym jednym? BTW jeśli całe rysowanie masz w synchronize to równie dobrze możesz zrezygnować z wątku


Chcesz pomocy - pokaż kod - abrakadabra źle działa z techniką.

Pozostało 580 znaków

2019-05-07 16:04
1

no sleep niczym nie zastąpisz, powinieneś go po prostu wywalić. procesor obciążony na 100%? Logiczne - w tym przypadku program będzie działał z pełną wydajnością; niezależnie jak szybki będzie procesor zawsze będzie obciążenie 100%. Dlaczego gry i inne programy rysujące nie obciążają na 100%? No bo rysowanie to raczej zadanie dla GPU nie CPU i ten drugi tylko wysyła parametry, tekstury i shadery i zleca rysowanie GPU; powinieneś raczej podążać tym tropem - w tym przypadku CPU nie jest obciążony w 100% tylko dlatego że czeka na GPU, w idealnej konfiguracji dla maksymalnego fps i gpu i cpu jest obciążony blisko w 100 procentach

dla mierzenia czasu wywal sleep całkowicie, lub mierz tylko to co się dzieje w Draw i sumuj
w prawdziwej aplikacji dla oszczędności CPU zrób frame limiter i dostosuj wartość sleepa tak żeby uzyskać stałą liczbę fpsów (nie więcej niż 60), możesz wykorzystać sprzężenie zwrotne i dostosować prędkość rysowania do użycia cpu

Pozostało 580 znaków

2019-05-07 18:49
1
Crow napisał(a):

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ć :).

Mam nadzieję, że Suspend i Resume są oznaczone jako deprecated.


Na ile to co skleciłem jest wiarygodnym miernikiem szybkości?

No niezbyt wiarygodnym. Nie wiem po co Ci do tego wątki, które tylko komplikują całość, nie wiem dlaczego jednostką pomiaru jest framerate, a nie jednostka czasu i nie wiem po co ten Sleep.

Aby mieć najbardziej wiarygodne pomiary, kod należy wykonać w głównym wątku, wielokrotnie, na nieobciążonym pececie, poza debuggerem i innymi narzędziami, które mogą spowalniać wykonanie kodu. Dodatkowo można ustawić wysoki priorytet dla procesu aplikacji testującej. Odpala się pętlę i w niej mierzy się czas wykonania właściwego algorytmu. Czas się mierzy, bo znając czas możesz sobie sam policzyć framerate.

Jeśli o odmierzanie czasu chodzi, to stosuję dwie techniki, obie wykorzystują funkcje QPF i QPC.

Pierwsza technika dotyczy pomiaru szybkości działania danego algorytmu. Oczywiście wołam go setki czy tysiące razy w pętli, pobierając za pomocą funkcji QPC stan licznika przed i po wywołaniu właściwej funkcji. Następnie pobieram rozdzielczość countera za pomocą QPF i przeliczam wynik na jednostkę czasu.

Druga technika dotyczy pomiaru potrzebnego podczas optymalizowania działania danego algorytmu – aby wiedzieć czy poprawiona wersja kodu daje lepsze rezultaty i jeśli tak, to o ile (w procentach). Wtedy przed wywołaniem właściwego kodu i po nim pobieram czas za pomocą QPC i sprawdzam jaka jest różnica, przeliczając ją na procenty.

Powyższe dotyczy testowania różnej maści algorytmów i przedstawiania wyników pomiarów w konsoli. Jeśli test ma dotyczyć generowania grafiki i jej renderowania na ekranie, to dorzucam wywołanie metody odmalowującej komponent/okno (w zależności od przeznaczenia algorytmu) oraz Application.ProcessMessages tuż po wywołaniu właściwej funkcji, ale przed pobraniem stanu licznika, tak aby wymusić przemalowanie płótna i oczyścić kolejkę komunikatów. Bez tego okno nie zostanie fizycznie przemalowane, więc wyniki pomiarów będą nieprawidłowe.


Przykładem jest mój Deep Platformer. Działa on w ten sposób, że generuje klatkę, przemalowuje okno, oczekuje na kolejną klatkę, następnie znów generuje klatkę i tak w kółko (działa bardzo podobnie jak NES), utrzymując 60fps. Na ekranie widoczny jest licznik framerate'u oraz procentowy licznik obciążenia procesora.

Jeśli optymalizuję kod to sprawdzam czy licznik procentowego obciążenia pokazuje niższą wartość niż wcześniej – podczas gry jest to około 41%. Jeśli chcę wiedzieć ile klatek da radę gra wyciągnąć to remuję wywołanie metody oczekującej na kolejną klatkę – otrzymuję 147fps. Jeśli chcę wiedzieć ile klatek gra wyciąga bez odmalowywania okna to remuję wywołanie metody odmalowującej okno – otrzymuję 223fps. A jeśli chcę sprawdzić ile klatek gra wyciągnie bez generowania bitmap klatek i odmalowywania okna (sama logika) to dodatkowo remuję wywołanie metody odpowiedzialnej za renderowanie klatki – otrzymuję około 18.095fps. Wyniki wyrzucam na ekran konsoli.

Proste w użyciu i daje dużo możliwości.


Druga kwestia: Czym zastąpić Sleep?

Zastąp go pustą linijką – nie jest do niczego potrzebny. A nawet inaczej, jest absolutnie zbędny i służy wyłącznie do przekłamywania wyników. Twój obecny program przedstawia wynik w postaci framerate'u, którego wartość jest sztucznie zaniżana przez tę procedurę.


Znajduje się on w obu wątkach tylko dlatego, że pętle bez niego niemalże mordują procesor.

Nie wiem czy wiesz, ale pomiary będą wiarygodne tylko jeśli aplikacja testująca będzie zjadać całą dostępną moc procesora (a właściwie: jednego jego jądra). Jeśli nie będzie tego robić, to wynikowy framerate będzie przekłamany – tym bardziej, im niższe zużycie CPU podczas testowania.

Testowanie szybkości działania algorytmu stricte wielowątkowego to inna para kaloszy.


edytowany 9x, ostatnio: furious programming, 2019-05-11 12:23

Pozostało 580 znaków

2019-05-07 18:55
1

Dzięki za odpowiedzi. Postaram się wyjaśnić trochę więcej. Otóż piszę sobie hobbystycznie klona trójwymiarowego Tetrisa, gdzie klocki układa się w formie wieży na planszy o powierzchni 3x3. To jest androidowy pierwowzór:

W mojej wersji wieża może mieć maksymalnie 20 pięter, z czego każde piętro może składać się z 8 sześcianów (bo 9 oznacza wykasowanie piętra), a każdy sześcian to oczywiście 8 wierzchołków. Razem z podstawką daje to maksymalnie ok. 1300 wierzchołków, które mogą jednocześnie pojawić się na ekranie. To na tyle mało, że wszystkie przekształcenia geometryczne (mnożenia macierzowe), mogę spokojnie robić na CPU i uzyskać przy tym zadowalającą płynność (mój procek zaczyna odczuwać cokolwiek dopiero w okolicy 8-10 tysięcy wierzchołków). Gorzej jest z renderowaniem samych sześcianów, zwłaszcza biorąc pod uwagę, że swoją wersję chcę jeszcze oteksturować. Ogólnie mam zamiar skorzystać z Direct2D (Directem 3D nie chcę się bawić, zwłaszcza że Delphi chyba nawet nie ma sensownego wrappera). Ten jednak - z tego co mi wiadomo - wbudowanego rastra nie ma (poprawcie mnie, jeżeli się mylę). To znaczy może pokryć wielokąt bitmapą (tak jak i GDI czy GDI Plus), ale nie z zastosowaniem perspektywy, niezbędnej przy rzutowaniu obiektu 3D na płaszczyznę. No więc wydaje mi się, że muszę mieć własny raster, zapisywać wynik jego pracy do buffora (tak obecnie robię), a dopiero potem przerzucać go do Direct2D, by ten wyświetlił mi go na ekranie. Napisałem więc ten raster (skorzystałem z algorytmu Juana Pinedy: "A Parallel Algorithm for Polygon Rasterization" i koordynatów barycentrycznych) i nawet jakoś działa:

title

Nie jestem jednak pewien co do jego wydajności, zwłaszcza, gdy na ekranie pojawi się więcej obiektów, stąd muszę mieć jakieś narzędzie porównawcze, które pomoże mi w ewentualnej optymalizacji (a mam jeszcze kilka miejsć, w których mogę uzyskać kilka dodatkowych milisekund, np. poprzez rasteryzację wielowątkową - obecnie używam tylko 1 wątku). Chce więc widzieć co przynosi poprawę, a co nie, bo przecież na oko tego nie zrobię :).

edytowany 3x, ostatnio: Crow, 2019-05-07 19:11
Pokaż pozostałe 11 komentarzy
Half-Life jedynka od początku miał software'owy renderer do wyboru, bo to gra z okresu kiedy nie wszyscy jeszcze mieli karty 3D. - Azarien 2019-05-09 11:09
@Azarien: ale to juz bylo robione na Engine Quake 2 - WhiteLightning 2019-05-09 11:10
Q1 początkowo miał tylko software rendering. potem wyszła wersja na OpenGL, za którą nie przepadam bo brakuje jej niektórych efektów. Q2 miał już oficjalnie software i OpenGL (tym razem zdecydowanie lepiej wyglądający niż soft). - Azarien 2019-05-09 11:48
problem ze wczesnymi grami z akcelerowaną grafiką jest że wtedy zachwycano się brakiem "pikseli" (a właściwie tekseli) - czyli filtrowaniem tekstur - mimo że te tekstury miały żałosną rozdzielczość i wszystko wygląda na rozmyte. czasami wolę ustawić nearest neighbour, jeśli się da. - Azarien 2019-05-09 11:59
Za to pierwsza gra ktora kojarze ze krzyczala ze musi miec DirectX to bylo to: https://en.wikipedia.org/wiki/Nuclear_Strike - WhiteLightning 2019-05-09 12:06

Pozostało 580 znaków

2019-05-07 19:56
0

Podstawowy ”pomocnik” to zwykły licznik framerate'u – on wszystko Ci powie. No ale tego implementacja zależy od implementacji głównej pętli gry – pisałeś ją sam, czy korzystasz z jakiegoś frameworka do tworzenia gier?


Pozostało 580 znaków

2019-05-07 20:00
0

Cały silnik piszę od zera i własnymi siłami (nie licząc pomocy z forum, zwłaszcza twojej; jak skończę, nie omieszkam wspomnieć o tym w creditsach ;D), małymi kroczkami starając się wszystko poskładać do kupy :).

edytowany 3x, ostatnio: Crow, 2019-05-07 20:02

Pozostało 580 znaków

2019-05-07 20:04
0

A w jakiej wersji środowiska programujesz ? Bo rozwiązaniem twojego problemu wydaje się być FireMonkey :)

Pozostało 580 znaków

2019-05-07 20:07
0
PrzemysławWiśniewski napisał(a):

A w jakiej wersji środowiska programujesz ? Bo rozwiązaniem twojego problemu wydaje się być FireMonkey :)

RAD Studio 10.2 (Tokyo Starter)

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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

Robot: CCBot (2x)