Innanzitutto, la maggior parte delle JVM include un compilatore, quindi "interpretato dal bytecode" è in realtà piuttosto raro (almeno nel codice di riferimento - non è così raro nella vita reale, in cui il tuo codice è di solito più di alcuni loop banali che si ripetono molto spesso ).
In secondo luogo, un discreto numero dei parametri di riferimento coinvolti sembra essere piuttosto parziale (sia per intento che per incompetenza, non posso davvero dirlo). Solo per esempio, anni fa ho esaminato alcuni dei codici sorgente collegati da uno dei collegamenti che hai pubblicato. 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;
}
Dato che calloc
fornisce memoria che è già azzerata, usare il for
loop per azzerarlo di nuovo è ovviamente inutile. Questo è stato seguito (se la memoria serve) riempiendo comunque la memoria di altri dati (e nessuna dipendenza dal fatto che fosse azzerato), quindi tutto l'azzeramento era completamente inutile comunque. Sostituire il codice sopra 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 methcall
benchmark utilizzato nel post di blog nel tuo ultimo link. Nonostante il nome (e il modo in cui le cose potrebbero persino apparire), la versione C ++ di questo non sta davvero misurando molto sull'overhead delle chiamate di metodo. La parte del codice che risulta essere 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 la state = !state;
. Considera cosa succede quando cambiamo il codice per codificare lo stato come un int
anziché come 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 un margine di 5: 1 . Anche se il benchmark era destinato a misurare il tempo di chiamata del metodo, in realtà gran parte di ciò che stava misurando era il tempo di conversione tra int
e bool
. Concordo certamente sul fatto che l'inefficienza mostrata dall'originale sia sfortunata, ma dato quanto raramente sembra sorgere nel codice reale e la facilità con cui può essere risolto quando / se si presenta, ho difficoltà a pensare ne significa molto.
Nel caso in cui qualcuno decida di rieseguire i benchmark coinvolti, dovrei anche aggiungere che c'è una modifica quasi altrettanto banale alla versione Java che produce (o almeno una volta prodotta - Non ho rieseguito i test con un recente JVM per confermare che lo fanno ancora) un miglioramento abbastanza sostanziale anche nella versione Java. La versione Java ha un NthToggle :: activ () 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);
}
Modificarlo per chiamare la funzione base invece di manipolarlo this.state
direttamente fornisce un sostanziale miglioramento della velocità (anche se non abbastanza per stare al passo con la versione C ++ modificata).
Quindi, ciò con cui finiamo è una falsa ipotesi sui codici byte interpretati rispetto ad alcuni dei peggiori benchmark (che abbia mai visto). Nessuno dei due sta dando un risultato significativo.
La mia esperienza personale è che con programmatori altrettanto esperti che prestano uguale attenzione all'ottimizzazione, C ++ batterà Java più spesso, ma (almeno tra questi due), il linguaggio raramente farà la stessa differenza dei programmatori e del design. I benchmark citati ci dicono di più sulla (in) competenza / (dis) onestà dei loro autori di quanto non facciano sulle lingue che pretendono di benchmark.
[Modifica: Come suggerito in un punto sopra ma mai dichiarato così direttamente come probabilmente avrei dovuto, i risultati che sto citando sono quelli che ho ottenuto quando ho provato questo ~ 5 anni fa, usando implementazioni C ++ e Java che erano attuali a quel tempo . Non ho rieseguito i test con le attuali implementazioni. Uno sguardo, tuttavia, indica che il codice non è stato corretto, quindi tutto ciò che sarebbe cambiato sarebbe la capacità del compilatore di coprire i problemi nel codice.]
Se ignoriamo gli esempi Java, tuttavia, è effettivamente possibile che il codice interpretato venga eseguito più velocemente del codice compilato (sebbene difficile e in qualche modo 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 di 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 che sta interpretando si adatta interamente alla cache dei dati. La cache è in genere più veloce della memoria principale di un fattore di almeno 10 e spesso molto di più (un fattore di 100 non è più particolarmente raro).
Quindi, se la cache è più veloce della memoria principale di un fattore N e ci vogliono meno di N istruzioni sul codice macchina per implementare ogni codice byte, il codice byte dovrebbe vincere (sto semplificando, ma penso che l'idea generale dovrebbe ancora essere evidente).