Optymalizacja oparta na profilu

Optymalizacja z przewodnikiem (PGO) to dobrze znana technika optymalizacji kompilatora. W PGO kompilator korzysta z profilów czasu wykonywania pochodzących z wykonywań programu, aby podejmować optymalne decyzje dotyczące wstawiania i układu kodu. Spowoduje to poprawę wydajności i zmniejszenie rozmiaru kodu.

PGO można wdrożyć w aplikacji lub bibliotece, wykonując te czynności: 1. Określ reprezentatywny zbiór zadań. 2. Zbierać profile. 3. Użyj profili w wersji wydania.

Krok 1. Zidentyfikuj reprezentatywny strumień zadań

Najpierw określ reprezentatywny benchmark lub zbiór zadań dla swojej aplikacji. Jest to kluczowy krok, ponieważ profile zebrane z zadania identyfikują najbardziej aktywne i „zimne” regiony w kodzie. Gdy używasz profili, kompilator wykonuje agresywną optymalizację i wstawianie w miejscach, w których występują często wykonywane instrukcje. Kompilator może też zmniejszyć rozmiar kodu w „zimnych regionach” przy jednoczesnym obniżeniu wydajności.

Identyfikacja prawidłowego zbioru zadań pomaga też śledzić ogólną wydajność.

Krok 2. Zbierz profile

Zbieranie profili obejmuje 3 etapy: - kompilowanie kodu natywnego z użyciem instrumentacji, - uruchamianie na urządzeniu aplikacji z użyciem instrumentacji i generowanie profili, - łączenie lub post-processing profili na hoście.

Utwórz kompilację instrumentalną

Profile są zbierane przez uruchomienie zbioru zadań z etapu 1 na wersji aplikacji z instrumentacją. Aby wygenerować kompilację zinstruktowaną, dodaj -fprofile-generate do flag kompilatora i flagi łączącej. Ten parametr powinien być kontrolowany przez osobną zmienną kompilacji, ponieważ nie jest on potrzebny podczas kompilacji domyślnej.

Generowanie profili

Następnie uruchom zinstrumentowaną aplikację na urządzeniu i wygeneruj profile. Profil jest zbierany w pamięci podczas uruchamiania skompilowanego binarnego i zapisuje się do pliku po zakończeniu działania. Jednak funkcje zarejestrowane w usłudze atexit nie są wywoływane w aplikacjach na Androida – po prostu się zamyka.

Aplikacja lub obciążenie musi wykonać dodatkowe czynności, aby ustawić ścieżkę do pliku profilu, a następnie wyraźnie wywołać zapis profilu.

  • Aby ustawić ścieżkę pliku profilu, wywołaj __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. %m jest przydatna, gdy masz wiele bibliotek udostępnionych. %m rozwija się do unikalnej sygnatury modułu dla danej biblioteki, co powoduje powstanie osobnego profilu dla każdej biblioteki. Inne przydatne specyfikatory wzorów znajdziesz tutaj. PROFILE_DIR to katalog, który można zapisać z aplikacji. Aby dowiedzieć się, jak wykrywać ten katalog w czasie wykonywania, zapoznaj się z demonstracją.
  • Aby jawnie wywołać zapis profilu, wywołaj funkcję __llvm_profile_write_file.
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}

#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
  // ...
  // run workload
  // ...

  // set path and write profiles after workload execution
  __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
  __llvm_profile_write_file();
  return;
}

Uwaga: generowanie pliku profilu jest prostsze, jeśli obciążenie jest samodzielnym binarnym plikiem. Przed uruchomieniem binarnego pliku ustaw zmienną środowiskową LLVM_PROFILE_FILE na %t/default-%m.profraw.

Profile przetwarzania końcowego

Pliki profilu mają format .profraw. Najpierw należy je pobrać z urządzenia za pomocą funkcji adb pull. Po pobraniu użyj w NDK narzędzia llvm-profdata, aby przekonwertować plik .profraw na .profdata, który można następnie przekazać kompilatorowi.

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
    merge --output=pgo_profile.profdata \
    <list-of-profraw-files>

Aby uniknąć niezgodności wersji formatów plików profilu, użyj elementów llvm-profdataclang z tej samej wersji NDK.

Krok 3. Użyj profili do tworzenia aplikacji

Użyj profilu z poprzedniego kroku podczas tworzenia wersji aplikacji, przekazując wartość -fprofile-use=<>.profdata do kompilatora i linkera. Profilów można używać nawet podczas ewolucji kodu – kompilator Clang może tolerować niewielkie niezgodności między źródłem a profilami.

Uwaga: w większości bibliotek profile są wspólne dla wszystkich architektur. Na przykład profile wygenerowane na podstawie wersji biblioteki arm64 mogą być używane na wszystkich architekturach. Problem polega jednak na tym, że jeśli w bibliotece są ścieżki kodu specyficzne dla danej architektury (1 z modułów x86 lub 32-bitowych lub 64-bitowych), dla każdej takiej konfiguracji należy użyć osobnego profilu.

Podsumowanie

Strona https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo zawiera kompleksową demonstrację korzystania z PGO w aplikacji. Zawiera dodatkowe informacje, które zostały skrócone w tym dokumencie.

  • Reguły kompilacji CMake pokazują, jak skonfigurować zmienną CMake, która kompiluje kod natywny z użyciem instrumentacji. Jeśli zmienna kompilacji nie jest ustawiona, kod natywny jest optymalizowany za pomocą wcześniej wygenerowanych profili PGO.
  • W kompilacji z instrumentacją plik pgodemo.cpp zapisuje profile w ramach wykonywania zbioru zadań.
  • Miejsce do zapisu profili jest uzyskiwane w czasie działania w pliku MainActivity.kt za pomocą funkcji applicationContext.cacheDir.toString().
  • Aby pobrać profile z urządzenia bez wymagania adb root, użyj adb przepisu tutaj.