Profilgestützte Optimierung

Die profilgestützte Optimierung (PGO) ist eine bekannte Compileroptimierungstechnik. Bei der dynamischen Optimierung werden vom Compiler Laufzeitprofile aus der Ausführung eines Programms verwendet, um optimale Entscheidungen hinsichtlich Inline-Einfügung und Code-Layout zu treffen. Dies führt zu einer verbesserten Leistung und einer reduzierten Codegröße.

So können Sie PGO in Ihrer Anwendung oder Bibliothek bereitstellen: 1. Identifizieren Sie eine repräsentative Arbeitslast. 2. Profile erfassen. 3. Verwenden Sie die Profile in einem Release-Build.

Schritt 1: Einen repräsentativen Arbeitslast ermitteln

Identifizieren Sie zuerst einen repräsentativen Benchmark oder eine repräsentative Arbeitslast für Ihre Anwendung. Dies ist ein wichtiger Schritt, da die aus der Arbeitslast erfassten Profile die heißen und kalten Regionen im Code identifizieren. Wenn Sie die Profile verwenden, führt der Compiler aggressive Optimierungen und Inline-Einfügungen in den Hotspots durch. Der Compiler kann auch die Codegröße von kalten Regionen reduzieren, was jedoch zu einer Leistungseinbuße führt.

Die richtige Arbeitslast zu ermitteln, ist auch hilfreich, um die Leistung im Allgemeinen im Blick zu behalten.

Schritt 2: Profile erfassen

Die Profilerhebung umfasst drei Schritte: - Nativen Code mit Instrumentierung erstellen, - die instrumentierte App auf dem Gerät ausführen und Profile generieren und - die Profile auf dem Host zusammenführen/nachbearbeiten.

Instrumentierten Build erstellen

Die Profile werden erfasst, indem die Arbeitslast aus Schritt 1 für einen instrumentierten Build der Anwendung ausgeführt wird. Wenn Sie einen instrumentierten Build generieren möchten, fügen Sie den Compiler- und Linker-Flags -fprofile-generate hinzu. Dieses Flag sollte über eine separate Build-Variable gesteuert werden, da es bei einem Standard-Build nicht benötigt wird.

Profile generieren

Führen Sie als Nächstes die instrumentierte App auf dem Gerät aus und generieren Sie Profile. Profile werden im Arbeitsspeicher erfasst, wenn das instrumentierte Binärprogramm ausgeführt wird, und beim Beenden in eine Datei geschrieben. 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 eine Profilbeschreibung auszulösen.

  • Rufen Sie __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw auf, um den Pfad der Profildatei festzulegen. %m ist nützlich, wenn es mehrere gemeinsam genutzte Bibliotheken gibt. %m wird in eine eindeutige 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, das über die Anwendung beschreibbar ist. In der Demo erfahren Sie, wie Sie dieses Verzeichnis während der Laufzeit erkennen.
  • Wenn Sie ein Profil explizit schreiben 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: Das Erstellen der Profildatei ist einfacher, wenn es sich bei der Arbeitslast um ein eigenständiges Binärprogramm handelt. Legen Sie dazu einfach die Umgebungsvariable LLVM_PROFILE_FILE auf %t/default-%m.profraw fest, bevor Sie das Binärprogramm ausführen.

Profile für die Nachbearbeitung

Die Profildateien liegen im .profraw-Format vor. Sie müssen zuerst mit adb pull vom Gerät abgerufen werden. Verwenden Sie nach dem Abrufen das llvm-profdata-Dienstprogramm im NDK, um von .profraw zu .profdata zu konvertieren. Diese Datei kann dann an den Compiler übergeben werden.

$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 derselben NDK-Version, um Versionsdiskrepanzen der Profildateiformate zu vermeiden.

Schritt 3: Profile zum Erstellen der Anwendung verwenden

Verwenden Sie das Profil aus dem vorherigen Schritt bei einem Release-Build Ihrer Anwendung, indem Sie -fprofile-use=<>.profdata an den Compiler und Linker übergeben. Die Profile können auch während der Entwicklung des Codes verwendet werden. Der Clang-Compiler toleriert leichte Abweichungen zwischen der Quelle und den Profilen.

Hinweis: Bei den meisten Bibliotheken sind die Profile für alle Architekturen gleich. Profile, die beispielsweise aus dem arm64-Build der Bibliothek generiert wurden, können für alle Architekturen verwendet werden. Wenn die Bibliothek jedoch Architektur-spezifische Codepfade enthält (ARM oder x86 oder 32-Bit oder 64-Bit), sollten für jede solche Konfiguration separate Profile verwendet werden.

Zusammenfassung

Unter https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo wird eine End-to-End-Demo zur Verwendung von PGO über eine App gezeigt. Sie enthält zusätzliche Details, die in diesem Dokument überflogen wurden.

  • In den CMake-Buildregeln wird gezeigt, wie Sie eine CMake-Variable einrichten, mit der nativer Code mit Instrumentierung erstellt wird. Wenn die Build-Variable nicht festgelegt ist, wird nativer Code mithilfe zuvor generierter PGO-Profile optimiert.
  • In einem instrumentierten Build schreibt pgodemo.cpp die Profile bei der Ausführung der Arbeitslast.
  • Ein beschreibbarer Speicherort für die Profile wird zur Laufzeit in MainActivity.kt mithilfe von applicationContext.cacheDir.toString() abgerufen.
  • Wenn Sie Profile vom Gerät abrufen möchten, ohne adb root zu benötigen, verwenden Sie das adb-Rezept hier.