La compilazione che produce un bytecode temporaneo (come con Java), piuttosto che andare "fino in fondo" al codice macchina, generalmente comporta una minore complessità (e quindi probabilmente richiede meno tempo)?
Sì, la compilazione in bytecode Java è più semplice rispetto alla compilazione su codice macchina. Ciò è dovuto in parte al fatto che esiste un solo formato di destinazione (come menziona Mandrill, sebbene ciò riduce solo la complessità del compilatore, non il tempo di compilazione), in parte perché la JVM è una macchina molto più semplice e più conveniente da programmare rispetto alle CPU reali - come è stato progettato in In tandem con il linguaggio Java, la maggior parte delle operazioni di Java esegue una mappatura esattamente su un'operazione bytecode in un modo molto semplice. Un altro motivo molto importante è che praticamente l'ottimizzazione no avviene. Quasi tutti i problemi di efficienza sono lasciati al compilatore JIT (o alla JVM nel suo insieme), quindi l'intera parte centrale dei normali compilatori scompare. Può fondamentalmente percorrere l'AST una volta e generare sequenze di bytecode pronte per ogni nodo. C'è un po 'di "overhead amministrativo" nella generazione di tabelle dei metodi, pool costanti, ecc. Ma non è nulla in confronto alle complessità di, diciamo, LLVM.
Un compilatore è semplicemente un programma che prende i file di testo 1 leggibili dall'uomo e li traduce in istruzioni binarie per una macchina. Se fai un passo indietro e pensi alla tua domanda da questa prospettiva teorica, la complessità è più o meno la stessa. Tuttavia, a un livello più pratico, i compilatori di codice byte sono più semplici.
Quali vasti passi devono essere fatti per compilare un programma?
Ci sono solo due vere differenze tra i due.
In generale, un programma con più unità di compilazione richiede il collegamento durante la compilazione al codice macchina e generalmente non con il codice byte. Si potrebbe dividere i capelli sul fatto che il collegamento faccia parte della compilazione nel contesto di questa domanda. In tal caso, la compilazione del codice byte sarebbe leggermente più semplice. Tuttavia, la complessità del collegamento viene compensata in fase di esecuzione quando molti problemi di collegamento vengono gestiti dalla VM (vedere la nota seguente).
I compilatori di codice byte tendono a non ottimizzare tanto perché la VM può farlo al volo (i compilatori JIT sono un'aggiunta abbastanza standard alle macchine virtuali oggigiorno).
Da ciò traggo la conclusione che i compilatori di codice byte possono omettere la complessità della maggior parte delle ottimizzazioni e di tutti i collegamenti, rinviando entrambi al runtime VM. I compilatori di codice byte sono più semplici in pratica perché spostano molte complessità sulla macchina virtuale che i compilatori di codice macchina assumono da soli.
1 Senza contare lingue esoteriche
Direi che semplifica la progettazione del compilatore poiché la compilazione è sempre Java in codice generico di macchine virtuali. Ciò significa anche che è necessario compilare il codice una sola volta e verrà eseguito su qualsiasi piattaforma (anziché dover compilare su ciascuna macchina). Non sono così sicuro se il tempo di compilazione sarà inferiore perché puoi considerare la macchina virtuale come una macchina standardizzata.
D'altra parte, ogni macchina dovrà avere la Java Virtual Machine caricata in modo che possa interpretare il "codice byte" (che è il codice macchina virtuale derivato dalla compilazione del codice java), tradurlo nel codice macchina reale ed eseguilo.
Questo è utile per programmi molto grandi ma molto brutti per quelli piccoli (perché la macchina virtuale è uno spreco di memoria).
La complessità della compilazione dipende in gran parte dal divario semantico tra la lingua di partenza e la lingua di destinazione e il livello di ottimizzazione che si desidera applicare mentre si colma questa lacuna.
Ad esempio, la compilazione del codice sorgente Java sul codice byte JVM è relativamente semplice, poiché esiste un sottoinsieme core di Java che mappa praticamente direttamente su un sottoinsieme del codice byte JVM. Ci sono alcune differenze: Java ha cicli ma no GOTO
, la JVM ha GOTO
ma nessun loop, Java ha generici, la JVM no, ma quelli possono essere facilmente gestiti (la trasformazione da loop a salti condizionali è banale, digita un po 'meno, ma è ancora gestibile). Ci sono altre differenze ma meno gravi.
Compilare il codice sorgente di Ruby in codice byte JVM è molto più complicato (specialmente prima che invokedynamic
e MethodHandles
siano stati introdotti in Java 7, o più precisamente nella 3a Edizione delle specifiche JVM). In Ruby, i metodi possono essere sostituiti in fase di esecuzione. Sulla JVM, la più piccola unità di codice che può essere sostituita in fase di esecuzione è una classe, quindi i metodi Ruby devono essere compilati non per i metodi JVM ma per le classi JVM. La spedizione del metodo di Ruby non corrisponde alla spedizione del metodo JVM e prima di invokedynamic
, non c'era modo di iniettare il proprio meccanismo di dispacciamento del metodo nella JVM. Ruby ha continuazioni e coroutine, ma la JVM non ha le strutture per implementarle. (Il GOTO
della JVM è limitato ai target di salto all'interno del metodo.) L'unica primitiva di flusso di controllo della JVM, che sarebbe abbastanza potente da implementare le continuazioni, sono eccezioni e implementare i thread di coroutine, entrambi estremamente pesanti, mentre il l'intero scopo delle coroutine deve essere molto leggero.
OTOH, la compilazione del codice sorgente Ruby in codice byte Rubinius o codice byte YARV è di nuovo banale, poiché entrambi sono esplicitamente progettati come destinazione di compilazione per Ruby (sebbene Rubinius sia stato utilizzato anche per altri linguaggi come CoffeeScript e notoriamente Fancy).
Allo stesso modo, la compilazione del codice nativo x86 sul codice byte JVM non è semplice, ancora una volta c'è un gap semantico piuttosto ampio.
Haskell è un altro buon esempio: con Haskell ci sono diversi compilatori pronti per la produzione di forza industriale ad alte prestazioni che producono codice macchina x86 nativo, ma a questa data non esiste alcun compilatore funzionante per la JVM o la CLI, perché il divario semantico è così grande che è molto complicato colpirlo. Quindi, questo è un esempio in cui la compilazione su codice macchina nativo è in realtà meno complessa rispetto alla compilazione in codice byte JVM o CIL. Questo perché il codice macchina nativo ha primitive di livello molto più basso ( GOTO
, puntatori, ...) che può essere più facile "forzato" a fare ciò che vuoi piuttosto che usare primitive di livello superiore come chiamate di metodo o eccezioni.
Quindi, si potrebbe dire che il livello più alto è la lingua di destinazione, più strettamente deve corrispondere alla semantica della lingua di partenza al fine di ridurre la complessità del compilatore.
In pratica, la maggior parte dei JVM oggi è un software molto complesso, che esegue la compilation JIT (quindi il bytecode è dinamicamente tradotto in codice macchina dalla JVM).
Quindi, anche se la compilazione dal codice sorgente Java (o codice sorgente Clojure) al codice byte JVM è davvero più semplice, la JVM stessa sta eseguendo una traduzione complessa su codice macchina.
Il fatto che questa traduzione JIT all'interno della JVM sia dinamica consente alla JVM di concentrarsi sulle parti più rilevanti del bytecode. In pratica, la maggior parte delle JVM ottimizzano più le parti più calde (ad esempio i metodi più chiamati o i blocchi di base più eseguiti) del codice byte JVM.
Non sono sicuro che la complessità combinata di JVM + Java nel compilatore bytecode sia significativamente inferiore alla complessità dei compilatori in anticipo.
Nota anche che i compilatori più tradizionali (come GCC o Clang / LLVM ) stanno trasformando il codice sorgente dell'ingresso C (o C ++, o Ada, ...) in una rappresentazione interna (Gimple per GCC, LLVM per Clang) che è abbastanza simile a qualche bytecode. Quindi stanno trasformando quelle rappresentazioni interne (prima ottimizzandola su se stessa, cioè la maggior parte delle passate di ottimizzazioni GCC stanno prendendo Gimple come input e producendo Gimple come output, emettendo successivamente assemblatore o codice macchina da esso) al codice oggetto.
BTW, con i recenti GCC (in particolare libgccjit ) e l'infrastruttura LLVM, potresti usarli per compilare qualche altro ( o la tua) lingua nelle loro rappresentazioni interne di Gimple o LLVM, quindi approfitta delle molte capacità di ottimizzazione del middle-end & parti di back-end di questi compilatori.
Leggi altre domande sui tag compiler bytecode machine-code