Primeiro, a maioria das JVMs inclui um compilador, portanto, "bytecode interpretado" é realmente muito raro (pelo menos no código de referência - não é tão raro na vida real, onde seu código geralmente é mais do que alguns loops triviais que são repetidos com muita frequência )
Segundo, um bom número de benchmarks envolvidos parece ser bastante tendencioso (seja por intenção ou incompetência, não sei dizer). Apenas por exemplo, anos atrás, observei alguns dos códigos-fonte vinculados a partir de um dos links que você postou. Tinha 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
fornece memória que já está zerada, usar o for
loop para zerá-lo novamente é obviamente inútil. Isso foi seguido (se a memória servir) preenchendo a memória com outros dados de qualquer maneira (e nenhuma dependência de zerar), portanto todo o zeramento era completamente desnecessário. Substituir o código acima por um simples malloc
(como qualquer pessoa sã teria usado no início) melhorou a velocidade da versão C ++ o suficiente para vencer a versão Java (por uma margem bastante ampla, se a memória servir).
Considere (por outro exemplo) a methcall
referência usada na entrada do blog em seu último link. Apesar do nome (e como as coisas podem parecer), a versão C ++ disso não está realmente medindo muito a sobrecarga de chamada de método. A parte do código que acaba sendo crítica está na 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;
};
A parte crítica acaba por ser a state = !state;
. Considere o que acontece quando alteramos o código para codificar o estado como um em int
vez 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);
}
};
Essa pequena alteração melhora a velocidade geral em uma margem de 5: 1 . Embora o benchmark visasse medir o tempo de chamada do método, na realidade, o que estava medindo era o tempo de conversão entre int
e bool
. Eu certamente concordaria que a ineficiência mostrada pelo original é lamentável - mas, dado o quão raramente parece surgir no código real, e a facilidade com que ele pode ser corrigido quando / se surgir, tenho dificuldade em pensar. disso significa muito.
Caso alguém decida executar novamente os benchmarks envolvidos, devo acrescentar que há uma modificação quase igualmente trivial na versão Java que produz (ou pelo menos uma vez produzida - eu não reexecutei os testes com um JVM recente para confirmar que ainda fazem) uma melhoria bastante substancial na versão Java também. A versão Java possui um NthToggle :: Activ () que se parece com isso:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Alterar isso para chamar a função base, em vez de manipular this.state
diretamente, oferece uma melhoria substancial na velocidade (embora não seja suficiente para acompanhar a versão C ++ modificada).
Então, acabamos com uma suposição falsa sobre códigos de bytes interpretados versus alguns dos piores benchmarks (já) que eu já vi. Nem está dando um resultado significativo.
Minha própria experiência é que, com programadores igualmente experientes prestando a mesma atenção à otimização, o C ++ vence o Java com mais frequência - mas (pelo menos entre esses dois), a linguagem raramente faz tanta diferença quanto os programadores e o design. Os benchmarks citados nos dizem mais sobre a (in) competência / (des) honestidade de seus autores do que sobre os idiomas que pretendem ser benchmark.
[Edit: Como está implícito em um lugar acima, mas nunca foi tão diretamente quanto eu provavelmente deveria), os resultados que eu estou citando são os que obtive quando testei isso ~ 5 anos atrás, usando implementações C ++ e Java atuais na época . Não executei novamente os testes com as implementações atuais. Uma olhada, no entanto, indica que o código não foi corrigido; portanto, tudo o que teria mudado seria a capacidade do compilador de encobrir os problemas no código.]
Se ignorarmos os exemplos de Java, no entanto, é realmente possível que o código interpretado seja executado mais rápido que o código compilado (embora difícil e um tanto incomum).
A maneira usual de isso acontecer é que o código que está sendo interpretado é muito mais compacto que o código da máquina ou está sendo executado em uma CPU que possui um cache de dados maior que o cache de código.
Nesse caso, um pequeno intérprete (por exemplo, o intérprete interno de uma quarta implementação) pode caber inteiramente no cache de código, e o programa que está interpretando se encaixa inteiramente no cache de dados. O cache normalmente é mais rápido que a memória principal por um fator de pelo menos 10 e geralmente muito mais (um fator de 100 não é mais particularmente raro).
Portanto, se o cache for mais rápido que a memória principal por um fator N e forem necessárias menos de N instruções de código de máquina para implementar cada código de byte, o código de byte deverá vencer (estou simplificando, mas acho que a ideia geral ainda deve ser aparente).