Tenere presente che quanto segue sta solo confrontando la differenza tra la compilazione nativa e JIT e non copre le specifiche di alcun linguaggio o framework specifici. Potrebbero esserci motivi legittimi per scegliere una piattaforma particolare oltre a questa.
Quando affermiamo che il codice nativo è più veloce, stiamo parlando del tipico caso d' uso del codice compilato in modo nativo rispetto al codice compilato JIT, in cui l'uso tipico di un'applicazione compilata JIT deve essere eseguito dall'utente, con risultati immediati (ad esempio, no prima in attesa sul compilatore). In tal caso, non credo che nessuno possa affermare con una faccia seria che il codice compilato JIT può corrispondere o battere il codice nativo.
Supponiamo di avere un programma scritto in un linguaggio X, e possiamo compilarlo con un compilatore nativo e di nuovo con un compilatore JIT. Ogni flusso di lavoro ha le stesse fasi coinvolte, che possono essere generalizzate come (Codice -> Rappresentanza intermedia -> Codice macchina -> Esecuzione). La grande differenza tra due è quali fasi sono visualizzate dall'utente e quali sono viste dal programmatore. Con la compilazione nativa, il programmatore vede tutto tranne la fase di esecuzione, ma con la soluzione JIT, la compilazione in codice macchina è vista dall'utente, oltre all'esecuzione.
L'affermazione che A è più veloce di B si riferisce al tempo impiegato per l'esecuzione del programma, come visto dall'utente . Se assumiamo che entrambi i pezzi di codice funzionino in modo identico nella fase di esecuzione, dobbiamo supporre che il flusso di lavoro JIT sia più lento per l'utente, poiché deve anche vedere il tempo T della compilazione rispetto al codice macchina, dove T> 0. Quindi , affinché qualsiasi possibilità che il flusso di lavoro JIT esegua lo stesso del flusso di lavoro nativo, per l'utente, è necessario ridurre i tempi di esecuzione del codice, in modo tale che Esecuzione + Compilazione al codice macchina siano inferiori alla sola fase di esecuzione del flusso di lavoro nativo. Ciò significa che dobbiamo ottimizzare il codice meglio nella compilazione JIT che nella compilazione nativa.
Ciò, tuttavia, è piuttosto impossibile, poiché per eseguire le ottimizzazioni necessarie per accelerare l'esecuzione, dobbiamo dedicare più tempo nella fase di compilazione per la fase del codice macchina e, quindi, ogni volta che risparmiamo a causa del codice ottimizzato viene effettivamente perso, poiché lo aggiungiamo alla compilation. In altre parole, la "lentezza" di una soluzione basata su JIT non è semplicemente dovuta al tempo aggiunto per la compilazione JIT, ma il codice prodotto da quella compilazione esegue più lentamente di una soluzione nativa.
Userò un esempio: registro allocazione. Poiché l'accesso alla memoria è migliaia di volte più lento dell'accesso ai registri, idealmente vogliamo usare i registri laddove possibile e avere il minor numero di accessi di memoria possibile, ma abbiamo un numero limitato di registri e dobbiamo trasferire lo stato in memoria quando ne abbiamo bisogno un registro. Se utilizziamo un algoritmo di allocazione dei registri che richiede 200 ms per il calcolo e, di conseguenza, risparmiamo 2 ms di tempo di esecuzione - non stiamo sfruttando al meglio il tempo per un compilatore JIT. Soluzioni come l'algoritmo di Chaitin, che può produrre codice altamente ottimizzato, non sono adatte.
Il ruolo del compilatore JIT è quello di trovare il miglior equilibrio tra tempo di compilazione e qualità del codice prodotto, tuttavia, con una grande propensione per i tempi di compilazione rapidi, poiché non si desidera lasciare l'utente in attesa. Le prestazioni del codice in esecuzione sono più lente nel caso JIT, poiché il compilatore nativo non è vincolato (molto) dal tempo nell'ottimizzazione del codice, quindi è libero di usare i migliori algoritmi. La possibilità che la compilazione + esecuzione complessiva per un compilatore JIT possa battere solo il tempo di esecuzione per il codice compilato in modo nativo è effettivamente 0.
Ma le nostre VM non si limitano solo alla compilazione JIT. Impiegano tecniche di compilazione anticipate, memorizzazione nella cache, hot swap e ottimizzazioni adattive. Quindi modifichiamo la nostra affermazione che le prestazioni sono ciò che l'utente vede e limitiamole al tempo impiegato per l'esecuzione del programma (supponiamo che abbiamo compilato AOT). Possiamo effettivamente rendere il codice di esecuzione equivalente al compilatore nativo (o forse meglio?). Una grande richiesta per le macchine virtuali è che potrebbero essere in grado di produrre un codice di qualità migliore rispetto a un compilatore nativo, perché ha accesso a più informazioni - quella del processo in esecuzione, come la frequenza con cui una determinata funzione può essere eseguita. La VM può quindi applicare ottimizzazioni adattive al codice più essenziale tramite hot swap.
Tuttavia, c'è un problema con questo argomento: si presume che l'ottimizzazione guidata dal profilo e simili sia qualcosa di unico per le macchine virtuali, il che non è vero. Possiamo applicarlo anche alla compilazione nativa - compilando la nostra applicazione con la profilazione abilitata, registrando le informazioni e quindi ricompilando l'applicazione con quel profilo. Probabilmente vale anche la pena sottolineare che lo scambio di codice non è qualcosa che solo un compilatore JIT può fare, possiamo farlo per il codice nativo, anche se le soluzioni basate su JIT per farlo sono più prontamente disponibili e molto più facili per lo sviluppatore. Quindi la grande domanda è: può una VM fornirci alcune informazioni che la compilazione nativa non può, il che può migliorare le prestazioni del nostro codice?
Non riesco a vederlo da solo. Possiamo applicare la maggior parte delle tecniche di una tipica VM anche al codice nativo, sebbene il processo sia maggiormente coinvolto. Allo stesso modo, possiamo applicare eventuali ottimizzazioni di un compilatore nativo a una macchina virtuale che utilizza la compilazione AOT o le ottimizzazioni adattative. La realtà è che la differenza tra il codice eseguito in modo nativo e quello eseguito in una VM non è così grande come ci è stato fatto credere. Alla fine portano allo stesso risultato, ma adottano un approccio diverso per arrivarci. La VM utilizza un approccio iterativo per produrre codice ottimizzato, in cui il compilatore nativo lo prevede dall'inizio (e può essere migliorato con un approccio iterativo).
Un programmatore C ++ potrebbe obiettare che ha bisogno delle ottimizzazioni fin dall'inizio, e non dovrebbe essere in attesa di una macchina virtuale per capire come eseguirle, se non del tutto. Questo è probabilmente un punto valido con la nostra attuale tecnologia, poiché l'attuale livello di ottimizzazione nelle nostre macchine virtuali è inferiore a quello che i compilatori nativi possono offrire, ma ciò potrebbe non essere sempre il caso se le soluzioni AOT nelle nostre macchine virtuali migliorano, ecc.