Compilazione just-in-time - Just-in-time compilation

Nel calcolo , just-in-time ( JIT ) di compilazione (anche dinamica di traduzione o compilation run-time ) è un modo di eseguire codice informatico che prevede la compilazione durante l'esecuzione di un programma (in fase di esecuzione ), piuttosto che prima dell'esecuzione. Questo può consistere nella traduzione del codice sorgente, ma più comunemente è la traduzione del bytecode in codice macchina , che viene quindi eseguito direttamente. Un sistema che implementa un compilatore JIT in genere analizza continuamente il codice in esecuzione e identifica parti del codice in cui l'accelerazione ottenuta dalla compilazione o dalla ricompilazione supererebbe il sovraccarico della compilazione di quel codice.

La compilazione JIT è una combinazione dei due approcci tradizionali alla traduzione in codice macchina - compilazione anticipata (AOT) e interpretazione - e combina alcuni vantaggi e svantaggi di entrambi. Approssimativamente, la compilazione JIT combina la velocità del codice compilato con la flessibilità dell'interpretazione, con il sovraccarico di un interprete e il sovraccarico aggiuntivo della compilazione e del collegamento (non solo dell'interpretazione). La compilazione JIT è una forma di compilazione dinamica e consente l'ottimizzazione adattiva come la ricompilazione dinamica e l' accelerazione specifica della microarchitettura . L'interpretazione e la compilazione JIT sono particolarmente adatte per i linguaggi di programmazione dinamici , in quanto il sistema runtime può gestire tipi di dati ad associazione tardiva e applicare garanzie di sicurezza.

Storia

Il primo compilatore JIT pubblicato è generalmente attribuito al lavoro su LISP di John McCarthy nel 1960. Nel suo articolo fondamentale Funzioni ricorsive di espressioni simboliche e loro calcolo da parte della macchina, Parte I , cita le funzioni che vengono tradotte durante il runtime, risparmiando così la necessità di salvare l'output del compilatore su schede perforate (anche se questo sarebbe più precisamente noto come " Compile and go system "). Un altro primo esempio è stato quello di Ken Thompson , che nel 1968 ha fornito una delle prime applicazioni delle espressioni regolari , qui per il pattern matching nell'editor di testo QED . Per la velocità, Thompson ha implementato la corrispondenza delle espressioni regolari mediante JITing al codice IBM 7094 sul sistema di condivisione del tempo compatibile . Una tecnica influente per derivare il codice compilato dall'interpretazione è stata introdotta da James G. Mitchell nel 1970, che ha implementato per il linguaggio sperimentale LC² .

Smalltalk (c. 1983) ha aperto la strada a nuovi aspetti delle compilation JIT. Ad esempio, la traduzione in codice macchina è stata eseguita su richiesta e il risultato è stato memorizzato nella cache per un uso successivo. Quando la memoria diventava scarsa, il sistema eliminava parte di questo codice e lo rigenerava quando era di nuovo necessario. Il linguaggio Self di Sun ha migliorato ampiamente queste tecniche ed è stato a un certo punto il sistema Smalltalk più veloce al mondo; raggiungendo fino alla metà della velocità del C ottimizzato ma con un linguaggio completamente orientato agli oggetti.

Self è stato abbandonato da Sun, ma la ricerca è andata al linguaggio Java. Il termine "Compilazione just-in-time" è stato preso in prestito dal termine di produzione " Just in time " e reso popolare da Java, con James Gosling che utilizza il termine dal 1993. Attualmente il JITing è utilizzato dalla maggior parte delle implementazioni della Java Virtual Machine , come HotSpot si basa e utilizza ampiamente questa base di ricerca.

Il progetto HP Dynamo era un compilatore JIT sperimentale in cui il formato "bytecode" e il formato del codice macchina erano gli stessi; il sistema ha trasformato il codice macchina PA-6000 in codice macchina PA-8000 . Controintuitivamente, ciò ha comportato un aumento della velocità, in alcuni casi del 30% poiché ciò ha consentito ottimizzazioni a livello di codice macchina, ad esempio, inline del codice per un migliore utilizzo della cache e ottimizzazioni delle chiamate a librerie dinamiche e molte altre ottimizzazioni di runtime che convenzionali i compilatori non sono in grado di tentare.

Nel novembre 2020, PHP 8.0 ha introdotto un compilatore JIT.

Design

In un sistema compilato in bytecode, il codice sorgente viene tradotto in una rappresentazione intermedia nota come bytecode . Bytecode non è il codice macchina per un computer particolare e può essere portabile tra le architetture di computer. Il bytecode può quindi essere interpretato o eseguito su una macchina virtuale . Il compilatore JIT legge i bytecode in molte sezioni (o per intero, raramente) e li compila dinamicamente in codice macchina in modo che il programma possa essere eseguito più velocemente. Questo può essere fatto per file, per funzione o anche su qualsiasi frammento di codice arbitrario; il codice può essere compilato quando sta per essere eseguito (da cui il nome "just-in-time"), e poi memorizzato nella cache e riutilizzato in seguito senza bisogno di essere ricompilato.

Al contrario, una macchina virtuale interpretata tradizionale interpreterà semplicemente il bytecode, generalmente con prestazioni molto inferiori. Alcuni interpreti interpretano persino il codice sorgente, senza il passaggio della prima compilazione in bytecode, con prestazioni ancora peggiori. Il codice compilato staticamente o il codice nativo viene compilato prima della distribuzione. Un ambiente di compilazione dinamica è quello in cui il compilatore può essere utilizzato durante l'esecuzione. Un obiettivo comune dell'utilizzo delle tecniche JIT è raggiungere o superare le prestazioni della compilazione statica , pur mantenendo i vantaggi dell'interpretazione del bytecode: gran parte del "pesante sollevamento" dell'analisi del codice sorgente originale e dell'esecuzione dell'ottimizzazione di base viene spesso gestito in fase di compilazione, prima della distribuzione: la compilazione dal bytecode al codice macchina è molto più veloce della compilazione dal sorgente. Il bytecode distribuito è portatile, a differenza del codice nativo. Poiché il runtime ha il controllo sulla compilazione, come il bytecode interpretato, può essere eseguito in una sandbox sicura. I compilatori da bytecode a codice macchina sono più facili da scrivere, perché il compilatore di bytecode portatile ha già svolto gran parte del lavoro.

Il codice JIT offre generalmente prestazioni di gran lunga migliori rispetto agli interpreti. Inoltre, in alcuni casi può offrire prestazioni migliori rispetto alla compilazione statica, poiché molte ottimizzazioni sono realizzabili solo in fase di esecuzione:

  1. La compilazione può essere ottimizzata per la CPU di destinazione e il modello di sistema operativo in cui viene eseguita l'applicazione. Ad esempio, JIT può scegliere le istruzioni CPU vettoriali SSE2 quando rileva che la CPU le supporta. Per ottenere questo livello di specificità di ottimizzazione con un compilatore statico, è necessario compilare un binario per ogni piattaforma/architettura prevista, oppure includere più versioni di porzioni del codice all'interno di un singolo binario.
  2. Il sistema è in grado di raccogliere statistiche su come il programma è effettivamente in esecuzione nell'ambiente in cui si trova e può riorganizzare e ricompilare per prestazioni ottimali. Tuttavia, alcuni compilatori statici possono anche accettare le informazioni sul profilo come input.
  3. Il sistema può eseguire ottimizzazioni globali del codice (ad es. l' inlining delle funzioni di libreria) senza perdere i vantaggi del collegamento dinamico e senza le spese generali inerenti ai compilatori e ai linker statici. In particolare, quando si eseguono sostituzioni in linea globali, un processo di compilazione statica potrebbe richiedere controlli in fase di esecuzione e garantire che si verifichi una chiamata virtuale se la classe effettiva dell'oggetto sovrascrive il metodo in linea e potrebbe essere necessario elaborare i controlli delle condizioni al contorno sugli accessi all'array all'interno dei loop. Con la compilazione just-in-time in molti casi questa elaborazione può essere spostata fuori dai cicli, spesso dando grandi aumenti di velocità.
  4. Sebbene ciò sia possibile con i linguaggi di raccolta dei rifiuti compilati in modo statico, un sistema di bytecode può riorganizzare più facilmente il codice eseguito per un migliore utilizzo della cache.

Poiché un JIT deve eseguire il rendering ed eseguire un'immagine binaria nativa in fase di esecuzione, i veri JIT in codice macchina necessitano di piattaforme che consentano l'esecuzione dei dati in fase di esecuzione, rendendo impossibile l'utilizzo di tali JIT su una macchina basata sull'architettura di Harvard ; lo stesso si può dire anche per alcuni sistemi operativi e macchine virtuali. Tuttavia, un tipo speciale di "JIT" potrebbe potenzialmente non mirare all'architettura della CPU della macchina fisica, ma piuttosto a un bytecode VM ottimizzato in cui prevalgono le limitazioni sul codice macchina non elaborato, specialmente dove la VM di quel bytecode alla fine sfrutta un JIT per il codice nativo.

Prestazione

JIT causa un ritardo da lieve a notevole nell'esecuzione iniziale di un'applicazione, a causa del tempo impiegato per caricare e compilare il bytecode. A volte questo ritardo è chiamato "ritardo del tempo di avvio" o "tempo di riscaldamento". In generale, maggiore è l'ottimizzazione JIT eseguita, migliore sarà il codice che genererà, ma aumenterà anche il ritardo iniziale. Un compilatore JIT deve quindi fare un compromesso tra il tempo di compilazione e la qualità del codice che spera di generare. Il tempo di avvio può includere un aumento delle operazioni IO-bound oltre alla compilazione JIT: ad esempio, il file di dati della classe rt.jar per la Java Virtual Machine (JVM) è di 40 MB e la JVM deve cercare molti dati in questo file contestualmente enorme .

Una possibile ottimizzazione, utilizzata da HotSpot Java Virtual Machine di Sun , consiste nel combinare l'interpretazione e la compilazione JIT. Il codice dell'applicazione viene inizialmente interpretato, ma la JVM controlla quali sequenze di bytecode vengono eseguite di frequente e le traduce in codice macchina per l'esecuzione diretta sull'hardware. Per bytecode che vengono eseguiti solo poche volte, questo fa risparmiare tempo di compilazione e riduce la latenza iniziale; per bytecode eseguiti di frequente, viene utilizzata la compilazione JIT per l'esecuzione ad alta velocità, dopo una fase iniziale di lenta interpretazione. Inoltre, poiché un programma trascorre la maggior parte del tempo nell'esecuzione di una minoranza del suo codice, il tempo di compilazione ridotto è significativo. Infine, durante l'interpretazione iniziale del codice, è possibile raccogliere le statistiche di esecuzione prima della compilazione, il che aiuta a eseguire una migliore ottimizzazione.

Il giusto compromesso può variare a seconda delle circostanze. Ad esempio, la Java Virtual Machine di Sun ha due modalità principali: client e server. In modalità client, viene eseguita una compilazione e un'ottimizzazione minime per ridurre i tempi di avvio. In modalità server, viene eseguita un'ampia compilazione e ottimizzazione, per massimizzare le prestazioni una volta che l'applicazione è in esecuzione, sacrificando il tempo di avvio. Altri compilatori Java just-in-time hanno utilizzato una misurazione di runtime del numero di volte in cui un metodo è stato eseguito in combinazione con la dimensione del bytecode di un metodo come euristica per decidere quando compilare. Un altro ancora utilizza il numero di volte eseguito combinato con il rilevamento di loop. In generale, è molto più difficile prevedere con precisione quali metodi ottimizzare nelle applicazioni di breve durata rispetto a quelle di lunga durata.

Native Image Generator (Ngen) di Microsoft è un altro approccio per ridurre il ritardo iniziale. Ngen precompila (o "pre-JIT") il bytecode in un'immagine Common Intermediate Language nel codice nativo della macchina. Di conseguenza, non è necessaria alcuna compilazione runtime. .NET Framework 2.0 fornito con Visual Studio 2005 esegue Ngen su tutte le DLL della libreria Microsoft subito dopo l'installazione. Il pre-jitting fornisce un modo per migliorare il tempo di avvio. Tuttavia, la qualità del codice che genera potrebbe non essere buona come quella che è JIT, per gli stessi motivi per cui il codice compilato staticamente, senza l' ottimizzazione guidata dal profilo , non può essere buono come il codice compilato JIT nel caso estremo: la mancanza dei dati di profilazione per guidare, ad esempio, la memorizzazione nella cache in linea.

Esistono anche implementazioni Java che combinano un compilatore AOT (in anticipo sui tempi) con un compilatore JIT ( Excelsior JET ) o un interprete ( GNU Compiler for Java ).

Sicurezza

La compilazione JIT utilizza fondamentalmente dati eseguibili e quindi pone sfide alla sicurezza e possibili exploit.

L'implementazione della compilazione JIT consiste nel compilare codice sorgente o byte code in codice macchina ed eseguirlo. Questo viene generalmente fatto direttamente in memoria: il compilatore JIT emette il codice macchina direttamente in memoria e lo esegue immediatamente, invece di emetterlo su disco e quindi invocare il codice come un programma separato, come nella consueta compilazione anticipata. Nelle architetture moderne questo incorre in un problema dovuto alla protezione dello spazio eseguibile : la memoria arbitraria non può essere eseguita, altrimenti si verifica un potenziale buco di sicurezza. Quindi la memoria deve essere contrassegnata come eseguibile; per motivi di sicurezza questo dovrebbe essere fatto dopo che il codice è stato scritto in memoria e contrassegnato come di sola lettura, poiché la memoria scrivibile/eseguibile è una falla di sicurezza (vedi W^X ). Ad esempio, il compilatore JIT di Firefox per Javascript ha introdotto questa protezione in una versione di rilascio con Firefox 46.

La spruzzatura JIT è una classe di exploit di sicurezza del computer che utilizzano la compilazione JIT per la spruzzatura dell'heap : la memoria risultante è quindi eseguibile, il che consente un exploit se l'esecuzione può essere spostata nell'heap.

Usi

La compilazione JIT può essere applicata ad alcuni programmi o può essere utilizzata per determinate capacità, in particolare capacità dinamiche come le espressioni regolari . Ad esempio, un editor di testo può compilare un'espressione regolare fornita in fase di esecuzione al codice macchina per consentire una corrispondenza più rapida: questa operazione non può essere eseguita in anticipo, poiché il modello viene fornito solo in fase di esecuzione. Diversi moderni ambienti di runtime si affidano a compilazione JIT per l'esecuzione di codice ad alta velocità, tra cui la maggior parte delle implementazioni di Java , insieme a Microsoft 's .NET Framework . Allo stesso modo, molte librerie di espressioni regolari presentano la compilazione JIT di espressioni regolari, sia in bytecode che in codice macchina. La compilazione JIT viene utilizzata anche in alcuni emulatori, per tradurre il codice macchina da un'architettura CPU a un'altra.

Un'implementazione comune della compilazione JIT consiste nell'avere prima la compilazione AOT in bytecode ( codice macchina virtuale ), nota come compilazione bytecode , e quindi la compilazione JIT in codice macchina (compilazione dinamica), anziché l'interpretazione del bytecode. Ciò migliora le prestazioni di runtime rispetto all'interpretazione, a scapito del ritardo dovuto alla compilazione. I compilatori JIT traducono continuamente, come con gli interpreti, ma la memorizzazione nella cache del codice compilato riduce al minimo il ritardo sull'esecuzione futura dello stesso codice durante una determinata esecuzione. Poiché viene compilata solo una parte del programma, c'è un ritardo significativamente inferiore rispetto a quello che si avrebbe se l'intero programma fosse stato compilato prima dell'esecuzione.

Guarda anche

Appunti

Riferimenti

Ulteriori letture

link esterno