Come funziona l'ottimizzazione guidata dal profilo (PGO)

L'ottimizzazione guidata dal profilo (nota anche come PGO o "pogo") è un modo per ottimizzare ulteriormente le build ottimizzate del tuo gioco utilizzando informazioni sul comportamento del tuo gioco quando viene giocato nel mondo reale. In questo modo, i codici eseguiti raramente, come errori o casi limite, vengono meno enfatizzati dai percorsi di esecuzione critici del codice, velocizzando così l'esecuzione.

Un diagramma che mostra una panoramica visiva di come
funziona PGO

Figura 1. Una panoramica del funzionamento di PGO.

Per utilizzare PGO, devi prima instrumentare la tua build per generare dati del profilo con cui il compilatore può lavorare. Poi puoi esercitarti con il codice eseguendo la build e generando uno o più file di dati del profilo. Infine, copi i file dal dispositivo e li utilizzi con il compilatore per ottimizzare l'eseguibile utilizzando le informazioni del profilo acquisite.

Come funzionano le build ottimizzate senza PGO

Una build ottimizzata senza utilizzare i dati del profilo utilizza una serie di euristiche per decidere come generare codice ottimizzato.

Alcune sono segnalate esplicitamente dallo sviluppatore, ad esempio in C++ 20 o versioni successive, utilizzando suggerimenti di direzioni di ramo come [[likely]] e [[unlikely]]. Un altro esempio è l'utilizzo della parola chiave inline o persino __forceinline (sebbene in generale sia meglio e più flessibile attenersi alla prima). Per impostazione predefinita, alcuni compilatori presumono che la prima gamba di un ramo (vale a dire l'istruzione if, non la parte else) sia la più probabile. L'ottimizzatore può anche fare ipotesi basate sull'analisi statica del codice su come verrà eseguito, ma di solito l'ambito di applicazione è limitato.

Il problema di queste euristiche è che non sono in grado di aiutare correttamente il compilatore in tutte le situazioni, anche con un markup manuale esaustivo. Pertanto, anche se il codice che viene generato è in genere ben ottimizzato, non sarebbe efficace se il compilatore avesse maggiori informazioni sul suo comportamento in fase di runtime.

Generazione di un profilo

Quando l'eseguibile viene creato con PGO abilitato in modalità instrumentata, l'eseguibile viene integrato con il codice all'inizio di ogni blocco di codice, ad esempio l'inizio di una funzione o l'inizio di ogni gruppo di un ramo. Questo codice viene utilizzato per tenere traccia di ogni volta che il blocco viene inserito in esecuzione del codice, che il compilatore può utilizzare in un secondo momento per generare il codice ottimizzato.

Viene eseguito anche un altro tracciamento, ad esempio la dimensione delle operazioni di copia tipiche in un blocco, in modo che versioni rapide e incorporate dell'operazione possano essere generate in un secondo momento.

Dopo che il gioco ha svolto un lavoro rappresentativo, l'eseguibile deve chiamare una funzione (__llvm_profile_write_file()) per scrivere i dati del profilo in una posizione personalizzabile sul dispositivo. Questa funzione viene collegata automaticamente al tuo gioco quando nella configurazione della build è abilitata la strumentazione PGO.

Il file dei dati scritti del profilo deve quindi essere copiato nel computer host e preferibilmente in una posizione insieme ad altri profili della stessa build, in modo da poter essere utilizzati insieme.

Ad esempio, puoi modificare il codice del gioco in modo da chiamare __llvm_profile_write_file() al termine della scena del gioco corrente. Poi, per creare un profilo devi creare il tuo gioco con la strumentazione attivata e poi eseguirne il deployment sul tuo dispositivo Android. Durante l'esecuzione, i dati del profilo vengono acquisiti automaticamente: l'ingegnere del QA esegue i vari passaggi del gioco, esercitando scenari diversi (o limitandosi a eseguire il test standard).

Dopo aver finito di allenare le diverse parti del gioco, puoi tornare al menu principale. In questo modo, la scena corrente del gioco terminerà e potrai scrivere i dati del profilo.

Uno script può quindi essere utilizzato per copiare i dati del profilo dal dispositivo di test e caricarli in un repository centrale dove possono essere acquisiti per un uso futuro.

Unione dei dati del profilo

Una volta ottenuto da un dispositivo, il profilo deve essere convertito dal file di dati del profilo generato dalla build con strumentazione in un formato utilizzabile dal compilatore. AGDE esegue questa operazione in modo automatico per ogni file di dati del profilo che aggiungete al progetto.

PGO è progettato per combinare i risultati di più esecuzioni di profili in strumentazione. AGDE lo fa automaticamente per te se in un singolo progetto sono presenti più file.

Per un esempio dell'utilità dell'unione di set di dati del profilo, supponiamo che tu abbia avuto un laboratorio con numerosi ingegneri del QA, che giocano tutti a livelli diversi del tuo gioco. Ogni partita viene registrata e poi utilizzata per generare i dati del profilo da una build del tuo gioco basata su PGO. L'unione dei profili ti consente di combinare i risultati di tutte queste diverse esecuzioni di test, che potrebbero eseguire parti molto diverse del codice, per ottenere risultati migliori.

Ancora meglio, quando si eseguono test longitudinali, in cui si conservano copie dei dati del profilo dalla release interna a quella interna, la ricreazione non rende necessaria la validità dei dati del profilo precedenti. Nella maggior parte dei casi, il codice è relativamente stabile da una release all'altra, quindi i dati del profilo delle build meno recenti possono essere comunque utili e non diventano obsoleti immediatamente.

Generazione di build ottimizzate guidate per i profili

Dopo aver aggiunto i dati del profilo al progetto, puoi utilizzarli per creare l'eseguibile attivando PGO in modalità di ottimizzazione nella configurazione della build.

Questo fa sì che l'ottimizzatore del compilatore utilizzi i dati del profilo acquisiti in precedenza per prendere decisioni di ottimizzazione.

Quando utilizzare l'ottimizzazione guidata dal profilo

PGO non è pensato per essere abilitato all'inizio dello sviluppo o durante l'iterazione quotidiana del codice. Durante lo sviluppo, dovresti concentrarti sulle ottimizzazioni basate su algoritmi e layout dei dati, in quanto offrono vantaggi molto maggiori.

PGO entra in una fase successiva del processo di sviluppo, quando stai preparando il rilascio. Pensa all'ottimizzazione guidata dal profilo come alla ciliegina sulla torta che ti consente di ottenere il massimo dal tuo codice dopo che hai già dedicato un po' di tempo all'ottimizzazione del codice.

Miglioramento del rendimento previsto con PGO

Ciò dipende da un gran numero di fattori, tra cui quanto sono completi e obsoleti i profili e quanto sarebbe vicino all'ottimizzazione del codice con una build ottimizzata tradizionale.

In generale, una stima molto prudente sarebbe che i costi della CPU diminuiranno di circa il 5% nei thread chiave. I risultati potrebbero essere diversi.

Overhead strumentazione

La strumentazione di PGO è completa e, sebbene sia generata automaticamente, non è senza costi. L'overhead della strumentazione PGO può variare a seconda del codebase.

Costo delle prestazioni della strumentazione guidata dal profilo

Potresti notare un calo della frequenza fotogrammi con le build con strumentazione. In alcuni casi, a seconda di quanto la CPU è utilizzata quasi al 100% durante il normale funzionamento, questo calo potrebbe essere talmente elevato da rendere difficile il gameplay normale.

Consigliamo alla maggior parte degli sviluppatori di creare una modalità di riproduzione semi-deterministica per il loro gioco. Questo tipo di funzionalità consente al team addetto al QA di iniziare il gioco da una posizione di partenza nota e ripetibile all'interno del gioco (ad esempio un gioco di salvataggio o un livello di test specifico), per poi registrare l'input. Questo input registrato dalla build di test può essere inserito in una build con strumentazione PGO, riprodotto e generare dati di profilo reali, indipendentemente dal tempo necessario per elaborare un singolo frame, anche se il gioco funzionava lentamente da non essere giocabile.

Questo tipo di funzionalità presenta anche altri vantaggi importanti, come la moltiplicazione dello sforzo del tester: un tester può registrare il proprio input su un dispositivo e quindi può essere riprodotto su più tipi di dispositivi diversi per il test del fumo.

Un sistema di replay come questo può avere enormi vantaggi su Android, dove sono presenti un gran numero di varianti di dispositivi nell'ecosistema e i vantaggi non finiscono qui: può anche costituire una parte fondamentale del tuo sistema di build di integrazione continua, consentendoti di eseguire regolari test di regressione delle prestazioni e di rilevamento della presenza di fumo durante la notte.

La registrazione deve registrare l'input dell'utente nel punto più appropriato all'interno del meccanismo di input del gioco (probabilmente non indirizzare gli eventi touchscreen, ma invece di registrare le conseguenze come comandi). Questi input devono anche presentare un conteggio dei frame che si contraddistingue in modo monotonico durante il gameplay, in modo che, durante la riproduzione, il meccanismo di riproduzione possa attendere il frame appropriato su cui dovrebbe attivare un evento.

In modalità di riproduzione, il gioco deve evitare l'accesso online, non deve mostrare annunci e deve funzionare a un passo temporale fisso (con la frequenza fotogrammi target). Dovresti disabilitare vsync.

Non è importante che tutto (ad esempio i sistemi di particelle) nel gioco sia perfettamente deterministicamente ripetibile, ma le stesse azioni dovrebbero avere le stesse conseguenze e gli stessi risultati in-game, ovvero il gameplay dovrebbe essere lo stesso.

Costo memoria della strumentazione guidata dal profilo

Il sovraccarico di memoria della strumentazione PGO varia molto in base alla libreria specifica in fase di compilazione. Nei nostri test abbiamo riscontrato un aumento complessivo delle dimensioni dell'eseguibile del test di circa 2,2 volte. Questo aumento di dimensione includeva sia il codice aggiuntivo necessario per implementare i blocchi di codice sia lo spazio necessario per archiviare i contatori. Questi test non sono stati esaustivi e la tua esperienza potrebbe essere diversa.

Quando aggiornare o eliminare i dati del profilo

Dovresti aggiornare i tuoi profili ogni volta che apporti modifiche significative al codice (o ai contenuti del gioco).

Tutto questo dipende esattamente dall'ambiente di build e dalla fase di sviluppo in cui ti trovi.

Come accennato in precedenza, non devi trasferire i dati del profilo durante le principali modifiche all'ambiente di build; anche se ciò non ti impedirà di creare o danneggiare la build, ridurrà i vantaggi in termini di prestazioni dell'utilizzo di PGO, poiché al nuovo ambiente di build saranno applicabili pochissimi dati del profilo. Tuttavia, questo non è l'unico caso in cui i dati del profilo potrebbero diventare obsoleti.

Partiamo dal presupposto che non utilizzerai PGO prima della fine dello sviluppo, quando ti prepari per la distribuzione, oltre alla raccolta di un'acquisizione settimanale in modo che i tecnici incentrati sulle prestazioni possano verificare che non ci saranno singhiozzi inaspettati prima del lancio.

Questo cambia man mano che ti avvicini al periodo di rilascio, quando il team addetto al QA esegue i test ogni giorno ed esamina il gioco in modo esaustivo. Durante questa fase puoi generare profili a partire da questi dati ogni giorno e utilizzarli per definire le build future per i test delle prestazioni e la regolazione dei tuoi budget delle prestazioni.

Quando ti prepari per la release, devi bloccare la versione della build che prevedi di rilasciare, quindi fare in modo che il QA la esegua generando i nuovi dati del profilo. Quindi, per creare una versione finale dell'eseguibile, puoi creare una build utilizzando questi dati.

Il QA può quindi fornire una versione ottimizzata della spedizione, un run-through finale per garantire che il lancio sia idoneo.