Primero, la mayoría de las JVM incluyen un compilador, por lo que "bytecode interpretado" es bastante raro (al menos en el código de referencia, no es tan raro en la vida real, donde el código suele ser más que unos pocos bucles triviales que se repiten con mucha frecuencia )
En segundo lugar, un buen número de los puntos de referencia involucrados parecen estar bastante sesgados (ya sea por intención o incompetencia, realmente no puedo decir). Solo por ejemplo, hace años, miré algunos de los códigos fuente vinculados desde uno de los enlaces que publicaste. Tenía un código como este:
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;
}
Como calloc
proporciona memoria que ya está puesta a cero, usar el for
bucle para ponerlo a cero nuevamente es obviamente inútil. Esto fue seguido (si la memoria sirve) llenando la memoria con otros datos de todos modos (y sin depender de que se ponga a cero), por lo que toda la puesta a cero fue completamente innecesaria de todos modos. Reemplazar el código anterior con un simple malloc
(como cualquier persona sensata hubiera usado para comenzar) mejoró la velocidad de la versión de C ++ lo suficiente como para vencer a la versión de Java (por un margen bastante amplio, si la memoria sirve).
Considere (para otro ejemplo) el methcall
punto de referencia utilizado en la entrada del blog en su último enlace. A pesar del nombre (y de cómo podrían verse las cosas), la versión C ++ de esto no mide mucho sobre la sobrecarga de llamadas al método. La parte del código que resulta ser crítica está en la clase 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 crítica resulta ser la state = !state;
. Considere lo que sucede cuando cambiamos el código para codificar el estado como un en int
lugar de un 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);
}
};
Este cambio menor mejora la velocidad general en aproximadamente un margen de 5: 1 . Aunque el punto de referencia estaba destinado a medir el tiempo de llamada al método, en realidad, la mayor parte de lo que medía era el tiempo de conversión entre int
y bool
. Ciertamente estoy de acuerdo en que la ineficiencia mostrada por el original es desafortunada, pero dado que rara vez parece surgir en el código real, y la facilidad con la que se puede solucionar cuando / si surge, me cuesta pensar de ello como mucho significado.
En caso de que alguien decida volver a ejecutar los puntos de referencia involucrados, también debo agregar que hay una modificación casi igualmente trivial a la versión de Java que produce (o al menos una vez producido; no he vuelto a ejecutar las pruebas con un JVM reciente para confirmar que todavía lo hacen) una mejora bastante sustancial en la versión de Java también. La versión de Java tiene un NthToggle :: enable () que se ve así:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Cambiar esto para llamar a la función base en lugar de manipular this.state
directamente proporciona una mejora de velocidad bastante sustancial (aunque no lo suficiente como para mantenerse al día con la versión modificada de C ++).
Entonces, lo que terminamos es una suposición falsa sobre los códigos de bytes interpretados frente a algunos de los peores puntos de referencia que he visto. Tampoco está dando un resultado significativo.
Mi propia experiencia es que con programadores igualmente experimentados que prestan igual atención a la optimización, C ++ vencerá a Java más a menudo que no, pero (al menos entre estos dos), el lenguaje rara vez hará tanta diferencia como los programadores y el diseño. Los puntos de referencia que se citan nos dicen más sobre la (in) competencia / (des) honestidad de sus autores que sobre los idiomas que pretenden comparar.
[Editar: como está implícito en un lugar anterior, pero nunca lo dije tan directamente como probablemente debería haberlo hecho, los resultados que estoy citando son los que obtuve cuando probé esto ~ hace 5 años, usando implementaciones de C ++ y Java que eran actuales en ese momento . No he vuelto a ejecutar las pruebas con las implementaciones actuales. Una mirada, sin embargo, indica que el código no se ha solucionado, por lo que todo lo que habría cambiado sería la capacidad del compilador para ocultar los problemas en el código.]
Sin embargo, si ignoramos los ejemplos de Java, en realidad es posible que el código interpretado se ejecute más rápido que el código compilado (aunque difícil y algo inusual).
La forma habitual en que esto sucede es que el código que se interpreta es mucho más compacto que el código de la máquina, o se ejecuta en una CPU que tiene un caché de datos más grande que el caché de código.
En tal caso, un pequeño intérprete (p. Ej., El intérprete interno de una implementación de Forth) puede encajar completamente en el caché de código, y el programa que está interpretando se ajusta completamente en el caché de datos. El caché suele ser más rápido que la memoria principal en un factor de al menos 10, y a menudo mucho más (un factor de 100 ya no es particularmente raro).
Entonces, si el caché es más rápido que la memoria principal por un factor de N, y se requieren menos de N instrucciones de código de máquina para implementar cada código de bytes, el código de bytes debería ganar (estoy simplificando, pero creo que la idea general aún debería ser aparente).