Comprensione delle differenze: interprete tradizionale, compilatore JIT, interprete JIT e compilatore AOT


130

Sto cercando di capire le differenze tra un interprete tradizionale, un compilatore JIT, un interprete JIT e un compilatore AOT.

Un interprete è solo una macchina (virtuale o fisica) che esegue le istruzioni in un linguaggio informatico. In tal senso, JVM è un interprete e le CPU fisiche sono interpreti.

Compilazione anticipata significa semplicemente compilare il codice in un linguaggio prima di eseguirlo (interpretarlo).

Tuttavia, non sono sicuro delle esatte definizioni di un compilatore JIT e di un interprete JIT.

Secondo una definizione che ho letto, la compilazione JIT è semplicemente compilare il codice appena prima di interpretarlo.

Quindi, in sostanza, la compilation JIT è una compilation AOT, eseguita prima dell'esecuzione (interpretazione)?

E un interprete JIT, è un programma che contiene sia un compilatore JIT che un interprete e compila il codice (JITs) appena prima di interpretarlo?

Si prega di chiarire le differenze.


4
Perché ti ha portato a credere che ci sia una distinzione tra "compilatore JIT" e "interprete JIT"? Sono essenzialmente due parole diverse per la stessa cosa. Il concetto generale di un JIT è lo stesso, ma esiste una grande varietà di tecniche di implementazione che non possono essere semplicemente divise in "compilatore" vs "interprete".
Greg Hewgill,

2
Leggi i wiki sulla compilazione di Just In Time , compilatore AOT , compilatore , interprete , bytecode e anche il libro di Queinnec Lisp in Small Pieces
Basile Starynkevitch,

Risposte:


198

Panoramica

Un interprete per la lingua X è un programma (o una macchina, o solo qualche tipo di meccanismo in generale) che esegue qualsiasi programma p scritto in linguaggio X tale che esegue gli effetti e valuta i risultati come prescritto dalle specifiche di X . Le CPU sono di solito interpreti per i rispettivi set di istruzioni, sebbene le moderne CPU per workstation ad alte prestazioni siano in realtà più complesse di così; possono effettivamente avere un set di istruzioni privato proprietario sottostante e tradurre (compilare) o interpretare il set di istruzioni pubblico visibile esternamente.

Un compilatore da X a Y è un programma (o una macchina, o semplicemente un qualche tipo di meccanismo in generale) che traduce qualsiasi programma p da una lingua X in un programma semanticamente equivalente p ′ in una lingua Y in modo tale che la semantica del programma sono conservati, cioè che interpretando p ' con un interprete per Y produrrà gli stessi risultati e hanno gli stessi effetti come interpretare p con un interprete per X . (Nota che X e Y possono avere la stessa lingua.)

I termini Ahead-of-Time (AOT) e Just-in-Time (JIT) si riferiscono a quando ha luogo la compilazione: il "tempo" a cui si fa riferimento in questi termini è "runtime", ovvero un compilatore JIT compila il programma così com'è in esecuzione , un compilatore AOT compila il programma prima che sia in esecuzione . Si noti che ciò richiede che un compilatore JIT dalla lingua X alla lingua Y debba in qualche modo collaborare con un interprete per la lingua Y, altrimenti non ci sarebbe alcun modo per eseguire il programma. (Ad esempio, un compilatore JIT che compila JavaScript in codice macchina x86 non ha senso senza una CPU x86; compila il programma mentre è in esecuzione, ma senza la CPU x86 il programma non sarebbe in esecuzione.)

Si noti che questa distinzione non ha senso per gli interpreti: un interprete esegue il programma. L'idea di un interprete AOT che esegue un programma prima che venga eseguito o di un interprete JIT che esegue un programma mentre è in esecuzione non ha senso.

Quindi abbiamo:

  • Compilatore AOT: compila prima dell'esecuzione
  • Compilatore JIT: compila durante l'esecuzione
  • interprete: corre

Compilatori JIT

All'interno della famiglia di compilatori JIT, ci sono ancora molte differenze su quando si compilano esattamente , con quale frequenza e con quale granularità.

Il compilatore JIT nel CLR di Microsoft, ad esempio, compila il codice una sola volta (quando viene caricato) e compila un intero assembly alla volta. Altri compilatori possono raccogliere informazioni mentre il programma è in esecuzione e ricompilare il codice più volte quando diventano disponibili nuove informazioni che consentono loro di ottimizzarle meglio. Alcuni compilatori JIT sono persino in grado di de-ottimizzare il codice. Ora, potresti chiederti perché uno vorrebbe mai farlo? La de-ottimizzazione consente di eseguire ottimizzazioni molto aggressive che potrebbero non essere sicure: se si scopre che si è troppo aggressivi si può semplicemente tornare indietro, mentre, con un compilatore JIT che non può de-ottimizzare, non è possibile eseguire il ottimizzazioni aggressive in primo luogo.

I compilatori JIT possono compilare un'unità statica di codice in una volta sola (un modulo, una classe, una funzione, un metodo, ...; questi sono in genere chiamati JIT metodo alla volta , ad esempio) o possono tracciare la dinamica esecuzione di codice per trovare tracce dinamiche (in genere loop) che verranno poi compilate (queste sono chiamate JIT di traccia ).

Combinazione di interpreti e compilatori

Interpreti e compilatori possono essere combinati in un motore di esecuzione in una sola lingua. Esistono due scenari tipici in cui ciò avviene.

Combinando un compilatore AOT da X a Y con un interprete per Y . Qui, in genere X è un linguaggio di livello superiore ottimizzato per la leggibilità da parte dell'uomo, mentre Yè un linguaggio compatto (spesso una specie di bytecode) ottimizzato per l'interpretazione da parte delle macchine. Ad esempio, il motore di esecuzione Python di CPython ha un compilatore AOT che compila il codice sorgente Python in bytecode CPython e un interprete che interpreta il bytecode CPython. Allo stesso modo, il motore di esecuzione YARV Ruby ha un compilatore AOT che compila il codice sorgente Ruby in bytecode YARV e un interprete che interpreta il bytecode YARV. Perché vorresti farlo? Ruby e Python sono entrambi linguaggi di livello molto alto e alquanto complessi, quindi prima li compiliamo in un linguaggio che è più facile da analizzare e più facile da interpretare, quindi interpretiamo quel linguaggio.

L'altro modo per combinare un interprete e un compilatore è un motore di esecuzione in modalità mista . Qui, "Mix" due "modi" di attuazione della stessa lingua insieme, vale a dire un interprete per X e un compilatore JIT da X a Y . (Quindi, la differenza qui è che nel caso precedente, abbiamo avuto più "fasi" con il compilatore che compila il programma e quindi alimentava il risultato nell'interprete, qui abbiamo i due che lavorano fianco a fianco nella stessa lingua. ) Il codice che è stato compilato da un compilatore tende a essere eseguito più velocemente del codice eseguito da un interprete, ma in realtà la prima compilazione del codice richiede tempo (e in particolare, se si desidera ottimizzare pesantemente l'esecuzione del codiceveramente veloce, ci vuole molto tempo). Quindi, per colmare questa volta in cui il compilatore JIT è impegnato a compilare il codice, l'interprete può già iniziare a eseguire il codice e una volta terminata la compilazione di JIT, possiamo passare all'esecuzione al codice compilato. Ciò significa che otteniamo entrambe le migliori prestazioni possibili del codice compilato, ma non dobbiamo aspettare il completamento della compilazione e la nostra applicazione inizia a funzionare immediatamente (anche se non la più veloce possibile).

Questa è in realtà solo l'applicazione più semplice possibile di un motore di esecuzione in modalità mista. Possibilità più interessanti, ad esempio, di non iniziare subito la compilazione, ma di far funzionare l'interprete per un po 'e di raccogliere statistiche, informazioni di profilazione, informazioni di tipo, informazioni sulla probabilità di prendere specifici rami condizionali, quali metodi sono chiamati molto spesso ecc. e quindi fornire queste informazioni dinamiche al compilatore in modo che possa generare codice più ottimizzato. Questo è anche un modo per implementare la de-ottimizzazione di cui ho parlato sopra: se si scopre che eri troppo aggressivo nell'ottimizzazione, puoi buttare via (una parte del) codice e tornare all'interpretazione. HotSpot JVM lo fa, ad esempio. Contiene sia un interprete per il bytecode JVM sia un compilatore per il bytecode JVM. (Infatti,due compilatori!)

È anche possibile e di fatto comune a combinare questi due approcci: due fasi con la prima è un compilatore AOT che compila X a Y e la seconda fase essendo un motore a modalità mista che sia interpreta Y e compila Y a Z . Il motore di esecuzione Rubinius Ruby funziona in questo modo, ad esempio: ha un compilatore AOT che compila il codice sorgente Ruby in bytecode Rubinius e un motore in modalità mista che prima interpreta il bytecode Rubinius e una volta che ha raccolto alcune informazioni compila i metodi più spesso chiamati in nativo codice macchina.

Si noti che il ruolo svolto dall'interprete nel caso di un motore di esecuzione in modalità mista, vale a dire fornire un avvio rapido e anche potenzialmente raccogliere informazioni e fornire funzionalità di fallback, può anche essere svolto da un secondo compilatore JIT. Ecco come funziona V8, ad esempio. V8 non interpreta mai, si compila sempre. Il primo compilatore è un compilatore molto veloce e molto sottile che si avvia molto rapidamente. Il codice che produce non è molto veloce, però. Questo compilatore inserisce anche il codice di profilazione nel codice che genera. L'altro compilatore è più lento e utilizza più memoria, ma produce codice molto più veloce e può utilizzare le informazioni di profilazione raccolte eseguendo il codice compilato dal primo compilatore.


1
I compilatori bytecode Python e Ruby contano davvero come AOT? Dato che entrambe le lingue consentono il caricamento dinamico dei moduli, che vengono compilati man mano che vengono caricati, vengono eseguiti durante il runtime del programma.
Sebastian Redl,

1
@SebastianRedl, con CPython puoi eseguire python -m compileall .o caricare i moduli una volta. Anche in quest'ultimo caso, poiché i file rimangono e vengono riutilizzati dopo la prima esecuzione, sembra AOT.
Paul Draper,

Hai riferimenti per ulteriori letture? Vorrei sapere di più su V8.
Vince Panuccio,

@VincePanuccio Il post fa riferimento ai compilatori Full-Codegen e Crankshaft che sono stati sostituiti . Puoi trovarli online.
eush77,

CLR Jit compila metodo per metodo, non l'intero assieme
Grigory
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.