Innanzitutto, la maggior parte delle JVM include un compilatore, quindi "interpretato da un codice" è in realtà piuttosto raro (almeno nel codice di riferimento) non è così raro nella vita reale, in cui il codice è in genere più di pochi loop banali che ottengono ripetuto molto spesso).
In secondo luogo, un buon numero di benchmark coinvolti sembra essere abbastanza di parte (sia per intento o incompetenza, non posso davvero dire). Solo per esempio, anni fa ho guardato parte del codice sorgente collegato da uno dei link che hai postato. Aveva un codice come questo:
init0 = (int*)calloc(max_x,sizeof(int));
init1 = (int*)calloc(max_x,sizeof(int));
init2 = (int*)calloc(max_x,sizeof(int));
for (x=0; x<max_x; x++) {
init2[x] = 0;
init1[x] = 0;
init0[x] = 0;
}
Poiché calloc
fornisce una memoria già azzerata, utilizzando il ciclo for
a zero, di nuovo è ovviamente inutile. Questo è stato seguito (se la memoria serve) riempiendo comunque la memoria con altri dati (e nessuna dipendenza dal suo azzeramento), quindi tutto l'azzeramento era del tutto inutile comunque. Sostituendo il codice precedente con un semplice malloc
(come qualsiasi persona sana di mente avrebbe usato per iniziare) ha migliorato la velocità della versione C ++ abbastanza da battere la versione Java (con un margine abbastanza ampio, se la memoria serve).
Considera (per un altro esempio) il benchmark methcall
utilizzato nella post di blog nel tuo ultimo link. Nonostante il nome (e il modo in cui le cose potrebbero sembrare), la versione C ++ di questo è non che misura davvero molto circa l'overhead di chiamata del metodo. La parte del codice che risulta critica è nella classe Toggle:
class Toggle {
public:
Toggle(bool start_state) : state(start_state) { }
virtual ~Toggle() { }
bool value() {
return(state);
}
virtual Toggle& activate() {
state = !state;
return(*this);
}
bool state;
};
La parte critica risulta essere state = !state;
. Considera cosa succede quando cambiamo il codice per codificare lo stato come int
invece di bool
:
class Toggle {
enum names{ bfalse = -1, btrue = 1};
const static names values[2];
int state;
public:
Toggle(bool start_state) : state(values[start_state])
{ }
virtual ~Toggle() { }
bool value() { return state==btrue; }
virtual Toggle& activate() {
state = -state;
return(*this);
}
};
Questa piccola modifica migliora la velocità complessiva di circa margine di 5: 1 . Anche se il parametro di riferimento era inteso per misurare il tempo di chiamata del metodo, in realtà la maggior parte di ciò che stava misurando era il tempo di convertire tra int
e bool
. Sarei certamente d'accordo sul fatto che l'inefficienza mostrata dall'originale sia sfortunata - ma vista la rarità che sembra sorgere nel codice reale e la facilità con cui può essere riparata quando / se si presenta, ho difficoltà a pensare ne significa molto.
Nel caso qualcuno decidesse di rieseguire i benchmark coinvolti, dovrei anche aggiungere che c'è una modifica quasi ugualmente banale alla versione di Java che produce (o almeno una volta prodotta - Non ho ri-eseguito prova con una JVM recente per confermare che fanno ancora) un miglioramento abbastanza sostanziale anche nella versione Java. La versione Java ha un NthToggle :: activate () che assomiglia a questo:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Cambiare questo per chiamare la funzione base invece di manipolare this.state
dà direttamente un notevole miglioramento della velocità (sebbene non sia sufficiente per tenere il passo con la versione modificata del C ++).
Quindi, quello che finiamo è una falsa ipotesi sui codici byte interpretati rispetto ad alcuni dei peggiori benchmark (che io abbia mai visto). Né sta dando un risultato significativo.
La mia esperienza personale è che con programmatori altrettanto esperti che prestano uguale attenzione all'ottimizzazione, il C ++ batterà Java molto spesso - ma (almeno tra questi due), il linguaggio raramente farà la stessa differenza dei programmatori e del design . I parametri di riferimento citati ci dicono di più sulla (in) competenza / (dis) onestà dei loro autori di quanto non facciano riguardo alle lingue che intendono indicare.
[Modifica: come implicito in un punto sopra ma mai affermato direttamente come probabilmente dovrei avere, i risultati che sto citando sono quelli che ho ottenuto quando ho testato questo ~ 5 anni fa, usando le implementazioni C ++ e Java che erano attuali a quel tempo. Non ho eseguito nuovamente i test con le implementazioni correnti. Uno sguardo, tuttavia, indica che il codice non è stato corretto, quindi tutto ciò che sarebbe cambiato sarebbe stata la capacità del compilatore di nascondere i problemi nel codice.]
Se ignoriamo gli esempi Java, tuttavia, è effettivamente possibile che il codice interpretato funzioni più velocemente del codice compilato (sebbene sia difficile e alquanto insolito).
Il solito modo in cui ciò accade è che il codice che viene interpretato è molto più compatto del codice macchina, o è in esecuzione su una CPU che ha una cache di dati più grande della cache del codice.
In tal caso, un piccolo interprete (ad esempio, l'interprete interno di un'implementazione Forth) potrebbe essere in grado di adattarsi interamente alla cache del codice e il programma dell'interpretazione si adatta perfettamente alla cache dei dati. La cache è in genere più veloce della memoria principale di un fattore di almeno 10 e spesso molto più (un fattore di 100 non è particolarmente raro).
Quindi, se la cache è più veloce della memoria principale di un fattore N e richiede meno di N istruzioni per codice macchina per implementare ogni codice byte, il codice byte dovrebbe vincere (sto semplificando, ma penso che il generale l'idea dovrebbe essere ancora evidente)