Optymalizacja oparta na profilu

Optymalizacja z przewodnikiem (PGO) to dobrze znana technika optymalizacji kompilatora. W PGO kompilator używa profili środowiska wykonawczego z uruchomień programu do wybierania optymalnych opcji wkładania i układu kodu. Pozwala to zwiększyć wydajność i zmniejszyć rozmiar kodu.

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

Krok 1. Zidentyfikuj reprezentatywne zadanie

Najpierw określ reprezentatywny punkt odniesienia lub zbiór zadań dla swojej aplikacji. Jest to bardzo ważny krok, ponieważ profile zebrane z zbioru zadań wskazują gorące i zimne regiony w kodzie. W przypadku korzystania z profili kompilator przeprowadza agresywną optymalizację i wprowadza treści w najpopularniejszych regionach. Kompilator może też zmniejszyć rozmiar kodu w „zimnych regionach” przy jednoczesnym obniżeniu wydajności.

Określanie odpowiedniego obciążenia jest też przydatne do ogólnego śledzenia wydajności.

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.

Tworzenie wersji z instrumentacją

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. Ta flaga powinna być sterowana przez osobną zmienną kompilacji, ponieważ flaga nie jest potrzebna w przypadku 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 za pomocą atexit nie są wywoływane w aplikacji na Androida – aplikacja po prostu zostaje zamknięta.

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ę do pliku profilu, wywołaj funkcję __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. %m jest przydatna, gdy masz wiele bibliotek udostępnionych. %m rozwija się do unikalnego podpisu modułu dla tej biblioteki, co powoduje utworzenie osobnego profilu dla każdej biblioteki. Inne przydatne specyfikatory wzorców znajdziesz tutaj. PROFILE_DIR to katalog, w którym można zapisywać zmiany w aplikacji. Zapoznaj się z prezentacją, jak wykryć ten katalog w czasie działania.
  • 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: wygenerowanie pliku profilu jest łatwiejsze, jeśli zbiór zadań jest samodzielnym plikiem binarnym. Wystarczy, że przed uruchomieniem pliku binarnego LLVM_PROFILE_FILE ustawi zmienną środowiskową na %t/default-%m.profraw.

Profile końcowe

Pliki profili 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 z formatami pliku profilu, użyj tych elementów: llvm-profdata i clang z tej samej wersji NDK.

Krok 3. Użyj profili do utworzenia aplikacji

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

Uwaga: w przypadku większości bibliotek profile są wspólne dla różnych 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

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo pokazuje pełne demo korzystania z PGO z aplikacji. Zawiera ono dodatkowe informacje, które zostały pominięte w tym dokumencie.

  • Reguły kompilacji CMake pokazują, jak skonfigurować zmienną CMake, która tworzy kod natywny przy użyciu instrumentacji. Jeśli zmienna kompilacji nie jest określona, kod natywny jest optymalizowany na podstawie wygenerowanych wcześniej 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 konieczności użycia funkcji adb root, skorzystaj z przepisu adb dostępnego tutaj.