En primer lugar, la mayoría de las JVM incluyen un compilador, por lo que el "bytecode interpretado" es bastante raro (al menos en el código de referencia), no es tan raro en la vida real, donde su código suele ser más que unos pocos bucles triviales que se obtienen. repetido extremadamente a menudo).
Segundo, un buen número de los puntos de referencia involucrados parecen estar bastante sesgados (ya sea por intención o por incompetencia, realmente no puedo decir). Solo por ejemplo, hace años miré algunos de los códigos fuente vinculados a uno de los enlaces que publicaste. Tenía 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, el uso del bucle for
para ponerlo en cero nuevamente es obviamente inútil. Esto fue seguido (si la memoria sirve) llenando la memoria con otros datos de todos modos (y no dependía de que se pusiera a cero), por lo que toda la reducción a cero era completamente innecesaria de todos modos. Reemplazar el código de arriba 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
benchmark usado en la entrada del blog en su último enlace. A pesar del nombre (y cómo podrían verse las cosas), la versión en C ++ de esto no realmente mide mucho sobre la sobrecarga de llamadas a métodos. La parte del código que resulta 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 el state = !state;
. Considere lo que sucede cuando cambiamos el código para codificar el estado como int
en lugar de 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 . A pesar de que el punto de referencia fue destinado para medir el tiempo de llamada del método, en realidad la mayor parte de lo que estaba midiendo era el tiempo para convertir entre int
y bool
. Ciertamente, estoy de acuerdo en que la ineficiencia mostrada por el original es desafortunada, pero dada la poca frecuencia con que parece surgir en el código real y la facilidad con la que puede solucionarse cuando surja, tengo dificultades para pensar. de lo que significa mucho.
En caso de que alguien decida volver a ejecutar los puntos de referencia involucrados, también debo agregar que hay una modificación casi igual de trivial en la versión de Java que produce (o al menos una vez que se produjo, no he vuelto a ejecutar el pruebas con una 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);
}
Al cambiar esto para llamar a la función base en lugar de manipular this.state
directamente, se obtiene una mejora sustancial de la velocidad (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 acerca de 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 la misma 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 establecer como puntos de referencia.
[Editar: Como lo implícito en un lugar arriba pero nunca lo dije tan directamente como debería haberlo hecho, los resultados que cito son los que obtuve cuando probé esto hace ~ 5 años, usando las implementaciones de C ++ y Java que eran actuales En ese tiempo. No he vuelto a ejecutar las pruebas con las implementaciones actuales. Sin embargo, una mirada indica que el código no se ha corregido, por lo que todo lo que habría cambiado sería la capacidad del compilador para encubrir los problemas en el código.]
Si ignoramos los ejemplos de Java, sin embargo, es realmente posible para que el código interpretado se ejecute más rápido que el código compilado (aunque es difícil y un tanto 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 del caché de código.
En tal caso, un pequeño intérprete (por ejemplo, el intérprete interno de una implementación de Forth) puede encajar completamente en el caché de código, y el programa que interpreta se ajusta completamente en el caché de datos. La memoria caché suele ser más rápida que la memoria principal en un factor de al menos 10, y con frecuencia 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 necesitan menos de N instrucciones de código de máquina para implementar cada código de byte, el código de byte debería ganar (estoy simplificando, pero creo que el general La idea aún debe ser aparente).