Jak działa optymalizacja wspomagana przez profil

Optymalizacja pod kątem profilu (nazywana też PGO lub „pogo”) to sposób na dalszą optymalizację zoptymalizowanych kompilacji gry na podstawie informacji o tym, jak gra działa w świecie rzeczywistym. Dzięki temu rzadko uruchamiany kod (np. błąd lub wielkość liter skrajnych) jest usuwany z krytycznych ścieżek wykonania kodu, co przyspiesza jego działanie.

Schemat pokazujący, jak działa PGO

Rysunek 1. Omówienie działania PGO.

Jeśli chcesz użyć PGO, najpierw przygotuj kompilację pod kątem generowania danych profilu, z którymi może pracować kompilator. Następnie ćwiczysz kod, uruchamiając kompilację i generując co najmniej 1 plik danych profilu. Na koniec skopiujesz te pliki z powrotem z urządzenia i użyjesz ich w kompilatorze, aby zoptymalizować plik wykonywalny przy użyciu przechwyconych informacji o profilu.

Jak działają zoptymalizowane kompilacje bez PGO

Kompilacja zoptymalizowana bez korzystania z danych profilu uwzględnia szereg czynników heurystycznych przy podejmowaniu decyzji o sposobie wygenerowania zoptymalizowanego kodu.

Niektóre z nich są wyraźnie sygnalizowane przez programistę, np. w języku C++ w wersji 20 lub nowszej za pomocą wskazówek dotyczących kierunku gałęzi, takich jak [[likely]] i [[unlikely]]. Innym przykładem może być użycie słowa kluczowego inline albo nawet __forceinline (chociaż generalnie lepiej i bardziej elastycznie jest trzymać się poprzedniego). Domyślnie niektóre kompilatory zakładają, że najbardziej prawdopodobna jest pierwsza część gałęzi (czyli instrukcja if, a nie część else). Optymalizator może też wyciągać wnioski na podstawie statycznej analizy kodu sposobu jego działania, ale zwykle ma to ograniczony zakres.

Problemem heurystyki jest to, że nie są one w stanie prawidłowo pomóc kompilatorowi w każdej sytuacji – nawet w przypadku korzystania z obszernych znaczników ręcznych. Dlatego chociaż generowany kod jest zwykle dobrze zoptymalizowany, nie jest tak dobry, jak byłoby to możliwe, gdyby kompilator miał więcej informacji o swoim działaniu w czasie działania.

Generowanie profilu

Jeśli Twój plik wykonywalny jest skompilowany z włączoną funkcją PGO w trybie instrumented, jest on rozszerzony o kod na początku każdego bloku kodu, np. na początku funkcji lub na początku każdej gałęzi gałęzi. Ten kod służy do śledzenia liczby przypadków wprowadzenia bloku przez uruchomienie kodu, którego kompilator może później użyć do wygenerowania zoptymalizowanego kodu.

Dostępne są też inne opcje śledzenia, np. rozmiar typowych operacji kopiowania w bloku, dzięki czemu później można wygenerować szybkie wersje w tekście.

Gdy gra wykona jakąś reprezentatywną pracę, musi on wywołać funkcję __llvm_profile_write_file(), która zapisze dane profilowe w możliwej do dostosowania lokalizacji na urządzeniu. Ta funkcja jest automatycznie połączona z grą, gdy w konfiguracji kompilacji włączone jest narzędzie PGO.

Napisany plik danych profilu należy skopiować na komputer hosta i przechowywać w tej samej lokalizacji wraz z innymi profilami z tej samej kompilacji, aby można było ich używać razem.

Możesz na przykład zmodyfikować kod gry, aby wywoływał __llvm_profile_write_file() po zakończeniu bieżącej sceny gry. Aby utworzyć profil, należy stworzyć grę z włączoną instrumentacją, a potem wdrożyć ją na urządzeniu z Androidem. Gdy gra jest aktywna, dane z profilu są automatycznie rejestrowane – inżynier ds. kontroli jakości przeszukuje grę, wypróbowując jej różne scenariusze (lub po prostu zajmuje się zdanym testem).

Po zakończeniu ćwiczeń różnych elementów gry możesz wrócić do menu głównego, w którym możesz zakończyć obecną scenę gry i zapisać dane z profilu.

Następnie za pomocą skryptu można skopiować dane profilu z urządzenia testowego i przesłać je do centralnego repozytorium, gdzie można je przechwycić do późniejszego użycia.

Scalanie danych profilu

Po uzyskaniu profilu z urządzenia trzeba go przekonwertować z pliku danych profilu wygenerowanego przez zinstruowaną kompilację do postaci, którą może wykorzystać kompilator. AGDE robi to za Ciebie automatycznie w przypadku wszystkich plików danych profilu, które dodajesz do projektu.

Narzędzie PGO zostało zaprojektowane tak, aby łączyć wyniki z wielu uruchomień profilowych. AGDE robi to automatycznie za Ciebie, jeśli w jednym projekcie masz wiele plików.

Jako przykład użyteczności można wykorzystać scalanie zbiorów danych profilu. Załóżmy, że masz laboratorium pełne inżynierów kontroli jakości, którzy grają na różnych poziomach gry. Każda rozgrywka jest rejestrowana, a następnie wykorzystywane do generowania danych profilowych z kompilacji gry opartej na PGO. Scalanie profili umożliwia łączenie wyników wszystkich tych różnych uruchomień testów, co może powodować wykonywanie bardzo różnych części kodu, aby uzyskać lepsze wyniki.

Co więcej, przy przeprowadzaniu testów podłużnych, w których przechowujesz kopie danych profilu z wersji wewnętrznej do wersji wewnętrznej, odbudowanie danych nie powoduje konieczności unieważnienia starych danych. W większości przypadków kod jest względnie stabilny od momentu opublikowania do publikacji, więc dane profili ze starszych kompilacji mogą być wciąż przydatne i nie stają się przestarzałe od razu.

Generuję kompilacje zoptymalizowane pod kątem profilu

Po dodaniu danych profilu do projektu możesz ich użyć do skompilowania pliku wykonywalnego, włączając PGO w trybie optymalizacji w konfiguracji kompilacji.

Dzięki temu optymalizator kompilatora będzie używać zebranych wcześniej danych profilowych podczas podejmowania decyzji dotyczących optymalizacji.

Kiedy warto korzystać z optymalizacji z pomocą profilu

PGO nie należy włączać na początku tworzenia ani w trakcie codziennej iteracji kodu. Podczas programowania należy skupić się na optymalizacji opartej na algorytmie i układzie danych, ponieważ przyniosą one znacznie większe korzyści.

Usługa PGO pojawia się w dalszej części procesu programowania, gdy przygotowujesz się do opublikowania. Optymalizacja z pomocą profilu to jedyny w swoim rodzaju punkt, dzięki któremu możesz w pełni wykorzystać potencjał swojego kodu, gdy już będziesz go samodzielnie optymalizować.

Oczekiwana poprawa skuteczności w ramach PGO

Zależy to od wielu czynników, w tym od tego, jak kompletne i nieaktualne są Twoje profile, oraz od tego, jak blisko optymalnego wykorzystania byłby Twój kod w przypadku tradycyjnej zoptymalizowanej kompilacji.

Ogólnie bardzo zachowawcze szacunki wskazują, że koszty procesora spadną o około 5% w wątkach kluczowych. Wyniki mogą się różnić.

Narzut na narzędzia

Narzędzia PGO są dostępne w całości i chociaż są generowane automatycznie, nie są bezpłatne. Nakłady pracy związane z narzędziami PGO mogą się różnić w zależności od bazy kodu.

Koszt wydajności korzystania z instrumentacji profilowej

W przypadku konstrukcji z instruktażem możesz zauważyć spadek liczby klatek. W niektórych przypadkach – w zależności od stopnia wykorzystania procesora podczas normalnego działania w 100% – spadek ten może być tak duży, że utrudnia normalną rozgrywkę.

Większości deweloperów zalecamy wprowadzenie w grze półdeterministycznego trybu ponownego rozgrywki. Dzięki tej funkcji zespół ds. kontroli jakości może rozpocząć grę w znanym, powtarzalnym miejscu początkowym (np. zapisać grę lub na określonym poziomie testowym), a potem zapisać swoje odpowiedzi. Dane wejściowe zarejestrowane z kompilacji testowej można umieścić w kompilacji z oprogramowaniem PGO, odtworzyć ją i wygenerować rzeczywiste dane profilowe niezależnie od tego, ile czasu zajmuje przetworzenie pojedynczej klatki – nawet jeśli gra działała tak wolno, że nie dało się jej odtworzyć.

Ta funkcja ma też inne ważne zalety, np. zwielokrotnia wysiłek testera: jeden tester może zapisać swoje odpowiedzi na urządzeniu, a potem odtwarzać tę funkcję na wielu różnych urządzeniach do celów związanych z testowaniem pod kątem dymu.

Taki system odtwarzania może mieć ogromne zalety w Androidzie, ponieważ w ekosystemie jest bardzo dużo wariantów urządzeń, a korzyści się tam nie skończą. Mogą też stać się podstawową częścią systemu ciągłej integracji i umożliwiać wykonywanie z dnia na dzień regularnych regresji wydajności i testów dymu.

Nagranie powinno rejestrować dane wejściowe użytkownika w najodpowiedniejszym punkcie mechanizmu wprowadzania danych w grze (prawdopodobnie nie jest to bezpośrednie zdarzenia na ekranie dotykowym, ale ich konsekwencje są rejestrowane jako polecenia). Wejścia te powinny też zawierać liczbę klatek, która rejestruje się monotonicznie podczas rozgrywki, tak aby podczas odtwarzania mechanizm ponownego odtwarzania mógł czekać na odpowiednią klatkę, która powinna wywołać zdarzenie.

W trybie odtwarzania gra powinna unikać logowania się online, nie powinna wyświetlać reklam i powinna działać w stałym czasie (z uwzględnieniem docelowej liczby klatek). Rozważ wyłączenie vsync.

Nie ma znaczenia, czy wszystkie elementy w grze (na przykład systemy cząstek) da się dokładnie powtarzać, ale te same działania powinny mieć te same konsekwencje i wyniki w grze, czyli czy rozgrywka powinna być taka sama.

Koszt pamięci dla instrumentacji prowadzonej przez profil

Zużycie pamięci w przypadku instrumentacji PGO znacznie się różni w zależności od kompilowanej biblioteki. Podczas testów zaobserwowaliśmy ogólny wzrost rozmiaru pliku wykonywalnego o około 2,2 raza. Zwiększenie rozmiaru obejmował zarówno dodatkowy kod potrzebny do instrumentowania bloków kodu, jak i miejsce na przechowywanie liczników. Te testy nie były wyczerpujące, a Twoje wrażenia mogą się różnić.

Kiedy aktualizować lub odrzucać dane w profilu

Po każdej dużej zmianie kodu (lub zawartości gry) należy aktualizować profile.

Co to oznacza, zależy od środowiska kompilacji i Twojego miejsca programowania.

Jak wspomnieliśmy wcześniej, nie należy przenosić danych profilu przez duże zmiany w środowisku kompilacji. Nie przeszkodzi to w utworzeniu ani zepsuciu kompilacji, ale zmniejszy to korzyści w związku z wydajnością, ponieważ w nowym środowisku kompilacji będzie miało bardzo mało danych dotyczących profilu. Nie jest to jednak jedyny przypadek, w którym dane profilowe mogą stać się nieaktualne.

Załóżmy, że w ramach przygotowań do premiery nie będziesz korzystać z PGO. Jest to możliwe dopiero po cotygodniowym zgromadzeniu danych, dzięki którym inżynierowie nastawieni na wydajność mogą sprawdzić, czy nie ma żadnych niespodziewanych zacięć przed wydaniem aplikacji.

Zmienia się to w miarę zbliżania się do terminu premiery, gdy zespół ds. kontroli jakości codziennie przeprowadza testy i intensywnie sprawdza grę. Na tej fazie możesz codziennie generować profile na podstawie tych danych, a potem używać ich do generowania przyszłych kompilacji na potrzeby testowania wydajności i dostosowywania budżetów wydajności.

Przygotowując się do wydania, zablokuj wersję kompilacji, którą planujesz opublikować, a następnie poproś o przeprowadzenie kontroli jakości i wygenerowanie nowych danych profilu. Następnie na podstawie tych danych tworzysz ostateczną wersję pliku wykonywalnego.

Kontrola jakości może następnie przeprowadzić optymalizację i wysyłkę, by upewnić się, że produkt nadaje się do publikacji.