Сравнение производительности разных языков программирования, технологий или библиотек это очень неблагодарное занятие.
Все сравнения такого рода искусственные и часто страдают от того, что их авторы в разной степени владеют различными технологиями
или потратили разное время для оптимизации кода под ту или иную технологию.
Однако, меня чаще всего интересует не столько крутизна той или иной технологии, сколько применимость различных технологий для
решения различных реальных задач. На Java я обычно решаю бизнес задачи и написанный код в первую очередь должен быть простым и понятным,
а уже во вторую очередь быстрым. Там же где нужна абсолютная скорость,
приходится оптимизировать код, дизайн и архитектуру системы,
и жертвовать многим, в том числе простотой и понятностью кода. И тогда возникает закономерный вопрос: а подходит ли Java
для написания высокопроизводительных систем? Не надо ли все их писать на C или на C++? Может ли код на Java выжать максимум,
или почти максимум, из имеющегося железа? Какую долю производительность приходится принести в жертву ради простоты и удобства Java?
Я не отвечу на все эти вопросы в этой записи, но поделюсь некоторым накопленным в этой области опытом. Для того, чтобы
была пища для первоначального обсуждения, я переписал тест
из предыдущей записи,
который просто заполнял массив случайными 32-битными числами и замерял скорость их суммирования, на C++.
Чтобы сохранить дух оригинального кода, я
использовал не массив напрямую, а абстракцию динамически растущего массива целых чисел. К счастью, в C++ для этого не надо
писать своего класса по аналогии с
простеньким
IntList.ViaJavaArray, который
пришлось написать для теста на Java,
а можно воспользоваться готовым
vector из STL.
Я специально использую тип __int32, чтобы было проще потом проверить
те же самые вычисления в другой архитектуре с другим размером int (для тех читателей, кто не часто общается с C/C++, напомню, что
там размеры стандартных типов типа int и long не стандартизированы, а в общем случае зависят от архитектуры, компилятора и
его настроек).
Основной цикл на C++ написан в том же самом духе, что и на Java, а весь код можно найти
здесь.
Я даже заполняю массив теми же самыми псевдо-случайными числами и убеждаюсь, что код на C++ выдает те же самые суммы, что и код на Java.
__int32 IntVectorIterationTiming::runIteration() {
__int32 sum = 0;
for (int i = 0, n = vec.size(); i < n; i++)
sum += vec[i];
return sum;
}
У меня на Windows машине для компиляции C++ кода есть Microsoft Visual C++ 2008 и
gcc version 4.5.0 (GCC) для mingw32. Я потратил некоторое время экспериментируя с различными опциями оптимизации, пытаясь достичь
максимальной скорости работы этого кода (минимального времени в расчете на одну итерацию) и добился наилучших результатов
используя "g++ -O3 -funroll-loops". Напомню, что на Java я использовал "java -server" для запуска JVM. Вот что у меня получилось,
в сравнении с аналогичным циклом итерации по массиву целых чисел на Java
из предыдущей записи:
РазмерJavaC++
1 0000.370.38
10 0000.400.41
100 0000.440.42
1 000 0000.660.71
10 000 0000.750.73
Результаты неотличимы с той точностью, которую дает этот эксперимент (я не замеряю вариацию результатов, но наблюдая за различными запусками вижу,
что она больше чем разница этих значений). Надо заметить, что отключение "-funroll-loops" дает заметно худшую производительность на маленьких
размерах списка, но не так сильно влияет на скорость работы на больших, ибо при больших размерах
скорость работы все-равно в целом упирается в память.
Размерg++ -O3 -funroll-loopsg++ -O3
1 0000.380.75
10 0000.410.75
100 0000.420.75
1 000 0000.710.92
10 000 0000.730.92
Какие выводы можно отсюда сделать? Из одного сравнения нельзя делать далеко идущие выводы! Однако, занимаясь подобного рода сравнениями
регулярно, можно заметить некоторые общие темы. При условии одинаковых
структур данных и алгоритмов, работа с целыми числами оптимизируется
HotSpot Server JVM на очень и очень достойном уровне и, в отличие от C++, не требует шаманства с настройками оптимизации компилятора,
ибо HotSpot сам вычислит какие именно куски кода требуют компиляции, где нужно развернуть циклы, где применить агрессивный inlining,
догадается где безопасно убрать проверки границ массивов, и т.д.
Конечно, отсутствие в Java массивов структур (есть только массивы примитивных типов и массивы указателей) ограничивает возможности
по оптимальному представлению структур данных в памяти, но и с массивами примитивных типов обычно можно выкрутиться не сильно
потеряв в производительности.
Ахиллесовой пятой HotSpot безусловно является работа с вещественными числами. В силу очень жестких ограничений спецификации Java
на допустимые результаты вычислений в вещественной арифметике, многие оптимизации, которые могут выполнять компиляторы C++, на Java
просто не возможны. Есть и другие тонкости и подводные камни, которые надо знать и которыми я планирую поделиться в своем журнале.
Однако, есть и одно общее правило: если вам важна производительность, то измеряйте скорость работы кода для вашей задачи
с выбранными вами технологиями, оптимизируйте, экспериментируйте.
UPDATE: Конечно, можно получить с помощью gcc и более быстрый код, если использовать "-msse2", но философские и практические
аспекты этого (с результатами замеров) подождут отдельной темы про SIMD.
UPDATE2: Посмотреть ассемблерный код, который выдает Java для этого теста можно
здесь.