Profilgestützte Optimierung

Die profilgestützte Optimierung (PGO) ist eine bekannte Compiler-Optimierungstechnik. In PGO verwendet der Compiler Laufzeitprofile aus den Ausführungen eines Programms, um optimale Entscheidungen bezüglich Inline- und Codelayout zu treffen. Dies führt zu einer verbesserten Leistung und einer geringeren Codegröße.

PGO kann mit den folgenden Schritten in Ihrer Anwendung oder Bibliothek bereitgestellt werden: 1. Eine repräsentative Arbeitslast identifizieren. 2. Profile erfassen. 3. Verwenden Sie die Profile in einem Release-Build.

Schritt 1: Repräsentative Arbeitslast ermitteln

Identifizieren Sie zuerst eine repräsentative Benchmark oder Arbeitslast für Ihre Anwendung. Dies ist ein entscheidender Schritt, da die aus der Arbeitslast erfassten Profile die heißen und kalten Regionen im Code identifizieren. Wenn die Profile verwendet werden, führt der Compiler aggressive Optimierungen und das Inlineing in den heißen Regionen durch. Der Compiler kann auch die Codegröße von kalten Regionen reduzieren, ohne die Leistung zu beeinträchtigen.

Die Identifizierung einer guten Arbeitslast ist auch hilfreich, um die Leistung im Allgemeinen zu verfolgen.

Schritt 2: Profile erfassen

Die Profilerfassung umfasst drei Schritte: – Erstellen von nativem Code mit Instrumentierung, Ausführen der instrumentierten Anwendung auf dem Gerät, Generieren von Profilen und Zusammenführen/Nachverarbeiten der Profile auf dem Host.

Instrumentierten Build erstellen

Die Profile werden erfasst, indem die Arbeitslast aus Schritt 1 auf einem instrumentierten Build der Anwendung ausgeführt wird. Fügen Sie den Compiler- und Verknüpfungs-Flags -fprofile-generate hinzu, um einen instrumentierten Build zu generieren. Dieses Flag sollte über eine separate Build-Variable gesteuert werden, da es während eines Standard-Builds nicht benötigt wird.

Profile erstellen

Führen Sie als Nächstes die instrumentierte App auf dem Gerät aus und generieren Sie Profile. Profile werden im Arbeitsspeicher erfasst, wenn die instrumentierte Binärdatei ausgeführt und beim Beenden in eine Datei geschrieben wird. Mit atexit registrierte Funktionen werden jedoch nicht in einer Android-App aufgerufen – die App wird einfach beendet.

Die Anwendung/Arbeitslast muss zusätzliche Arbeit leisten, um einen Pfad für die Profildatei festzulegen und dann explizit einen Profilschreibvorgang auszulösen.

  • Rufen Sie __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw auf, um den Profildateipfad festzulegen. %m ist nützlich, wenn mehrere gemeinsam genutzte Bibliotheken vorhanden sind. %m wird zu einer eindeutigen Modulsignatur für diese Bibliothek erweitert, was zu einem separaten Profil pro Bibliothek führt. Weitere nützliche Musterspezifizierer finden Sie hier. PROFILE_DIR ist ein Verzeichnis, in das die Anwendung geschrieben werden kann. Informationen zum Erkennen dieses Verzeichnisses zur Laufzeit finden Sie in der Demo.
  • Wenn Sie einen Profilschreibvorgang explizit auslösen möchten, rufen Sie die Funktion __llvm_profile_write_file auf.
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;
}

Hinweis: Wenn die Arbeitslast ein eigenständiges Binärprogramm ist, ist das Generieren der Profildatei einfacher. Legen Sie dazu einfach die Umgebungsvariable LLVM_PROFILE_FILE auf %t/default-%m.profraw fest, bevor Sie die Binärdatei ausführen.

Nachbearbeitung von Profilen

Die Profildateien haben das .profraw-Format. Sie müssen zuerst mit adb pull vom Gerät abgerufen werden. Verwenden Sie nach dem Abruf das Dienstprogramm llvm-profdata im NDK, um .profraw in .profdata zu konvertieren und dann an den Compiler zu übergeben.

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

Verwenden Sie llvm-profdata und clang aus demselben NDK-Release, um Versionsunterschiede zwischen den Profildateiformaten zu vermeiden.

Schritt 3: Profile zum Erstellen einer Anwendung verwenden

Verwenden Sie das Profil aus dem vorherigen Schritt während eines Release-Builds Ihrer Anwendung. Dazu übergeben Sie -fprofile-use=<>.profdata an den Compiler und den Linker. Die Profile können auch dann verwendet werden, wenn sich der Code weiterentwickelt. Der Clang-Compiler kann geringfügige Abweichungen zwischen der Quelle und den Profilen tolerieren.

Hinweis: Im Allgemeinen sind die Profile bei den meisten Bibliotheken in allen Architekturen gleich. Beispielsweise können Profile, die aus dem arm64-Build der Bibliothek generiert wurden, für alle Architekturen verwendet werden. Wenn es in der Bibliothek architekturspezifische Codepfade gibt (Verzweigung vs. x86 oder 32-Bit bzw. 64-Bit), sollten für jede solche Konfiguration separate Profile verwendet werden.

Zusammenfassung

https://github.com/DanAlbert/ndk-sample/tree/pgo/pgo zeigt eine End-to-End-Demo zur Verwendung von PGO in einer App. Es enthält zusätzliche Details, die in diesem Dokument überfliegen.

  • In den Build-Regeln für CMake wird gezeigt, wie Sie eine CMake-Variable einrichten, die nativen Code mit Instrumentierung erstellt. Wenn die Build-Variable nicht festgelegt ist, wird nativer Code mithilfe von zuvor generierten PGO-Profilen optimiert.
  • In einem instrumentierten Build schreibt pgodemo.cpp die Profile als Arbeitslastausführung.
  • Ein beschreibbarer Speicherort für die Profile wird zur Laufzeit in MainActivity.kt mit applicationContext.cacheDir.toString() abgerufen.
  • Um Profile vom Gerät abzurufen, ohne dass adb root erforderlich ist, verwenden Sie das adb-Schema.