Ogólnie, jak często i kiedy powinienem optymalizować kod?

13

W „normalnym” etapie optymalizacji programowania biznesowego często pozostaje się do momentu, gdy naprawdę jest potrzebny. Oznacza to, że nie powinieneś optymalizować, dopóki nie jest naprawdę potrzebny.

Pamiętaj, co powiedział Donald Knuth „Powinniśmy zapomnieć o małej wydajności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła”

Kiedy należy zoptymalizować, aby nie marnować wysiłku. Czy powinienem to zrobić na poziomie metody? Poziom klasy? Poziom modułu?

Co też powinien mierzyć mój pomiar optymalizacji? Kleszcze? Częstotliwość wyświetlania klatek? Czas całkowity?

David Basarab
źródło

Odpowiedzi:

18

Tam, gdzie pracowałem, zawsze używamy wielu poziomów profilowania; jeśli zauważysz problem, po prostu przesuń się nieco dalej w dół listy, aż zorientujesz się, co się dzieje:

  • „Human profiler”, czyli po prostu zagraj w grę ; czy czasami wydaje się powolny lub „zaczep”? Zauważyłeś gwałtowne animacje? (Jako programista pamiętaj, że będziesz bardziej wrażliwy na niektóre problemy z wydajnością i nieświadomy innych. Odpowiednio zaplanuj dodatkowe testy).
  • Włącz wyświetlacz FPS , który jest przesuwanym 5-sekundowym średnim FPS. Bardzo małe koszty ogólne do obliczenia i wyświetlenia.
  • Włącz paski profilu , które są tylko serią quadów (kolory ROYGBIV), które reprezentują różne części ramki (np. Vblank, preframe, update, collision, render, postframe) za pomocą prostego timera „stopera” wokół każdej sekcji kodu . Aby podkreślić to, czego chcemy, ustawiliśmy pasek o szerokości jednego ekranu, aby był reprezentatywny dla ramki docelowej 60 Hz, więc naprawdę łatwo jest sprawdzić, czy np. Masz 50% mniej budżetu (tylko pół słupka), czy 50% więcej ( pasek owija się i staje się półtora paska). Łatwo jest również stwierdzić, co ogólnie zjada większość ramki: czerwony = renderowanie, żółty = aktualizacja itp.
  • Zbuduj specjalną oprzyrządowaną kompilację, która wstawia „stoper” jak kod wokół każdej funkcji. (Pamiętaj, że możesz przy tym zrobić ogromne uderzenie wydajności, dcache i icache, więc jest to zdecydowanie ingerujące. Ale jeśli brakuje odpowiedniego profilera próbkowania lub przyzwoitej obsługi procesora, jest to dopuszczalna opcja. Możesz także być sprytny o nagrywaniu minimum danych o wejściu / wyjściu funkcji i późniejszej przebudowie śladów wywołań.) Kiedy budowaliśmy nasze, naśladowaliśmy wiele formatu wyjściowego gprof .
  • Co najlepsze, uruchom profilowanie próbkowania ; VTune i CodeAnalyst są dostępne dla x86 i x64, masz różne środowiska symulacji lub emulacji, które mogą dawać ci dane tutaj.

(Zabawna historia z ubiegłorocznego GDC programisty grafiki, który zrobił sobie cztery zdjęcia - szczęśliwego, obojętnego, zirytowanego i wściekłego - i pokazał odpowiednie zdjęcie w rogu wewnętrznych kompilacji na podstawie liczby klatek na sekundę. twórcy treści szybko nauczyli się nie włączać skomplikowanych shaderów dla wszystkich swoich obiektów i środowisk: rozgniewałyby programistę. Zobacz moc sprzężenia zwrotnego).

Pamiętaj, że możesz również robić zabawne rzeczy, takie jak ciągłe wykresy „pasków profilu”, dzięki czemu możesz zobaczyć wzorce pików („tracimy ramkę co 7 klatek”) lub podobne.

Aby odpowiedzieć bezpośrednio na twoje pytanie: z mojego doświadczenia, podczas gdy kuszące (i często satysfakcjonujące - zwykle czegoś się uczę), przepisanie pojedynczych funkcji / modułów w celu zoptymalizowania liczby instrukcji lub wydajności icache lub dcache, i faktycznie musimy to zrobić czasami, gdy mamy szczególnie wstrętny problem z wydajnością, zdecydowana większość problemów z wydajnością, z którymi regularnie się borykamy, sprowadza się do projektowania . Na przykład:

  • Czy powinniśmy buforować w pamięci RAM lub ponownie ładować z dysku ramki animacji stanu „ataku” odtwarzacza? Co powiesz na każdego wroga? Nie mamy pamięci RAM, aby wykonać je wszystkie, ale obciążenia dysków są drogie! Możesz zobaczyć zaczep, jeśli pojawi się 5 lub 6 różnych wrogów naraz! (Dobra, a co powiesz na oszałamiające tarło?)
  • Czy wykonujemy jeden rodzaj operacji na wszystkich cząsteczkach, czy wszystkie operacje na pojedynczej cząsteczce? (Jest to kompromis icache / dcache, a odpowiedź nie zawsze jest jasna.) Co powiesz na rozebranie wszystkich cząstek i przechowywanie razem pozycji (słynna „struktura tablic”) w porównaniu do przechowywania wszystkich danych cząstek w jednym miejscu („ tablica struktur ”).

Słyszysz go, dopóki nie stanie się nieprzyjemny na kursach informatycznych na uniwersytecie, ale: tak naprawdę chodzi o struktury danych i algorytmy. Poświęcenie czasu na projektowanie algorytmów i przepływów danych zapewni ci więcej korzyści. (Upewnij się, że zapoznałeś się z doskonałymi slajdami z zakresu programowania obiektowego przedstawionymi przez pracowników Sony Developer Services, aby uzyskać wgląd tutaj.) To nie „wydaje się” jak optymalizacja; to głównie czas spędzany z tablicą lub narzędziem UML lub tworzeniem wielu prototypów, zamiast przyspieszania bieżącego kodu. Ale ogólnie jest o wiele bardziej opłacalne.

I jeszcze jedna przydatna heurystyka: jeśli jesteś blisko „rdzenia” silnika, warto zoptymalizować (np. Wektoryzację mnożenia macierzy!) Dodatkowego eksperymentu i eksperymentów. Im dalej od rdzenia, tym mniej powinieneś się tym przejmować, chyba że jedno z narzędzi profilujących mówi inaczej.

leander
źródło
6
  1. Korzystaj z odpowiednich struktur danych i algorytmów z góry.
  2. Nie dokonuj mikrooptymalizacji, dopóki nie profilujesz i nie wiesz dokładnie, gdzie znajdują się Twoje gorące punkty.
  3. Nie martw się o bycie sprytnym. Kompilator wykonuje już wszystkie małe sztuczki, o których myślisz („Och! Muszę pomnożyć przez cztery! Przesunę dwa w lewo!”)
  4. Zwróć uwagę na brakujące pamięci podręczne.
hojny
źródło
1
Poleganie na kompilatorze jest inteligentne tylko do pewnego momentu. Tak, wykona pewne optymalizacje wizjera, o których nie pomyślałbyś (i nie mógłbyś obejść się bez montażu), ale nie ma pojęcia o tym, co powinien zrobić twój algorytm, więc nie może przeprowadzić inteligentnych optymalizacji. Byłbyś również zaskoczony, ile cykli możesz wygrać, wdrażając krytyczny kod w asemblerze lub wewnętrznie ... jeśli wiesz, co robisz. Kompilatory nie są tak inteligentne, jak zostały zaprojektowane, nie znają rzeczy, które robisz, chyba że powiesz im to wszędzie (np. Używając religijnego ograniczenia).
Kaj
1
I jeszcze raz muszę skomentować, że jeśli szukasz tylko gorących punktów, przegapisz wiele cykli, ponieważ nie znajdziesz żadnych rozlanych cykli na całej planszy (na przykład smartpointers ... dereferencje nigdzie, nigdy się nie pojawiają. jako hotspot, ponieważ faktycznie cały program jest hotspotem).
Kaj
1
Zgadzam się z obydwoma punktami, ale większość z nich zbiłbym pod „używaj odpowiednich struktur danych i algorytmów”. Jeśli wszędzie rozrzucasz inteligentne wskaźniki z licznikiem, a cykle liczenia spadają, na pewno wybrałeś niewłaściwą strukturę danych.
hojny
5

Pamiętaj jednak także o „przedwczesnej pesymizacji”. Chociaż nie ma potrzeby wybierać się na każdy wiersz kodu, istnieje uzasadnienie dla zrozumienia, że ​​faktycznie pracujesz nad grą, która ma wpływ na wydajność w czasie rzeczywistym.
Podczas gdy wszyscy mówią ci, aby mierzyć i optymalizować hotspoty, ta technika nie pokazuje wydajności utraconej w ukrytych miejscach. Na przykład, jeśli każda operacja „+” w kodzie zajmie dwa razy więcej czasu, niż powinna, nie pojawi się jako hot-spot, a zatem nigdy go nie zoptymalizujesz, a nawet nie zauważysz, ponieważ jest on używany w całym miejsce może kosztować dużo wydajności. Byłbyś zaskoczony, jak wiele z tych cykli ucieka, nie będąc nigdy wykrytym. Bądź więc świadom tego, co robisz.
Poza tym zwykle profiluję się regularnie, aby dowiedzieć się, co tam jest i ile czasu pozostało na klatkę. Dla mnie czas na klatkę jest najbardziej logiczny, ponieważ mówi mi bezpośrednio, gdzie jestem, z celami klatek na sekundę. Spróbuj także dowiedzieć się, gdzie są szczyty i co je powoduje - wolę stabilną szybkość klatek niż wysoką szybkość klatek z kolcami.

Kaj
źródło
Wydaje mi się to takie złe. Jasne, moje „+” może trwać dwa razy dłużej przy każdym wywołaniu, ale tak naprawdę ma to znaczenie tylko w ciasnej pętli. Wewnątrz wąskiej pętli zmiana pojedynczego „+” może zrobić rzędy wielkości więcej niż zmiana „+” poza pętlą. Po co myśleć o dziesiątej części mikrosekundy, kiedy można uratować milisekundę?
Wilduck,
1
Wtedy nie rozumiesz idei stojącej za stratą. „+” (tak jak na przykład) nazywa się setki tysięcy razy na klatkę, a nie tylko w ciasnych pętlach. Jeśli straci to kilka cykli za każdym razem, gdy stracisz wiele cykli na tablicy, ale nigdy nie pojawi się jako hotspot, ponieważ połączenia są równomiernie rozłożone na twojej bazie kodu / ścieżce wykonania. Więc nie mówisz o jednej dziesiątej mikrosekundy, ale w rzeczywistości tysiące razy więcej niż te dziesiąte mikrosekundy, co daje sumę wielu milisekund. Po przejściu na nisko wiszący owoc (ciasne pętle) zyskałem milisekundy w ten sposób więcej niż raz.
Kaj,
To jak kapiący kran. Po co martwić się o uratowanie tej małej kropli? - „Jeśli twój kran kapie w tempie jednej kropli na sekundę, możesz spodziewać się marnowania 2700 galonów rocznie”.
Kaj,
Och, chyba nie było jasne, że miałem na myśli, kiedy operator + był przeciążony, więc wpłynęłoby to na każde „+” w kodzie - naprawdę nie chciałbyś optymalizować każdego „+” w kodzie. Chyba zły przykład ... Miałem to na myśli jako „podstawową funkcjonalność, która jest wywoływana wszędzie tam, gdzie implementacja może być wolniejsza niż zakładano, szczególnie gdy jest ukryta przez przeciążenie operatora lub inne zaciemniające konstrukcje C ++”.
Kaj
3

Gdy gra będzie gotowa do wydania (wersja ostateczna lub beta) lub będzie zauważalnie wolna, to prawdopodobnie najlepszy czas na profilowanie aplikacji. Oczywiście zawsze możesz uruchomić profiler w dowolnym momencie; ale tak, przedwczesna optymalizacja jest źródłem wszelkiego zła. Nieuzasadniona optymalizacja również; potrzebujesz rzeczywistych danych, aby pokazać, że jakiś kod działa wolno, zanim zaczniesz go „optymalizować”. Profiler robi to za Ciebie.

Jeśli nie wiesz o profilerze, naucz się go! Oto dobry post na blogu pokazujący przydatność profilera.

Większość optymalizacji kodu gry sprowadza się do zmniejszenia liczby cykli procesora potrzebnych dla każdej klatki. Jednym ze sposobów jest zoptymalizowanie każdej rutyny podczas jej pisania i upewnienie się, że jest tak szybka, jak to możliwe. Jednak często mówi się, że 90% cykli procesora jest zużywanych na 10% kodu. Oznacza to, że skierowanie wszystkich prac związanych z optymalizacją do tych procedur wąskiego gardła przyniesie 10-krotny efekt optymalizacji wszystkiego równomiernie. Jak więc rozpoznać te procedury? Profilowanie ułatwia.

W przeciwnym razie, jeśli Twoja mała gra działa przy 200 FPS, mimo że ma nieefektywny algorytm, czy naprawdę masz powód do optymalizacji? Powinieneś mieć dobry pomysł na specyfikację komputera docelowego i upewnić się, że gra działa dobrze na tym komputerze, ale poza tym wszystko (prawdopodobnie) to zmarnowany czas, który można lepiej poświęcić na kodowanie lub dopracowywanie gry.

Ricket
źródło
Podczas gdy nisko wiszący owoc rzeczywiście zawiera 10% kodu i łatwo zostaje złapany przez profilowanie w końcu, samo działanie przy profilowaniu do tego sprawi, że przegapisz procedury, które są często nazywane, ale mają tylko trochę trochę zły kod - nie będą się pojawiać w twoim profilu, ale krwawią wiele cykli na połączenie. To naprawdę się składa.
Kaj,
@Kaj, dobre profilery sumują wszystkie setki indywidualnych wykonań złego algorytmu i pokazują sumę. Następnie powiesz „A co, jeśli miałbyś 10 złych metod i wszystkie wywołałyby 1/10 częstotliwości?” Jeśli poświęcisz cały swój czas na te 10 metod, stracisz wszystkie nisko wiszące owoce, w których dostaniesz znacznie większy huk za swoją złotówkę.
John McDonald,
2

Uważam, że przydatne jest budowanie profilowania. Nawet jeśli nie aktywnie optymalizujesz, dobrze jest mieć pomysł na to, co ogranicza Twoją wydajność w danym momencie. Wiele gier ma nakładany HUD, który wyświetla prosty wykres graficzny (zwykle tylko kolorowy pasek) pokazujący, jak długo poszczególne części pętli gry zajmują każdą klatkę.

Byłoby złym pomysłem pozostawienie analizy i optymalizacji wydajności zbyt późno na późnym etapie. Jeśli już zbudowałeś grę i masz 200% więcej niż budżet procesora i nie możesz tego znaleźć dzięki optymalizacji, to masz problemy.

Podczas pisania musisz wiedzieć, jakie są budżety na grafikę, fizykę itp. Nie możesz tego zrobić, jeśli nie masz pojęcia, jaka będzie twoja wydajność, i nie możesz zgadywać, nie wiedząc, jaka jest twoja wydajność i jaki może być zastój.

Więc buduj statystyki wydajności od pierwszego dnia.

Co do tego, kiedy radzić sobie z różnymi rzeczami - prawdopodobnie lepiej nie pozostawiać tego za późno, aby nie trzeba było refaktoryzować połowy silnika. Z drugiej strony, nie skupiaj się zbytnio na optymalizacji rzeczy, aby wycisnąć każdy cykl, jeśli uważasz, że możesz zmienić algorytm całkowicie jutro lub jeśli nie przeszedłeś przez to prawdziwych danych gry.

Zrywaj wiszące nisko owoce, od czasu do czasu radz sobie z dużymi rzeczami i wszystko powinno być w porządku.

JasonD
źródło
Aby dodać do profilu gry (z którym całkowicie się zgadzam), rozszerzenie profilu gry w celu wyświetlania wielu pasków (dla wielu ramek) pomaga skorelować zachowanie gry z pikami i może pomóc w znalezieniu wąskich gardeł, które nie pojawią się w twoim przeciętnym przechwytywaniu z profilerem.
Kaj
2

Jeśli spojrzy na cytat Knutha w jego kontekście, wyjaśnia dalej, że powinniśmy optymalizować, ale za pomocą narzędzi, takich jak profiler.

Powinieneś stale profilować i profilować pamięć swojej aplikacji po ułożeniu bardzo podstawowej architektury.

Profilowanie nie tylko pomoże zwiększyć prędkość, ale także pomoże znaleźć błędy. Jeśli twój program nagle drastycznie zmienia prędkość, zwykle dzieje się tak z powodu błędu. Jeśli nie jesteś profilowany, może zostać niezauważony.

Sztuką optymalizacji jest zrobienie tego zgodnie z projektem. Nie czekaj do ostatniej chwili. Upewnij się, że konstrukcja programu zapewnia wydajność, której potrzebujesz, bez naprawdę paskudnych sztuczek w pętli wewnętrznej.

Jonathan Fischoff
źródło
1

W moim projekcie zwykle stosuję BARDZO potrzebną optymalizację w moim silniku podstawowym. Na przykład zawsze lubię implementować dobrą solidną implementację SIMD przy użyciu SSE2 i 3DNow! Zapewnia to, że moja matematyka zmiennoprzecinkowa jest zgodna z tym, gdzie chcę. Inną dobrą praktyką jest nawyk optymalizacji dzięki kodowaniu zamiast cofania się. Przez większość czasu te małe praktyki są tak samo czasochłonne, jak to, co pisałeś. Przed zakodowaniem funkcji upewnij się, że znalazłeś najbardziej efektywny sposób, aby to zrobić.

Podsumowując, moim zdaniem, jest trudniej, aby twój kod był bardziej wydajny po tym, jak już jest do bani.

Krankzinnig
źródło
0

Powiedziałbym, że najłatwiejszym sposobem jest użycie zdrowego rozsądku - jeśli coś wygląda na to, że działa wolno, spójrz na to. Sprawdź, czy jest to wąskie gardło.
Skorzystaj z narzędzia do profilowania, aby zobaczyć, jakie funkcje prędkości biorą i jak często są wywoływane.
Absolutnie nie ma sensu optymalizować lub spędzać czasu próbując zoptymalizować coś, czego nie potrzebuje.

Kaczka komunistyczna
źródło
0

Jeśli kod działa wolno, uruchom profiler i zobacz, co dokładnie powoduje jego wolniejsze działanie. Lub możesz być proaktywny i już mieć profiler, zanim zaczniesz zauważać problemy z wydajnością.

Będziesz chciał zoptymalizować, gdy liczba klatek na sekundę spadnie do punktu, w którym gra zacznie cierpieć. Najbardziej prawdopodobnym winowajcą będzie nadmierne zużycie procesora (100%).

Bryan Denny
źródło
Powiedziałbym, że GPU jest tak samo prawdopodobne jak procesor. Rzeczywiście, w zależności od tego, jak ściśle są ze sobą sprzężone, całkowicie możliwe jest, aby procesor był mocno związany z połową ramki, a układ GPU z drugiej połowy. Głupie profilowanie może nawet wykazać o wiele mniej niż 100% wykorzystania w obu przypadkach. Upewnij się, że Twoje profilowanie jest wystarczająco drobnoziarniste, aby to pokazać (ale nie tak drobnoziarniste, aby było nachalne!)
JasonD
0

Powinieneś optymalizować kod ... tak często, jak potrzebujesz.

To, co zrobiłem w przeszłości, to ciągłe uruchamianie gry z włączonym profilowaniem (przynajmniej licznik klatek na ekranie przez cały czas). Jeśli gra staje się wolniejsza (na przykład poniżej docelowej liczby klatek na maszynie z minimalną specyfikacją), włącz profiler i sprawdź, czy pojawią się jakieś gorące punkty.

Czasami to nie jest kod. Wiele problemów, które napotkałem w przeszłości, dotyczyły procesorów GPU (oczywiście, było to na iPhonie). Problemy z wypełnianiem, zbyt wiele wywołań do rysowania, niewystarczająca geometria, nieefektywne moduły cieniujące ...

Poza nieefektywnymi algorytmami trudnych problemów (np. Wyszukiwanie ścieżek, fizyka) bardzo rzadko napotykałem problemy, w których przyczyną był sam kod. A te trudne problemy powinny być rzeczami, które poświęcasz dużo wysiłku na poprawne działanie algorytmu i nie martwisz się o mniejsze rzeczy.

Tetrad
źródło
0

Dla mnie najlepiej śledzić dobrze przygotowany model danych. I optymalizacja - przed głównym krokiem naprzód. Mam na myśli, zanim zacznę wdrażać coś nowego. Innym powodem optymalizacji jest to, że kiedy tracę kontrolę nad zasobami, aplikacja potrzebuje dużo obciążenia procesora / GPU lub pamięci i nie wiem dlaczego :) lub to za dużo.

samboush
źródło