Come il compilatore JIT ottimizza il codice

Quando viene scelto un metodo per la compilazione, la JVM fornisce i propri bytecode al compilatore Just - In - Time (JIT). JIT deve comprendere la semantica e la sintassi dei bytecode prima di poter compilare correttamente il metodo.

Per consentire al compilatore JIT di analizzare il metodo, i bytecode vengono riformulati per la prima volta in una rappresentazione interna denominata trees, che assomiglia più al codice macchina che ai bytecode. Analisi e ottimizzazioni vengono quindi eseguite sulle strutture ad albero del metodo. Alla fine, le strutture ad albero sono tradotte in codice nativo. Il resto di questa sezione fornisce una breve panoramica delle fasi della compilazione JIT. Per ulteriori informazioni, consultare Diagnosi di un problema JIT o AOT.

Il compilatore JIT può utilizzare più di un thread di compilazione per eseguire le attività di compilazione JIT. L'utilizzo di più thread può potenzialmente aiutare le applicazioni Java ad avviarsi più velocemente. In pratica, più thread di compilazione JIT mostrano miglioramenti delle prestazioni solo quando ci sono core di elaborazione non utilizzati nel sistema.

Il numero predefinito di thread di compilazione è identificato dalla JVM ed è dipendente dalla configurazione del sistema. Se il numero di thread risultante non è ottimale, è possibile sovrascrivere la decisione JVM utilizzando l'opzione -XcompilationThreads . Per informazioni sull'utilizzo di questa opzione, consultare Opzioni -X.
Nota: se il proprio sistema non dispone di core di elaborazione inutilizzati, è improbabile che l'aumento del numero di thread di compilazione produca un miglioramento delle prestazioni.

La compilazione si compone delle seguenti fasi. Tutte le fasi tranne la generazione di codice nativo sono codice multipiattaforma.

Fase 1 - allineamento

L'allineamento è il processo mediante il quale gli alberi dei metodi più piccoli vengono uniti, o "allineati", negli alberi dei loro chiamanti. Ciò accelera le chiamate di metodo eseguite di frequente. Vengono utilizzati due algoritmi di allineamento con diversi livelli di aggressività, a seconda del livello di ottimizzazione corrente. Le ottimizzazioni eseguite in questa fase includono:
  • Allineamento semplice
  • Allineamento grafico chiamate
  • Eliminazione della ricorsione della coda
  • Ottimizzazioni call guard virtuali

Fase 2 - ottimizzazioni locali

Le ottimizzazioni locali analizzano e migliorano una piccola sezione del codice alla volta. Molte ottimizzazioni locali implementano tecniche collaudate utilizzate nei compilatori statici classici. Le ottimizzazioni includono:
  • Ottimizzazioni e analisi dei flussi di dati locali
  • Registra ottimizzazione utilizzo
  • Semplificazioni dei modi di dire Java
Queste tecniche vengono applicate ripetutamente, soprattutto dopo le ottimizzazioni globali, che potrebbero aver evidenziato maggiori opportunità di miglioramento.

Fase 3 - ottimizzazione del flusso di controllo

Le ottimizzazioni del flusso di controllo analizzano il flusso di controllo all'interno di un metodo (o sezioni specifiche di esso) e riorganizzano i percorsi di codice per migliorarne l'efficienza. Le ottimizzazioni sono:
  • Riordino, suddivisione e rimozione del codice
  • Riduzione e inversione del loop
  • Striding del loop e movimento del codice invariante del loop
  • Srotolamento e sbucciatura del loop
  • Controllo delle versioni e specializzazione dei loop
  • Ottimizzazione gestita da eccezioni
  • Analisi switch

Fase 4 - ottimizzazioni globali

Le ottimizzazioni globali funzionano sull'intero metodo contemporaneamente. Sono più "costosi", richiedendo maggiori quantità di tempo di compilazione, ma possono fornire un grande aumento delle prestazioni. Le ottimizzazioni sono:
  • Ottimizzazioni e analisi dei flussi di dati globali
  • Eliminazione della ridondanza parziale
  • Analisi di escape
  • Ottimizzazioni GC e allocazione memoria
  • Ottimizzazioni di sincronizzazione

Fase 5 - generazione codice nativo

I processi di generazione del codice nativo variano, a seconda dell'architettura della piattaforma. Generalmente, durante questa fase della compilazione, gli alberi di un metodo vengono tradotti in istruzioni di codice macchina; alcune piccole ottimizzazioni vengono eseguite in base alle caratteristiche dell'architettura. Il codice compilato viene inserito in una parte dello spazio del processo JVM denominato code cache; l'ubicazione del metodo nella code cache viene registrata, in modo che le chiamate future ad esso richiamino il codice compilato. In qualsiasi momento, il processo JVM è costituito dai file eseguibili JVM e da una serie di codici compilati JIT collegati dinamicamente all'interprete bytecode nella JVM.