Risposte:
Sì, la compilazione in bytecode Java è più semplice della compilazione in codice macchina. Ciò è in parte dovuto al fatto che esiste solo un formato di destinazione (come menzionato Mandrill, sebbene ciò riduca solo la complessità del compilatore, non il tempo di compilazione), in parte perché JVM è una macchina molto più semplice e più comoda da programmare rispetto alle CPU reali, poiché è stata progettata in in tandem con il linguaggio Java, la maggior parte delle operazioni Java si associa esattamente a un'operazione bytecode in un modo molto semplice. Un altro motivo molto importante è che praticamente nol'ottimizzazione ha luogo. Quasi tutti i problemi di efficienza sono lasciati al compilatore JIT (o alla JVM nel suo insieme), quindi l'intera parte centrale dei compilatori normali scompare. Fondamentalmente può attraversare l'AST una volta e generare sequenze bytecode già pronte per ciascun nodo. Esiste un "sovraccarico amministrativo" di generazione di tabelle dei metodi, pool costanti, ecc., Ma questo non è nulla rispetto alle complessità, per esempio, di LLVM.
Un compilatore è semplicemente un programma che prende 1 file di testo 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à è all'incirca la stessa. Tuttavia, a un livello più pratico, i compilatori di codici byte sono più semplici.
Quali passi fondamentali devono accadere per compilare un programma?
Ci sono solo due differenze reali 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 il problema se il collegamento sia 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 molte preoccupazioni relative al collegamento vengono gestite dalla VM (vedere la mia nota di seguito).
I compilatori di codici byte tendono a non ottimizzare tanto perché la VM può farlo meglio al volo (i compilatori JIT sono un'aggiunta abbastanza standard alle VM al giorno d'oggi).
Da ciò concludo che i compilatori di codici byte possono omettere la complessità della maggior parte delle ottimizzazioni e di tutti i collegamenti, rinviando entrambi al runtime della VM. I compilatori di codici byte sono in pratica più semplici perché pongono sulla VM molte complessità che i compilatori di codici macchina si assumono da soli.
1 Senza contare le lingue esoteriche
Direi che semplifica la progettazione del compilatore poiché la compilazione è sempre da Java a codice di macchina virtuale generica. Ciò significa anche che è necessario compilare il codice una sola volta e verrà eseguito su qualsiasi piattaforma (anziché dover compilare su ogni macchina). Non sono sicuro che il tempo di compilazione sarà inferiore perché puoi considerare la macchina virtuale proprio come una macchina standardizzata.
D'altra parte, ogni macchina dovrà avere Java Virtual Machine caricata in modo che possa interpretare il "codice byte" (che è il codice della macchina virtuale derivato dalla compilazione del codice java), tradurlo nel codice macchina effettivo ed eseguirlo .
Imo questo va bene per programmi molto grandi ma molto male 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 origine e la lingua di destinazione e dal livello di ottimizzazione che si desidera applicare mentre si colma questo divario.
Ad esempio, la compilazione del codice sorgente Java nel codice byte JVM è relativamente semplice, poiché esiste un sottoinsieme principale di Java che si associa praticamente direttamente a un sottoinsieme del codice byte JVM. Ci sono alcune differenze: Java ha i loop ma no GOTO
, la JVM ha GOTO
ma nessun loop, Java ha i generici, la JVM no, ma quelli possono essere facilmente affrontati (la trasformazione da loop a salti condizionali è banale, il tipo di cancellazione è leggermente inferiore così, ma ancora gestibile). Ci sono altre differenze ma meno gravi.
Compilare il codice sorgente Ruby in codice byte JVM è molto più coinvolto (specialmente prima invokedynamic
e MethodHandles
sono 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 in metodi JVM ma in classi JVM. L'invio del metodo Ruby non corrisponde all'invio del metodo JVM e prima invokedynamic
, non c'era modo di iniettare il proprio meccanismo di invio del metodo nella JVM. Ruby ha continuazioni e coroutine, ma alla JVM mancano le strutture per implementarle. (Le JVMGOTO
è limitato a saltare obiettivi all'interno del metodo.) L'unico flusso di controllo di base della JVM, che sarebbe abbastanza potente da implementare continuazioni sono le eccezioni e implementare fili di coroutine, entrambi estremamente pesanti, mentre lo scopo delle coroutine è quello di essere molto leggero.
OTOH, compilare il codice sorgente di Ruby in codice byte Rubinius o codice byte YARV è di nuovo banale, dal momento che entrambi sono esplicitamente progettati come target di compilazione per Ruby (anche se Rubinius è stato utilizzato anche per altri linguaggi come CoffeeScript e, più famoso, Fancy) .
Allo stesso modo, la compilazione del codice nativo x86 in codice byte JVM non è semplice, di nuovo, c'è un gap semantico piuttosto grande.
Haskell è un altro buon esempio: con Haskell, ci sono molti compilatori pronti per la produzione ad alte prestazioni e forza industriale che producono codice macchina x86 nativo, ma fino ad oggi non esiste un compilatore funzionante né per la JVM né per la CLI, perché la semantica il divario è così grande che è molto complesso colmare il divario. Quindi, questo è un esempio in cui la compilazione in codice macchina nativo è in realtà meno complessa della compilazione in codice byte JVM o CIL. Questo perché il codice macchina nativo ha primitive di livello molto inferiore ( GOTO
, puntatori, ...) che possono essere più "forzate" a fare ciò che si desidera rispetto all'utilizzo di primitive di livello superiore come chiamate di metodo o eccezioni.
Quindi, si potrebbe dire che più è alta la lingua di destinazione, più deve corrispondere alla semantica della lingua di origine al fine di ridurre la complessità del compilatore.
In pratica, la maggior parte dei JVM oggi sono software molto complessi, che fanno compilation JIT (quindi il bytecode viene tradotto dinamicamente in codice macchina da JVM).
Quindi, mentre la compilazione dal codice sorgente Java (o codice sorgente Clojure) al codice byte JVM è davvero più semplice, la stessa JVM sta facendo una traduzione complessa in 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 ottimizza maggiormente le parti più calde (ad esempio i metodi più chiamati o i blocchi di base più eseguiti) del bytecode JVM.
Non sono sicuro che la complessità combinata di JVM + Java per il compilatore bytecode sia significativamente inferiore alla complessità dei compilatori anticipati.
Si noti inoltre che i compilatori più tradizionali (come GCC o Clang / LLVM ) stanno trasformando il codice sorgente di input C (o C ++ o Ada, ...) in una rappresentazione interna ( Gimple per GCC, LLVM per Clang) che è abbastanza simile a alcuni bytecode. Quindi stanno trasformando quelle rappresentazioni interne (prima ottimizzandolo in se stesso, cioè la maggior parte dei passaggi di ottimizzazione GCC stanno prendendo Gimple come input e producendo Gimple come output; in seguito emettendo assemblatore o codice macchina da esso) a codice oggetto.
A proposito, con la recente GCC (in particolare libgccjit ) e l'infrastruttura LLVM, potresti usarli per compilare un'altra lingua (o la tua) lingua nelle loro rappresentazioni interne Gimple o LLVM, quindi trarre profitto dalle molte capacità di ottimizzazione di fascia media e back- parti finali di questi compilatori.