Случайно наткнулся на статью как раз по теме. Кое-что сказанное в ней весьма спорно (или устарело), однако в целом достаточно разумно, imho.
Ниже перевод примерно половины этой статьи (не совсем дословный).
Dejan Jelovic
Why Java Will Always Be Slower than C++
"Java обладает высоким быстродействием. Под высоким быстродействием мы подразумеваем достаточное. Под достаточным — низкое." © Mr.Bunny
Всякий кто пользовался достаточно сложными Java-программами, или программировал на Java, знает что Java уступает С++ в быстродействии. При использовании Java мы принимаем это как неизбежность.
Однако, многие уверяют нас, что это лишь временное явление. Java не является медленной по своей сути, говорят они. Быстродействие мало из-за несовершенства сегодняшних JIT компиляторов, которые недостаточно хорошо оптимизирует код.
Это неверно. Вне зависимости от того, насколько хорошими станут JIT компиляторы, Java всегда будет уступать С++ в быстродействии.
Суть дела
Люди, которые утверждают что Java может сравняться с С++ в быстродействии или даже обогнать его, зачастую исходят из того, что более строгий язык дает компилятору больше возможностей для оптимизации. Следовательно, если вы не собираетесь оптимизировать вручную всю программу, такой компилятор даст лучший результат, в целом.
Это правда. FORTRAN до сих пор уделывает С++ при работе с числами, поскольку является более строгим языком. C++ может конкурировать с FORTRAN лишь при использовании грамотно спроектированной библиотеки, например Blitz++.
Однако, для того чтобы получить подобные результаты, язык должен предоставлять компилятору возможности для оптимизации. К сожалению, Java не был разработан с учетом этого. Таким образом, каким бы умным не был компилятор, Java никогда не догонит С++.
Тесты
Парадоксально, но единственной областью, где Java может догнать С++ по скорости, являются типичные тесты на быстродействие. Если вам нужно вычислить энное число Фибоначчи или запустить Linpack, нет никаких причин для того, чтобы Java уступала С++. Пока вычисления происходят внутри одного класса и используются лишь встроенные типы данных вроде int и double, Java не отстает от С++.
Реальность
Как только вы начинаете использовать объекты в своей программе, Java теряет потенциальные возможности для оптимизации. В данном разделе перечислены некоторые причины этого.
1. Все объекты создаются в куче
Java выделяет память в стеке только для встроенных типов вроде int и double, а так же для ссылок. Все объекты создаются в куче.
Для больших объектов, которые по смыслу представляют из себя некую сущность, это не является недостатком. С++ программисты тоже создают такие объекты в куче. Однако, для небольших объектов, по смыслу являющимися значениями, это главная причина падения производительности.
Что за небольшие объекты? Для меня это итераторы. Я очень широко использую их в своих разработках. Кто-то другой может использовать комплексные числа. Программист 3-D графики может использовать вектора или точки. Всем этим категориям замена мгновенного выделения памяти в стеке на выделение памяти в куче определенно не понравится. Если подобное происходит в цикле, получаем время O(n) вместо нуля. Вложенный цикл даст уже O(n
2).
2. Частое приведение типов
С введением шаблонов, хорошие С++ программисты получили возможность почти полностью избавится от приведения типов в высокоуровневых программах. К сожалению, шаблонов в Java нет, таким образом приведение типов используется очень часто.
Что это значит с точки зрения быстродействия? Приведение типов в Java может быть только динамическим, а это ведет к накладным расходам. Насколько большим расходам? Допустим, вам необходимо реализовать динамическое приведение типов:
Самый оптимальный с точки зрения скорости способ — связать каждый класс с его номером, и создать матрицу, по которой можно узнать, связанны ли классы межу собой, и если да, какое смещение требуется добавить для приведения типа. Псевдокод может выглядеть примерно так:
code text
DestinationClass makeCast (Object o, Class destinationClass) {
Class sourceClass = o.getClass (); // JIT compile-time
int sourceClassId = sourceClass.getId (); // JIT compile-time
int destinationId = destinationClass.getId ();
int offset = ourTable [sourceClassId][destinationClassId];
if (offset != ILLEGAL_OFFSET_VALUE) {
return <object o adjusted for offset>;
}
else {
throw new IllegalCastException ();
}
}
Изрядное количество кода! И это оптимистичный вариант - ведь использование матрицы для хранения информации о взаимосвязи классов требует довольно большого объема памяти, и ни один нормальный компилятор так не сделает. Вместо этого, используют отображение (map) или проход по дереву наследования — оба этих способа еще более медленные.
3. Повышенные требования к объему памяти
Java-программы требуют примерно в два раза больший объем памяти для хранения данных, по сравнению с аналогами на С++. Это связано с тремя причинами:
- Программы с автоматической сборкой мусора, как правило, требуют на 50% больший объем памяти, по сравнению с программами, использующими "ручное" управление выделением памяти.
- Объекты, память под которые может быть выделена в стеке (С++), будут размещены в куче при использовании Java.
- Объекты Java больше сами по себе, так как обязательно имеют таблицу виртуальных функций и поддерживают синхронизацию.
Повышенные требования к объему памяти увеличивают вероятность использования файла подкачки. Это в свою очередь убивает быстродействие как ничто другое.