Ottimizzazione basata sul profilo

L'ottimizzazione guidata dal profilo (PGO) è una tecnica di ottimizzazione del compilatore ben nota. In PGO, il compilatore utilizza i profili di runtime delle esecuzioni di un programma per fare scelte ottimali sull'incorporamento e sul layout del codice. Questo porta a un miglioramento delle prestazioni e a una riduzione delle dimensioni del codice.

Puoi eseguire il deployment di PGO nella tua applicazione o libreria seguendo questi passaggi: 1. Identifica un carico di lavoro rappresentativo. 2. Raccogli i profili. 3. Utilizzare i profili in una build di release.

Passaggio 1: identifica un carico di lavoro rappresentativo

Innanzitutto, identifica un benchmark o un carico di lavoro rappresentativo per la tua applicazione. Questo è un passaggio critico poiché i profili raccolti dal carico di lavoro identificano le regioni calde e fredde nel codice. Quando utilizzi i profili, il compilatore eseguirà ottimizzazioni aggressive e l'inserimento incorporato nelle regioni più attive. Il compilatore può anche scegliere di ridurre le dimensioni del codice delle regioni a freddo, a scapito delle prestazioni.

Identificare un buon carico di lavoro è utile anche per tenere traccia delle prestazioni in generale.

Passaggio 2: raccogli i profili

La raccolta dei profili prevede tre passaggi: - creazione di codice nativo con la strumentazione - esecuzione dell'app instrumentata sul dispositivo e generazione di profili e - unione/post-elaborazione dei profili sull'host.

Crea build strumentale

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

Genera profili

Quindi, esegui l'app con gli strumenti sul dispositivo e genera profili. I profili vengono raccolti in memoria quando viene eseguito il programma binario strumentato e vengono scritti in un file all'uscita. Tuttavia, le funzioni registrate con atexit non vengono chiamate in un'app Android, l'app viene semplicemente interrotta.

L'applicazione/il carico di lavoro deve svolgere un ulteriore lavoro per impostare un percorso per il file del profilo e quindi attivare esplicitamente la 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 quella libreria, il che genera un profilo separato per ogni libreria. Consulta qui per altri utili identificatori di pattern. PROFILE_DIR è una directory a cui è possibile scrivere dall'app. Consulta la demo per il rilevamento di questa directory in fase di runtime.
  • 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 di profilo è più semplice se il carico di lavoro è un programma binario autonomo. Devi solo impostare la variabile di ambiente LLVM_PROFILE_FILE su %t/default-%m.profraw prima di eseguire il programma binario.

Profili post-elaborazione

I file del profilo sono in formato .profraw. Devono essere prima recuperate dal dispositivo utilizzando adb pull. Dopo il recupero, utilizza l'utilità llvm-profdata nell'NDK per convertire da .profraw a .profdata, che possono poi essere passati 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 che la versione non corrisponda ai formati 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 con l'evolversi del codice: il compilatore Clang può tollerare una leggera mancata corrispondenza tra l'origine 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 build arm64 della libreria possono essere utilizzati per tutte le architetture. Il problema è che, se nella libreria sono presenti percorsi di codice specifici per l'architettura (arm o x86 o a 32 bit o 64 bit), per ciascuna configurazione devono essere utilizzati profili separati.

Riassumendo

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

  • Le regole di build di CMake mostrano come configurare una variabile CMake che crei codice nativo con la strumentazione. Se la variabile di build non è impostata, il codice nativo viene ottimizzato utilizzando i profili PGO generati in precedenza.
  • In una build con strumentazione, pgodemo.cpp scrive che i profili sono in esecuzione del carico di lavoro.
  • Una posizione scrivibile per i profili si ottiene al runtime in MainActivity.kt utilizzando applicationContext.cacheDir.toString().
  • Per estrarre i profili dal dispositivo senza richiedere adb root, usa la ricetta adb qui.