Lubię myśleć o wydajności w kategoriach „ limitów ”. Jest to przydatny sposób na konceptualizację dość skomplikowanego, połączonego systemu. Gdy masz problem z wydajnością, zadajesz pytanie: „Jakie limity uderzam?” (Lub: „Czy jestem związany procesorem / kartą graficzną?”)
Możesz podzielić go na wiele poziomów. Na najwyższym poziomie masz CPU i GPU. Możesz być związany z procesorem (GPU siedzi bezczynnie i czeka na procesor) lub może być związany z GPU (procesor czeka na GPU). Oto dobry post na blogu na ten temat.
Możesz to dalej rozbić. Po stronie procesora możesz wykorzystywać wszystkie swoje cykle na danych znajdujących się już w pamięci podręcznej procesora. Lub możesz mieć ograniczoną pamięć , pozostawiając procesor bezczynny, czekając na dane z pamięci głównej ( zoptymalizuj układ danych ). Nadal możesz to zepsuć.
(Podczas gdy robię szeroki przegląd wydajności w odniesieniu do XNA, zwrócę uwagę, że przydział typu referencyjnego ( class
nie struct
), chociaż normalnie tani, może spowodować wyrzucanie elementów bezużytecznych, co spowoduje spalenie wielu cykli - szczególnie na Xbox 360 . Zobacz tutaj szczegóły).
Po stronie GPU zacznę od wskazania tego doskonałego posta na blogu, który zawiera wiele szczegółów. Jeśli chcesz uzyskać szalony poziom szczegółowości w przygotowaniu, przeczytaj tę serię postów na blogu . ( Oto prostszy ).
Mówiąc prościej, niektóre z dużych to: „ limit wypełnienia ” (liczba pikseli, które można zapisać w buforze backbuffera - często jak dużo overdraw można mieć), „ limit shadera ” (jak skomplikowane mogą być twoje shadery i ile danych można przez nie przepchnąć), „ pobieranie tekstur / limit przepustowości tekstur ” (ile danych tekstur można uzyskać dostęp).
I teraz dochodzimy do dużego - o to tak naprawdę pytasz - gdzie procesor i GPU muszą oddziaływać (za pośrednictwem różnych interfejsów API i sterowników). Luźno istnieje „ limit partii ” i „ przepustowość ”. (Należy zauważyć, że pierwsza część serii, o której wspomniałem wcześniej, zawiera szczegółowe informacje).
Ale w zasadzie partia ( jak już wiesz ) dzieje się za każdym razem, gdy wywołasz jedną z GraphicsDevice.Draw*
funkcji (lub część XNA, na przykład SpriteBatch
, robi to za Ciebie). Jak już bez wątpienia czytałeś, dostajesz kilka tysięcy * na ramkę. Jest to limit procesora - więc konkuruje z innym wykorzystaniem procesora. Zasadniczo jest to sterownik pakujący wszystko o tym, co kazałeś mu narysować, i wysyłający go do GPU.
A potem jest przepustowość do GPU. To ile surowych danych możesz tam przenieść. Obejmuje to wszystkie informacje o stanie, które towarzyszą wsadom - wszystko, od ustawiania stanu renderowania i stałych / parametrów modułu cieniującego (który obejmuje takie rzeczy jak świat / widok / matryce projektu), aż do wierzchołków podczas korzystania z DrawUser*
funkcji. Obejmuje ona także żadnych połączeń do SetData
i GetData
na fakturach, bufory wierzchołków itp
W tym miejscu powinienem powiedzieć, że wszystko, co można wywołać SetData
(tekstury, bufory wierzchołków i indeksów itp.), A także Effect
s - pozostaje w pamięci GPU. Nie jest stale wysyłane ponownie do GPU. Polecenie rysowania, które odwołuje się do tych danych, jest po prostu wysyłane ze wskaźnikiem do tych danych.
(Również: możesz wysyłać polecenia rysowania tylko z głównego wątku, ale możesz SetData
z dowolnego wątku.)
XNA komplikuje nieco z jego czynią klas państwowych ( BlendState
, DepthStencilState
itp). Dane o stanie są wysyłane dla każdego losowania (w każdej partii). Nie jestem w 100% pewien, ale mam wrażenie, że jest wysyłany leniwie (wysyła tylko stan, który się zmienia). Tak czy inaczej, zmiany stanu są tanie do tego stopnia, że są wolne od kosztu partii.
Na koniec należy wspomnieć o wewnętrznym potoku GPU . Nie chcesz wymuszać jego opróżnienia przez zapis do danych, które wciąż muszą czytać, lub odczytywanie danych, które wciąż muszą zapisywać. Opróżnianie potoku oznacza, że czeka on na zakończenie operacji, dzięki czemu wszystko jest w spójnym stanie po uzyskaniu dostępu do danych.
Dwa szczególne przypadki, na które należy uważać, to: Wzywanie GetData
czegokolwiek dynamicznego - szczególnie takiego RenderTarget2D
, na którym GPU może pisać. Jest to bardzo niekorzystne dla wydajności - nie rób tego.
Innym przypadkiem jest wywoływanie SetData
buforów wierzchołków / indeksów. Jeśli musisz to robić często, użyj DynamicVertexBuffer
(także DynamicIndexBuffer
). Pozwalają one GPU wiedzieć, że często się zmieniają, i wykonać wewnętrzną magię buforowania, aby uniknąć przepłukiwania rurociągu.
(Należy również pamiętać, że bufory dynamiczne są szybsze niż DrawUser*
metody - ale muszą być wstępnie przydzielone przy maksymalnym wymaganym rozmiarze).
... I to prawie wszystko, co wiem o wydajności XNA :)