Sembra che ci siano almeno due diverse possibili domande qui. Uno riguarda davvero i compilatori in generale, con Java praticamente solo un esempio del genere. L'altro è più specifico per Java dei codici byte specifici che utilizza.
Compilatori in generale
Consideriamo innanzitutto la domanda generale: perché un compilatore dovrebbe utilizzare una rappresentazione intermedia nel processo di compilazione del codice sorgente per eseguire un determinato processore?
Riduzione della complessità
Una risposta è abbastanza semplice: converte un problema O (N * M) in un problema O (N + M).
Se ci vengono dati N lingue di origine e target M e ogni compilatore è completamente indipendente, allora abbiamo bisogno di compilatori N * M per tradurre tutte quelle lingue di origine in tutti quei target (dove un "target" è qualcosa come una combinazione di un processore e sistema operativo).
Se, tuttavia, tutti quei compilatori concordano su una rappresentazione intermedia comune, allora possiamo avere N front-end del compilatore che traducono le lingue di origine nella rappresentazione intermedia, e back-end del compilatore M che traducono la rappresentazione intermedia in qualcosa di adatto per un obiettivo specifico.
Segmentazione del problema
Meglio ancora, separa il problema in due domini più o meno esclusivi. Le persone che conoscono / si preoccupano della progettazione del linguaggio, dell'analisi e cose del genere possono concentrarsi sui front-end del compilatore, mentre le persone che conoscono i set di istruzioni, la progettazione del processore e cose del genere possono concentrarsi sul back-end.
Quindi, ad esempio, dato qualcosa come LLVM, abbiamo molti front-end per varie lingue diverse. Abbiamo anche back-end per molti processori diversi. Un ragazzo di lingua può scrivere un nuovo front-end per la sua lingua e supportare rapidamente molti obiettivi. Un ragazzo del processore può scrivere un nuovo back-end per il suo target senza occuparsi di design del linguaggio, analisi, ecc.
Separare i compilatori in un front-end e un back-end, con una rappresentazione intermedia per comunicare tra i due non è originale con Java. È stata una pratica abbastanza comune per molto tempo (dal momento che molto prima che arrivasse Java, comunque).
Modelli di distribuzione
Nella misura in cui Java ha aggiunto qualcosa di nuovo a questo proposito, era nel modello di distribuzione. In particolare, anche se i compilatori sono stati separati internamente in pezzi front-end e back-end per lungo tempo, in genere sono stati distribuiti come un singolo prodotto. Ad esempio, se hai acquistato un compilatore Microsoft C, internamente aveva un "C1" e un "C2", che erano rispettivamente il front-end e il back-end - ma quello che hai acquistato era solo "Microsoft C" che includeva entrambi pezzi (con un "driver del compilatore" che ha coordinato le operazioni tra i due). Anche se il compilatore è stato creato in due parti, per un normale sviluppatore che utilizzava il compilatore era solo una cosa che traduceva da codice sorgente a codice oggetto, con nulla di visibile in mezzo.
Java, invece, ha distribuito il front-end nel Java Development Kit e il back-end nella Java Virtual Machine. Ogni utente Java aveva un back-end del compilatore per indirizzare qualunque sistema stesse stava usando. Gli sviluppatori Java hanno distribuito il codice nel formato intermedio, quindi quando un utente lo ha caricato, JVM ha fatto tutto il necessario per eseguirlo sul proprio computer.
precedenti
Si noti che anche questo modello di distribuzione non era del tutto nuovo. Ad esempio, il sistema P UCSD ha funzionato in modo simile: i front-end del compilatore producevano il codice P e ogni copia del sistema P includeva una macchina virtuale che faceva il necessario per eseguire il codice P su quel particolare target 1 .
Codice byte Java
Il codice byte Java è abbastanza simile al codice P. Fondamentalmente sono le istruzioni per una macchina abbastanza semplice. Quella macchina vuole essere un'astrazione delle macchine esistenti, quindi è abbastanza facile da tradurre rapidamente in quasi tutti gli obiettivi specifici. La facilità di traduzione era importante sin dall'inizio perché l'intento originale era quello di interpretare i codici byte, proprio come aveva fatto P-System (e, sì, è esattamente così che funzionavano le prime implementazioni).
Punti di forza
Il codice byte Java è facile da produrre per un front-end del compilatore. Se (ad esempio) hai un albero abbastanza tipico che rappresenta un'espressione, in genere è abbastanza facile attraversare l'albero e generare codice abbastanza direttamente da ciò che trovi in ciascun nodo.
I codici byte Java sono piuttosto compatti - nella maggior parte dei casi, molto più compatti del codice sorgente o del codice macchina per i processori più tipici (e, soprattutto per la maggior parte dei processori RISC, come lo SPARC che Sun ha venduto quando hanno progettato Java). Ciò era particolarmente importante in quel momento, perché uno dei principali intenti di Java era supportare applet - codice incorporato in pagine Web che sarebbero state scaricate prima dell'esecuzione - in un momento in cui la maggior parte delle persone accedeva a noi tramite modem tramite linee telefoniche a circa 28,8 kilobit al secondo (anche se, naturalmente, c'erano ancora alcune persone che usano modem più vecchi e più lenti).
Punti di debolezza
Il principale punto debole dei codici byte Java è che non sono particolarmente espressivi. Sebbene possano esprimere abbastanza bene i concetti presenti in Java, non funzionano altrettanto bene per esprimere concetti che non fanno parte di Java. Allo stesso modo, sebbene sia facile eseguire codici byte sulla maggior parte dei computer, è molto più difficile farlo in un modo che sfrutti appieno ogni particolare computer.
Ad esempio, è abbastanza normale che se si desidera davvero ottimizzare i codici byte Java, fondamentalmente si fa un po 'di reverse engineering per tradurli all'indietro da una rappresentazione simile a un codice macchina e trasformarli in istruzioni SSA (o qualcosa di simile) 2 . Quindi manipoli le istruzioni SSA per eseguire l'ottimizzazione, quindi traduci da lì in qualcosa che si rivolge all'architettura a cui tieni davvero. Anche con questo processo piuttosto complesso, tuttavia, alcuni concetti che sono estranei a Java sono sufficientemente difficili da esprimere che è difficile tradurre da alcune lingue di origine in codice macchina che viene eseguito (anche vicino) in modo ottimale sulla maggior parte delle macchine tipiche.
Sommario
Se ti stai chiedendo perché utilizzare le rappresentazioni intermedie in generale, due fattori principali sono:
- Ridurre un problema O (N * M) a un problema O (N + M) e
- Rompere il problema in pezzi più gestibili.
Se stai chiedendo quali siano i dettagli dei codici byte Java e perché abbiano scelto questa particolare rappresentazione invece di qualche altra, allora direi che la risposta in gran parte ritorna al loro intento originale e ai limiti del web in quel momento , portando alle seguenti priorità:
- Rappresentazione compatta.
- Facile e veloce da decodificare ed eseguire.
- Veloce e facile da implementare sulle macchine più comuni.
Essere in grado di rappresentare molte lingue o eseguire in modo ottimale su una vasta gamma di obiettivi erano priorità molto più basse (se fossero considerate priorità).
- Quindi perché il sistema P è quasi completamente dimenticato? Principalmente una situazione di prezzi. Il sistema P è stato venduto abbastanza decentemente su Apple II, Commodore SuperPet, ecc. Quando è uscito il PC IBM, il sistema P era un sistema operativo supportato, ma MS-DOS costava meno (dal punto di vista della maggior parte delle persone, era essenzialmente gettato gratuitamente) e rapidamente ha avuto più programmi disponibili, poiché è quello per cui Microsoft e IBM (tra gli altri) hanno scritto.
- Ad esempio, ecco come funziona la fuliggine .