Во-первых, большинство JVM включают в себя компилятор, поэтому «интерпретируемый байт-код» на самом деле довольно редко (по крайней мере, в тестовом коде - это не так уж редко в реальной жизни, когда ваш код обычно представляет собой несколько простых циклов, которые повторяются очень часто ).
Во-вторых, значительное число задействованных критериев выглядит довольно предвзятым (я не могу сказать, по намерению или по некомпетентности). Например, несколько лет назад я посмотрел на исходный код, связанный с одной из опубликованных вами ссылок. У него был такой код:
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;
}
Так как calloc
обеспечивает нулевую память, использование for
цикла для обнуления снова бесполезно. За этим последовало (если память служит) заполнение памяти другими данными в любом случае (и никакой зависимости от обнуления), поэтому все обнуление было совершенно ненужным в любом случае. Замена приведенного выше кода на простое malloc
(как любой здравомыслящий человек использовал бы для начала) улучшило скорость версии C ++, достаточную для того, чтобы превзойти версию Java (с довольно большим запасом, если память будет работать).
Рассмотрим (для другого примера) methcall
тест, использованный в записи блога в вашей последней ссылке. Несмотря на название (и как вещи могут даже выглядеть), версия этого языка на C ++ не особо измеряет затраты на вызовы методов вообще. Часть кода, которая оказывается критической, находится в классе 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;
};
Критическая часть оказывается state = !state;
. Рассмотрим, что происходит, когда мы изменяем код для кодирования состояния int
вместо 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);
}
};
Это незначительное изменение улучшает общую скорость примерно на 5: 1 . Несмотря на то, что эталонный тест предназначался для измерения времени вызова метода, в действительности большая часть того, что он измерял, была временем преобразования между int
и bool
. Я, безусловно, согласен с тем, что неэффективность, показанная оригиналом, вызывает сожаление - но учитывая, как редко он возникает в реальном коде, и легкость, с которой его можно исправить, когда / если он возникает, мне трудно думать это так много значит.
В случае, если кто-то решит повторно запустить соответствующие тесты, я должен также добавить, что есть почти одинаково тривиальное изменение в версии Java, которая производит (или, по крайней мере, когда-то произведено) - я не запускал тесты повторно с недавняя JVM, чтобы подтвердить, что они все еще делают) довольно существенное улучшение в версии Java также. В версии Java есть NthToggle :: activ (), который выглядит следующим образом:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Изменение этого параметра для вызова базовой функции вместо this.state
непосредственного управления дает довольно существенное улучшение скорости (хотя этого недостаточно, чтобы не отставать от модифицированной версии C ++).
Итак, в результате мы получаем ложное предположение о интерпретируемых байтовых кодах и некоторые из худших тестов (которые я когда-либо видел). Ни один не дает значимого результата.
Мой собственный опыт показывает, что при одинаково опытных программистах, уделяющих одинаковое внимание оптимизации, C ++ будет превосходить Java чаще, чем нет - но (по крайней мере, между этими двумя) язык редко будет иметь такое же большое значение, как программисты и дизайн. Упомянутые критерии говорят нам больше о (не) компетентности / (не) честности их авторов, чем о языках, на которые они ориентированы.
[Править: Как подразумевается в одном месте выше, но никогда не указывалось так явно, как мне следовало бы, результаты, которые я цитирую, - это те результаты, которые я получил, когда тестировал это ~ 5 лет назад, используя реализации C ++ и Java, которые были актуальны в то время , Я не перезапускал тесты с текущими реализациями. Однако взгляд указывает, что код не был исправлен, поэтому все, что изменилось бы, это способность компилятора скрыть проблемы в коде.]
Если мы будем игнорировать примеры Java, однако, это действительно возможно интерпретировать код , чтобы работать быстрее , чем скомпилированный код (хотя трудно и несколько необычно).
Обычный способ, которым это происходит, заключается в том, что интерпретируемый код гораздо более компактен, чем машинный код, или он работает на процессоре, который имеет больший кэш данных, чем кэш кода.
В таком случае небольшой интерпретатор (например, внутренний интерпретатор реализации Forth) может полностью уместиться в кеше кода, а программа, которую он интерпретирует, полностью умещается в кеше данных. Кэш обычно быстрее, чем основная память, как минимум в 10 раз, а зачастую и намного больше (в 100 раз больше не редкость).
Таким образом, если кэш быстрее, чем основная память, с коэффициентом N, и для реализации каждого байтового кода требуется меньше, чем N инструкций машинного кода, байт-код должен выиграть (я упрощаю, но я думаю, что общая идея должна все же быть очевидным).