Optymalizacja oparta na profilu

Optymalizacja oparta na profilu to znana technika optymalizacji przez kompilatory. W PGO kompilator używa profili środowiska wykonawczego z wykonań programu do podejmowania optymalnych wyborów dotyczących wbudowania i układu kodu. Zwiększa to wydajność i zmniejsza rozmiar kodu.

Usługę PGO można wdrożyć w aplikacji lub bibliotece w ten sposób: 1. Określ reprezentatywny zbiór zadań. 2. Zbieraj profile. 3. używać profili w kompilacji wersji.

Krok 1. Zidentyfikuj reprezentatywne zadanie

Najpierw określ wartość referencyjną lub zbiór zadań reprezentatywnych dla Twojej aplikacji. Jest to kluczowy krok, ponieważ profile zbierane przez zadanie identyfikują w kodzie obszary gorące i zimne. Podczas korzystania z profili kompilator przeprowadza agresywne optymalizacje i wbudowuje w gorących regionach. Kompilator może też zmniejszyć rozmiar kodu w zimnych regionach, obniżając jednocześnie wydajność.

Określenie dobrego zbioru zadań jest też korzystne do śledzenia ogólnej wydajności.

Krok 2. Zbierz profile

Zbieranie profili obejmuje 3 kroki: – tworzenie natywnego kodu za pomocą narzędzi, – uruchomienie aplikacji z instrumentacją na urządzeniu i generowanie profili, – scalanie/przetwarzanie profili na hoście.

Utwórz modelowaną kompilację

Profile są gromadzone przez uruchomienie zadania z kroku 1 w instruowanej kompilacji aplikacji. Aby wygenerować kompilację instrumentalną, dodaj -fprofile-generate do flag kompilatora i łącznika. Ta flaga powinna być kontrolowana przez oddzielną zmienną kompilacji, ponieważ jest ona niepotrzebna podczas kompilacji domyślnej.

Wygeneruj profile

Następnie uruchom aplikację z instrumentacją na urządzeniu i wygeneruj profile. Profile są zbierane w pamięci przy uruchamianiu instrumentowanego pliku binarnego i zapisywane w pliku przy zamykaniu. Jednak funkcje zarejestrowane w atexit nie są wywoływane w aplikacji na Androida – aplikacja po prostu zostaje zamknięta.

Aplikacja lub zadanie musi wykonać dodatkową pracę, aby ustawić ścieżkę do pliku profilu, a następnie jawnie aktywować zapis profilu.

  • Aby ustawić ścieżkę pliku profilu, wywołaj __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. %m jest przydatny, gdy masz wiele bibliotek udostępnionych. %m rozwija się do niepowtarzalnego podpisu modułu dla danej biblioteki, co powoduje utworzenie osobnego profilu dla każdej biblioteki. Inne przydatne specyfikatory wzorców znajdziesz tutaj. PROFILE_DIR to katalog, który można zapisywać z aplikacji. Zobacz prezentację, jak wykryć ten katalog w czasie działania.
  • Aby jawnie aktywować zapis w 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 prostsze, jeśli zadanie jest samodzielnym plikiem binarnym. Wystarczy, że ustawisz zmienną środowiskową LLVM_PROFILE_FILE na %t/default-%m.profraw przed uruchomieniem pliku binarnego.

Profile procesu końcowego

Pliki profili mają format .profraw. Należy je najpierw pobrać z urządzenia za pomocą adb pull. Po pobraniu użyj narzędzia llvm-profdata w pakiecie NDK, aby przekonwertować dane z .profraw na .profdata i przekazać je do kompilatora.

$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-profdata i clang z tej samej wersji NDK.

Krok 3. Użyj profili do utworzenia aplikacji

Użyj profilu z poprzedniego kroku podczas kompilacji wersji swojej aplikacji, przekazując -fprofile-use=<>.profdata do kompilatora i tagu łączącego. Profili można używać nawet w trakcie rozwoju kodu – kompilator Clang może tolerować niewielkie rozbieżności między źródłem a profilami.

Uwaga: w przypadku większości bibliotek profile są wspólne w różnych architekturach. Na przykład profile wygenerowane z kompilacji biblioteki Arm64 można używać we wszystkich architekturach. Jeśli jednak w bibliotece istnieją ścieżki kodu związane z konkretną architekturą (arm a x86 lub 32-bitowe lub 64-bitowe), do każdej takiej konfiguracji należy używać osobnych profili.

Podsumowanie

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo zawiera kompleksową prezentację korzystania z usługi PGO z aplikacji. Zawiera ona dodatkowe informacje, które zostały przejrzane w tym dokumencie.

  • Reguły kompilacji CMake pokazują, jak skonfigurować zmienną CMake, która tworzy kod natywny z instrumentacją. Jeśli zmienna kompilacji nie jest ustawiona, kod natywny jest optymalizowany przy użyciu wcześniej wygenerowanych profili PGO.
  • W kompilacji z instrumentacją pgodemo.cpp zapisuje, że profile są wykonywaniem zadań.
  • Lokalizacja profili z możliwością zapisu jest tworzona w czasie działania w MainActivity.kt za pomocą applicationContext.cacheDir.toString().
  • Aby pobrać profile z urządzenia bez wymagania adb root, skorzystaj z przepisu na adb, który znajdziesz tutaj.