Ottimizzazione basata sul profilo

L'ottimizzazione basata sul profilo (PGO) è una tecnica di ottimizzazione del compilatore ben nota. In PGO, i profili di runtime delle esecuzioni di un programma vengono utilizzati dal compilatore per fare scelte ottimali riguardo al layout incorporato e al codice. Ciò si traduce in un miglioramento delle prestazioni e in una riduzione delle dimensioni del codice.

PGO può essere implementato nella tua applicazione o libreria seguendo i passaggi che seguono: 1. Identifica un carico di lavoro rappresentativo. 2. Raccogli i profili. 3. Utilizzare i profili in una release build.

Passaggio 1: identifica un carico di lavoro rappresentativo

Per prima cosa, identifica un benchmark o un carico di lavoro rappresentativo per la tua applicazione. Questo è un passaggio fondamentale in quanto i profili raccolti dal carico di lavoro identificano le regioni calde e fredde nel codice. Quando utilizzi i profili, il compilatore effettuerà ottimizzazioni aggressive e inlining nelle regioni più utilizzate. Il compilatore può anche scegliere di ridurre le dimensioni del codice delle regioni non attive, sacrificando al contempo le prestazioni.

Identificare un buon carico di lavoro è utile anche per monitorare le prestazioni in generale.

Passaggio 2: raccogli i profili

La raccolta dei profili prevede tre passaggi: - compilazione del codice nativo con la misurazione, - esecuzione dell'app misurata sul dispositivo e generazione dei profili, - unione/post-elaborazione dei profili sull'host.

Crea build strumentata

I profili vengono raccolti eseguendo il carico di lavoro del passaggio 1 su una compilazione instrumentata dell'applicazione. Per generare una build con strumenti, aggiungi-fprofile-generate ai flag del compilatore e del linker. Questo flag deve essere controllato da una variabile di build separata perché il flag non è necessario durante una build predefinita.

Genera profili

Successivamente, esegui l'app sottoposta a strumenti sul dispositivo e genera i profili. I profili vengono raccolti in memoria quando viene eseguito il codice binario sottoposto a ispezione e vengono scritti in un file all'uscita. Tuttavia, le funzioni registrate con atexit non vengono richiamate in un'app per Android, che viene semplicemente interrotta.

L'applicazione/il carico di lavoro deve eseguire un'operazione aggiuntiva per impostare un percorso per il file del profilo e poi attivare esplicitamente una scrittura del profilo.

  • Per impostare il percorso del file del profilo, chiama __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. %m è utile quando sono presenti più librerie condivise. %m si espande in una firma del modulo univoca per la libreria, generando un profilo separato per ogni libreria. Consulta questa pagina per altri specificatori di pattern utili. PROFILE_DIR è una directory scrivibile dall'app. Consulta la demo per rilevare questa directory in fase di esecuzione.
  • Per attivare esplicitamente la scrittura di un profilo, chiama la funzione __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;
}

Nota: la generazione del file del profilo è più semplice se il carico di lavoro è un file binario autonomo. Basta impostare la variabile di ambiente LLVM_PROFILE_FILE su %t/default-%m.profraw prima di eseguire il file binario.

Profili post-elaborazione

I file del profilo sono in formato .profraw. Devono prima essere recuperati dal dispositivo utilizzando adb pull. Dopo il recupero, utilizza l'utilità llvm-profdata nell'NDK per convertire da .profraw a .profdata, in modo da passare al compilatore.

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

Utilizza llvm-profdata e clang della stessa release NDK per evitare la mancata corrispondenza delle versioni dei formati dei file del profilo.

Passaggio 3: utilizza i profili per creare l'applicazione

Utilizza il profilo del passaggio precedente durante una build di release della tua applicazione passando -fprofile-use=<>.profdata al compilatore e al linker. I profili possono essere utilizzati anche durante l'evoluzione del codice: il compilatore Clang può tollerare una leggera mancata corrispondenza tra il codice sorgente e i profili.

Nota: in generale, per la maggior parte delle librerie, i profili sono comuni a tutte le architetture. Ad esempio, i profili generati dalla compilazione arm64 della libreria possono essere utilizzati per tutte le architetture. L&#39;avvertenza è che se nella libreria sono presenti percorsi di codice specifici per l&#39;architettura (arm o x86 o 32 bit o 64 bit), devono essere utilizzati profili distinti per ogni configurazione.

Riassumendo

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo mostra una demo end-to-end per l'utilizzo di PGO da un'app. Fornisce dettagli aggiuntivi che non sono stati trattati in questo documento.

  • Le regole di compilazione CMake illustrano come configurare una variabile CMake che genera codice nativo con la misurazione. Se la variabile di build non è impostata, il codice nativo viene ottimizzato utilizzando i profili PGO generati in precedenza.
  • In una build con strumenti, pgodemo.cpp scrive i profili di esecuzione del carico di lavoro.
  • Una posizione in cui scrivere i profili viene ottenuta in fase di runtime in MainActivity.kt utilizzando applicationContext.cacheDir.toString().
  • Per estrarre i profili dal dispositivo senza richiedere adb root, utilizza la adb procedura qui.