Erstens enthalten die meisten JVMs einen Compiler, so dass "interpretierter Bytecode" eigentlich ziemlich selten ist (zumindest im Benchmark-Code - es ist nicht ganz so selten im wirklichen Leben, wo Ihr Code normalerweise mehr als ein paar unbedeutende Schleifen enthält, die extrem oft wiederholt werden ).
Zweitens scheint eine ganze Reihe von Benchmarks ziemlich voreingenommen zu sein (ob absichtlich oder inkompetent, kann ich nicht wirklich sagen). Zum Beispiel habe ich mir vor Jahren einen Teil des Quellcodes angesehen, der über einen der von Ihnen geposteten Links verlinkt wurde. Es hatte Code wie folgt:
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;
}
Da calloc
der Speicher bereits auf Null gesetzt ist, ist die Verwendung der for
Schleife auf Null offensichtlich unbrauchbar. Daraufhin wurde der Speicher (sofern der Speicher belegt ist) ohnehin mit anderen Daten gefüllt (und es bestand keine Abhängigkeit davon, dass sie auf Null gesetzt wurden), sodass das gesamte Nullsetzen ohnehin völlig unnötig war. Das Ersetzen des obigen Codes durch einen einfachen Code malloc
(wie es jede vernünftige Person zu Beginn getan hätte) verbesserte die Geschwindigkeit der C ++ - Version so weit, dass sie die Java-Version übertraf (bei ausreichendem Speicherplatz).
Betrachten Sie (für ein anderes Beispiel) den methcall
Benchmark, der im Blogeintrag in Ihrem letzten Link verwendet wurde. Ungeachtet des Namens (und wie die Dinge vielleicht sogar aussehen) ist die C ++ - Version davon überhaupt nicht sehr an Methodenaufruf-Overhead interessiert. Der Teil des Codes, der sich als kritisch herausstellt, befindet sich in der Toggle-Klasse:
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;
};
Der kritische Teil ist der state = !state;
. Überlegen Sie, was passiert, wenn wir den Code ändern, um den Status als int
statt als zu codieren 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);
}
};
Diese geringfügige Änderung verbessert die Gesamtgeschwindigkeit um etwa 5: 1 . Obwohl der Benchmark die Methodenaufrufzeit messen sollte, war der größte Teil der Zeit, die gemessen wurde, die Zeit, zwischen int
und umzurechnen bool
. Ich würde mit Sicherheit zustimmen, dass die Ineffizienz, die das Original zeigt, bedauerlich ist - aber angesichts der Tatsache, wie selten es in echtem Code vorkommt und mit welcher Leichtigkeit es behoben werden kann, wenn / wenn es auftritt, fällt es mir schwer, nachzudenken davon als viel zu bedeuten.
Falls sich jemand entscheidet, die betroffenen Benchmarks erneut auszuführen, sollte ich auch hinzufügen, dass die Java-Version, die produziert wird (oder zumindest einmal produziert wurde - ich habe die Tests mit a nicht erneut ausgeführt), eine fast ebenso triviale Änderung aufweist Jüngste JVMs bestätigen, dass sie dies immer noch tun. Auch in der Java-Version ist dies eine ziemlich wesentliche Verbesserung. Die Java-Version hat ein NthToggle :: activate (), das so aussieht:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Wenn Sie dies ändern, um die Basisfunktion aufzurufen, anstatt this.state
direkt zu manipulieren, wird die Geschwindigkeit erheblich verbessert (obwohl dies nicht ausreicht, um mit der modifizierten C ++ - Version Schritt zu halten).
Also, was wir am Ende haben, ist eine falsche Annahme über interpretierte Bytecodes im Vergleich zu einigen der schlechtesten Benchmarks (die ich je gesehen habe). Weder gibt ein aussagekräftiges Ergebnis.
Meine eigene Erfahrung ist, dass C ++ mit ebenso erfahrenen Programmierern, die gleichermaßen auf die Optimierung achten, Java häufiger schlagen wird als nicht - aber (zumindest zwischen diesen beiden) wird die Sprache selten so viel Unterschied machen wie die Programmierer und das Design. Die angeführten Benchmarks sagen mehr über die (In-) Kompetenz / (Dis-) Ehrlichkeit ihrer Autoren aus als über die Sprachen, die sie als Benchmark angeben.
[Bearbeiten: Wie oben an einer Stelle angedeutet, aber nie so direkt angegeben, wie ich es wahrscheinlich hätte tun sollen, sind die Ergebnisse, die ich erhalten habe, als ich dies vor ~ 5 Jahren mit C ++ - und Java-Implementierungen getestet habe, die zu dieser Zeit aktuell waren . Ich habe die Tests mit den aktuellen Implementierungen nicht wiederholt. Ein Blick zeigt jedoch, dass der Code noch nicht repariert wurde. Alles, was sich geändert hätte, wäre die Fähigkeit des Compilers, die Probleme im Code zu vertuschen.]
Wenn wir die Java Beispiele ignorieren, aber es ist tatsächlich möglich , interpretierten Code , schneller zu laufen als kompilierten Code (wenn auch schwierig und etwas ungewöhnlich).
In der Regel ist der zu interpretierende Code viel kompakter als der Maschinencode, oder er wird auf einer CPU ausgeführt, die einen größeren Daten-Cache als der Code-Cache hat.
In einem solchen Fall kann ein kleiner Interpreter (z. B. der innere Interpreter einer Forth-Implementierung) vollständig in den Code-Cache passen, und das Programm, das er interpretiert, passt vollständig in den Daten-Cache. Der Cache ist in der Regel mindestens um den Faktor 10 schneller als der Hauptspeicher und häufig um ein Vielfaches schneller (ein Faktor 100 ist nicht mehr besonders selten).
Wenn der Cache also um den Faktor N schneller als der Hauptspeicher ist und weniger als N Maschinencodeanweisungen erforderlich sind, um jeden Bytecode zu implementieren, sollte der Bytecode gewinnen (ich vereinfache, aber ich denke, die allgemeine Idee sollte es trotzdem sein offensichtlich sein).