Java обгоняет по производительности c


Содержание

Может ли Java быть быстрее C++?

Есть у меня друг, Стас, с которым мы частенько, как настоящие программисты, особенно при наличии разных средств внутреннего подогрева, регулярно имеем традиционные пузомерки C++ (я) vs Java (он). Понятно, что социальная составляющая является основной в данных беседах, и, очевидно, полного конценсуса тут нет и быть не может, что и хорошо.

Но иногда, уже на трезвую голову, когда я думаю о подобном сравнении, при всем моем желании понять, почему Java в принципе может быть не то что быстрее, а хотя бы не медленнее C++, у меня не хватает аргументов даже для себя самого.

Для начала несколько “дано”:

  • Мы сравниваем С++ 2011, компилируемый в машинный код, и обычную Java 7 (не real-time, embedded или что-то в этом роде), компилируемую в JVM-код, который только в процессе выполнения будет налету через JIT тоже компилироваться в машинный код.
  • Допустим, компиляторы C++ и Java генерируют максимально эффективный код, насколько семантика языка позволяет оптимизировать.

Положим, A – это линейная скорость выполнения машинного когда. B – скорость компиляции байт-кода JVM в машинный код. Тогда общая скорость выполнения кода:

Очевидно, что B1 = 0 , так как С++ генерирует машинный код напрямую и не требует дополнительной работы в процессы выполнения. Но B2 стопроцентно НЕ ноль, так как каким бы эффективным не был компилятор JIT, он ВСЕГДА требует какого-то времени для компиляцию. Более того, JIT не компилирует все сразу, а “подкомпилирует” по мере прохождения путей выполнения. Получается, всегда есть ненулевая вероятность, что неожиданно придется выполнить код, ранее не требуемый, и потребуется время на его компиляцию. Даже если предположить, что компилятор JIT применяет изощренные способы предсказания путей выполнения и делает все, чтобы уменьшить B2 , но B2 по определению не 0. Если был бы 0, то не было бы JVM, а был бы чистый машинный код.

Далее, рассмотрим A1 и A2 . Эти параметры определяют, насколько эффективно компилятор создает код (или байт-код). По моему личному, субъективному и предвзятому мнению, у С++ (не С) больше шансов на оптимизацию благодаря шаблонам (компилятор имеет полноценную семантическую информацию для проведения inline’а) и генерация машинного кода под конкретную платформу (компилятор точно знает, какие машинные инструкции были бы максимально эффективны в каждом случае). Увы, я не особо силен в generic’ах Java, и руководствуюсь только слухами, что в Java они “ненастоящие”, добавленные гораздо позже и уступающие шаблонам C++. И так как компилятор обязан выдать стандартный переносимый JVM-код, то нет возможности оптимизировать под конкретную платформу. Есть надежда, что это сделает JIT, но там уже не будет семантической информации для более глубокой оптимизации. А еще JIT должен быть быстр, то есть будет компромисс между качеством оптимизации и скоростью компиляции. В С++ такой проблемы нет, так как компилировать можно как угодно долго.

Итак, это мои доводы для меня самого, измеренные в виртуальных попугаях. Не получается у меня убедить самого себя, что Java может быть быстрее или хотя бы на уровне с С++ по скорости. Буду рад за помощь в понимании этого вопроса.

Мы со Стасом проводили несколько несложных сравнений, в основном на реализации QuickSort, и Java по линейной скорости кода проигрывала где-то на 10%.

До C++ 2011 можно было говорить, у С++ нет модели памяти и стандартной библиотеки для потоков, поэтому у Java есть шанс выиграть на многопоточности, но сейчас у С++ все на месте. А подходы к многопоточности у С++ и Java, как мне кажется, одинаково неудобные (хотя std::async() – это очень сильная возможность), и им обоим далеко до goroutines в Go, actor’ов в Scala и т.д.

Понятно, что 10% не всегда делают погоду. Иногда важнее развитые инструменты интроспекции, среды разработки, контролируемое выполнение, замена кода налету и много другое, что дает платформа Java, и не дает “молотилка” C++. Но зачем говорить про скорость то?

Оптимизация производительности Java приложений

Не смотря на то, что описываемые ниже трюки и советы работают не только в J2ME, именно для мобильных приложений они имеют первостепенное значение в силу ограниченности ресурсов платформы.

Техникиоптимизации производительности, как правило, основаны на увеличении объема памяти, необходимой программе для работы. К сожалению, ресурсы платформы Java ME очень ограничены, и программисту приходится постоянно балансировать между производительностью и экономией системных ресурсов. На мой взгляд, рано начатая оптимизация кода ведет к усложнению и замедлению процесса разработки, поэтому большинство приведенных тут советов лучше применять уже на завершающей фазе разработки, когда уже все отлажено и работает.

1. Избегайте синхронизации

Известно, что код, в котором используется механизм синхронизации, примерно в 4 раза медленнее обычного кода. Независимо от конкретной реализации Java VM использование синхронизации требует от виртуальной машины большого количества дополнительных действий: она должна отслеживать блокировки, блокировать контекст при начале работы с ним и разблокировать, когда работа с контекстом закончена. Потоки, которые хотят получить доступ к заблокированному контексту вынуждены стоять в очереди и ждать его освобождения. Думаю, что привел достаточно убедительные доводы, и Вы будете использовать синхронизацию только там, где без нее действительно невозможно обойтись.

2. Используйте предварительные вычисления

Если Вы разрабатываете игру с 3D или 2.5D графикой, то наверняка используете массу математических вычислений с тригонометрическими функциями. Такие расчеты сильно нагружают процессор, поэтому стоит заранее просчитать наиболее сложные выражения и представить их в виде массива, откуда доставать готовые значения в процессе выполнения программы. Помимо графики существует масса приложений, где предварительные вычисления можно сделать заранее и оформить в виде массивов данных.

3. Вытягивание массивов

Доступ к элементам массива занимает больше времени, чем работа с обычными переменными. Многомерные массивы — еще более медленная история. Избегайте использования многомерных массивов. В большинстве случав они легко заменяются одномерными.

А еще одномерные массивы потребляют меньше динамической памяти, чем их многомерные собраться.

4. Разворачивание циклов for

Циклы — это замечательная штука, но они несут в себе дополнительные накладные расходы. Вместе с вызовом тела цикла на каждом шаге выполняется операция увеличения счетчика и проверка условия. Например

Выполняется виртуальной машиной следующим образом:

Я специально сделал этот список таким длинным, чтобы Вы почувствовали, насколько наша жизнь стала проще с появлением циклов. Однако многие программисты привыкли видеть в цикле только абстракцию и не задумываются о накладных расходах, сопряженных с их использованием.

Если вы на этапе программирования точно знаете число необходимых итераций, Вы можете частично развернуть цикл:

Эта реализация будет повторяться всего 3 раза, соответственно накладные расходы уменьшатся в 5 раз по сравнению с предыдущим примером.

Не смотря на вполне очевидный выигрыш от использования разворачивания циклов, не слишком увлекаться этой техникой. В результате разворачивания генерируется больший по размеру байт код, и может сложиться ситуация, когда тело цикла не поместится полностью в кеш процессора. В результате будут задействованы механизмы подгрузки-выгрузки частей кеша, и ваш оптимизированный код будет сильно тормозить.

5. Сжатие циклов for

Смысл операции сжатия заключается в вынесении за рамки цикла всего того, что не нуждается в повторном вычислении:

Во втором примере переменным a и b значение присваивается всего один раз. Таким образом, по сравнению с первым вариантом нам удалось избавиться от 20 лишних операций присваивания значений.

Приведу еще одну менее очевидную технику сжатия циклов. Никогда не вызывайте методы вычисления размеров в заголовке цикла. Сравните:

В первом случае на каждом шаге вычисляется размер msgs. Во втором случае — это делается один раз до начала цикла. Конечно, эта оптимизация подразумевает, что тело цикла никак не влияет на размер msgs.

6. Избегайте интерфейсных вызовов методов

В байткоде Java существует 4 типа методов. Ниже они перечислены в порядке уменьшения скорости вызова.

invokestatic
Статические методы не используют экземпляр класса, поэтому им не нужно соблюдать правила полиморфизма и искать подходящий параметрам вызова экземпляр.


invokespecial
Специальные методы — конструкторы, private и super class методы.

invokeinterface
Требуют поиск подходящей реализации интерфейсного метода.

Тип используемых методов влияет на весь дизайн приложения, поэтому помните о скорости вызовов методов в процессе разработки.

Вызов статических методов обеспечивает наилучшую производительность, поскольку Java VM не нужно ничего искать. Вызов интерфейса напротив — самый медленный путь, требующий два поиска.

7. Избегайте ненужных обращений к массиву

Обращение к элементу массива — не самая быстрая операция. Если Вы видите, что какой-то элемент используется в алгоритме многократно — сохраните его в переменную, которую затем и используйте.

8. Избегайте использования аргументов

При вызове нестатических методов вы неявно передаете ссылку this вместе с другими параметрами. В Java VM вызов методов реализован по принципу стека. При каждом вызове аргументы заносятся в стек, а затем извлекаются из него при выполнении метода. В некоторых случаях можно избежать необходимости использования стека, отказавшись от передачи аргументов.

С точки зрения высокоуровневого программирования приведенные в примере стили очень похожи, однако второй метод работает быстрее. Еще большей скорости вызова можно добиться, если объявить все переменные и методы как статические. В этом случае при вызове метода стек не будет задействован вообще.

9. Откажитесь от использования локальных переменных

Локальные переменные помещаются и извлекаются из стека при вызове каждого метода. С точки зрения производительности гораздо эффективнее использовать обычные переменные.

10. Не используйте getter-ы/setter-ы

Откажитесь от использования методов, устанавливающих и считывающих значения полей класса и обращайтесь к ним напрямую. Конечно, это немного противоречит базовым принципам ООП, но с точки зрения оптимизации этот шаг вполне оправдан. Обычно я избавляюсь от этих методов на финальной стадии разработки проекта. Это редкий случай, когда оптимизация производительности связана с уменьшением размера программы.

11. Эффективная математика

С точки зрения производительности, не все математические операции равны по скорости выполнения. Быстро работают сложение и вычитание. Умножение, деление, вычисление модуля — заметно медленнее. Самыми быстрыми являются побитывые операции.

Этими методами оптимизации пользовались еще праотцы, когда писали первые игры под DOS. Для Java они тоже подходят.

12. Пишите кратко

Применяйте операторы типа +=, поскольку они генерируют короткий байткод.

13. Используйте встроенные методы

Пользуйтесь методами, предоставляемыми платформой. Например, использование System.arraycopy для копирования элементов массива будет более эффективным, чем аналогичная собственная реализация.

14. Используйте StringBuffer вместо String

Если Вы работаете со строками, значения которых могут меняться по ходу выполнения программы, используйте класс StringBuffer вместо String. Любое изменение объекта String приводит к созданию нового объекта.

Заключение

Выше были приведены простейшие методы оптимизации, которые могут с успехом использоваться при разработке приложений для мобильной платформы J2ME. Эти методы основаны на анализе байт-кода и позволяют выработать стиль программирования, обеспечивающий оптимальный результат.

Сравнительный анализ по скорости работы Java, C++, Assembler

Делаю диплом и обоснование того, почему я выбрал язык С++ для написания дипломной работы. Так вот, одним из пунктов должно быть что-то типа скорость работы или быстрота работы. Но так не назовешь этот пункт. Ясно что быстрее всего работают проги на ассемблере, потом плюсы, потом джава. Подсобите, как можно назвать пункт в котором проводится это сравнение? Скорость работы? Время выполнения . но код в таблицу не вставишь.

4 ответа 4

Неверно утверждение о том, что код на ассемблере работает быстрее всего. Точнее сказать верно только при одновременном соблюдении условий: конкретный процессор, конкретный разработчик, умеющий оптимизировать код под этот процессор. В среднем компилятор лучше знает о всевозможных оптимизациях.

Попробуйте акцентировать внимание на другом:

  • C++ — высокоуровневый язык, что позволяет ускорить разработку и упростить поддержку в сравнении с ассемлером;
  • C++ — высокоуровневый язык, что позволяет упростить портирование кода на другую платформу в сравнении с ассемблером;
  • С++ язык с прямым управлением памятью, что позволяет избежать деградаций в работе приложения, связанных со сборкой мусора в сравнении с языками с автоматическим управлением памятью (Java, С#).

Как ускорить java?

Язык Java /

Основы языка Java

29 сен 2020 18:53
29 сен 2020 21:17
30 сен 2020 20:58

Java далеко не всегда отстает по производительности. В основном это касается низкоуровневых оптимизаций вроде применения SSE, где хороший компилятор C++ может дать результаты, лучшие, чем встроенный JIT-компилятор, жестко ограниченный в допустимом времени компиляции.


Перед тем, как искать пути «ускорения языка», нужно измерить производительность критичного участка кода, убедиться, что она недостаточна для решаемой задачи, затем повторить то же самое на C++, убедиться, что есть существенная разница, и только потом задумываться об оптимизации. По сравнению с C# у Java вообще не должно быть отставаний в скорости, это языки сходного типа.

И помните, что в 99% случаев для оптимизации гораздо важнее алгоритм, а не качество оптимизации в компиляторе.

30 сен 2020 22:32

1) Если разрабатывается что то вроде web-приложение, то производительность, скорее всего, ограничится скоростью операций ввода/вывода. И не важно, что С++ в несколько раз быстрее Java, все равно большую часть времени процессор будет простаивать в ожидании завершения этих операций;
2) Разрабатывается приложение, выполняющее тяжелые вычисления. В этом случае выбор Java будет архитектурным просчетом, т.к. не будет обеспечена необходимая гибкость в возможностях оптимизации.

P.S. Есть еще, конечно, вариант, когда делается одноразовое решение под свой проект, в котором эффективность по времени не важна или приемлема без оптимизации. Алгоритм работает сутки? Да пускай хоть год — потерпим.

Изменен:01 окт 2020 05:29
01 окт 2020 14:41

1) Если разрабатывается что то вроде web-приложение, то производительность, скорее всего, ограничится скоростью операций ввода/вывода. И не важно, что С++ в несколько раз быстрее Java, все равно большую часть времени процессор будет простаивать в ожидании завершения этих операций;
2) Разрабатывается приложение, выполняющее тяжелые вычисления. В этом случае выбор Java будет архитектурным просчетом, т.к. не будет обеспечена необходимая гибкость в возможностях оптимизации.

Все правильно. Но все-таки стоит уточнить, что эти правила не безусловны.

1) Существуют веб-сервисы и библиотеки общего назначения, которые должны быть максимально производительны — просто потому, что они ни при каких условиях не должны оказаться узким местом. Я недавно изучал библиотеку Grizzly на достаточно «низком» уровне (мне помогал один из ее разработчиков), и видно, что разработчики экономили буквально каждый такт. Хотя она написана на Java, но приемы, которые там использованы, далеки от духа Java: никаких неизменяемых объектов, обработка цепочек символов посимвольно (вместо применения String и т.п.), порой довольно громоздкий код. Казалось бы, несколько тысяч тактов (меньше микросекунды) — пустяк, но если это происходит при каждом запросе к серверу под нагрузкой с тысячами запросов в секунду, то в сумме подобные неэффективности могут заставить выбрать другую библиотеку.

2) Способность оптимизации «тяжелых вычислений» очень зависит от сложности алгоритма. Если это простые операции на уровне отдельных байтов — да, C++ может выдать лучший код. А если это решение системы линейных уравнений или аналогичные манипуляции вещественными числами double — то далеко не факт, что оптимизатор C++ «обгонит» JVM. Кроме того, внутренние циклы Java всегда можно вынести в нативный код через JNI. Но в отличие от изначальной ориентации на C++ это будет именно оптимизацией, которую можно будет выполнить именно для тех платформ, где это понадобилось (в том числе и доведя до уровня ассемблера), между тем все прочие участки кода останутся работоспособными и надежными.

Сегодня во многих случаях гораздо актуальнее не оптимизация за счет хорошего компилятора, а грамотное использование GPU. Это уже и не C++, и не Java. При этом выбор хорошего алгоритма остается основой быстродействия.

Сравнение производительности программ на C++ и Java

Есть две программы работающие по одному и тому же алгоритму(нахождение простых чисел через решето Эратосфена) на C++ и Java вот код:

20.04.2020, 18:47

Сравнение производительности графики
Скажите какой код будет быстрее работать? Визуально трудно определить, а с точки зрения технлогии.

Сравнение производительности
Приветствую. Начал изучать асм вставки. Решил ради наглядности написать несколько функций и.

Сравнение производительности
Здравствуйте! Вопрос довольно простой, но мне важно убедиться) vector v;.

Оптимизация производительности программ на С++
Какие более известные узкие места есть в программах на С++? Из того что я знаю: 1. Должен быть.

Оценка производительности программ
Как оценивать производительность программы? Например, время выполнения конкретного участка кода. С.

21.04.2020, 10:23 2
21.04.2020, 11:45 3
21.04.2020, 15:06 [ТС] 4
21.04.2020, 15:06
21.04.2020, 15:27 5
21.04.2020, 17:50 [ТС] 6
21.04.2020, 21:01 7
21.04.2020, 21:17 8
21.04.2020, 21:24 9
21.04.2020, 22:15 10
22.04.2020, 01:46 11

А какая современная и эффективная?

Это какая именно?

22.04.2020, 02:49 12

Ну на инстантном переборе мусора с сортировкой кого надо перебирать кого нет в компайлтайме. Для обеспечения корректного поведения слабых ссылок и ресурсов другого варианта нет. А тем более для обеспечения поведения связей по правилам ООП.

Добавлено через 6 минут
При этом не надо путать понятие управление памятью и управление временем жизни. Это две абсолютно разных задачи. Кастомизация ни одной из них с GC не возможна. А тем более с задачей менеджмента взаимосвязей автоматика для которой в яве вообще невозможна.

Java обгоняет по производительности c

Ява? Это когда вместо «Кобол наносит ответный удар» пишут «удар.нанестиОтвет (новый Кобол ())» — вот это Ява.


»
— Программисты Lisp

Saying that Java is nice because it works on all OS’s is like saying that anal sex is nice because it works on all genders.

« »
— Программисты .NET

Жа́ба — искажённое название языка программирования Java, наследника C++, откуда выпилили почти все беззнаковые типы данных и указатели. Определённой эмоциональной окраски не несёт, но, ввиду плохого отношения к Java значительной части населения ЛОРа, чаще употребляется в уничижительных выражениях: жабокодер, жабобыдлокодер и т. д. — это значение было популяризовано Луговским. Алсо, жабонебыдлокодеры имеют зарплату, на которую можно купить не только хлеб, воду и сало, как похапекодерам. А некоторым хватает ещё и на чёрную икру, Феррари и 25-летний виски.

Наиболее заметная особенность языка — крайне низкая скорость работы, связанная, в основном, с некоторыми особенностями национального программирования.

Содержание

[править] История

Посадить двуногого автомата, можно нескольких, набирать счета вручную. И никакой жабы не надо.

»
— Программисты 1С

Есть версия, что миф о «тормознутости» Java еще связан с тем, что в доисторические времена в браузерах были апплеты, которые адски тормозили. Открытие странички с Java-апплетом вызывало подвисание браузера на минуту-другую, а в строке состояния появлялась надпися: «Приложеньице загружается…» — что не могло не радовать пользователей. Особенно цинично это выглядело, когда причиной тормозов был апплет с анимированной кнопкой «e-mail» размером 100 на 50 пикселей. Были даже спецпрограммы, чтобы генерировать такие апплеты для говносайтов.

Реклама тех лет преподносила жаббу как волшебную таблетку, как серебряную пулю, как избавление от страданий: « Life is too short to spend rewriting code ». Обещалось также, что исходные коды станут собственностью разработчиков, они смогут продавать их, получая отчисления, да такие, шо все бабы будут теч. Корпоративный коммунизм не наступил, а исходные коды апплетов просуществовали ровно два года, с 1997 по 1999, после чего жабба уползла в мир Энтерпрайзных Серверов, а те немногие, кто по молодости верил в рекламные бредни, херачили интернет-магазины на решётках да на Пыхе по 8 часов в день 40 часов в неделю: в нирвану их так и не взяли.

В мире тяжёлых серверов жаба мутировала настолько, что изменились даже классы для работы с датой и временем, а некоторые методы в них исчезли. Кроме того, в доисторические времена у жаба-машин не было JIT-компиляции, и они выполняли байт-коды последовательно, команда за командой. Но теперь, при обращении к участку кода, который был вызван уже с десяток тысяч раз (как известно, 99% всего времени выполняется 0,1% кода), его стали компилировать в реальный машинный код. В итоге, жабба теперь ещё сильней тормозит при запуске… но зато потом уже работает очень быстро. В том числе и на новых процессорах, под которые JIT создаёт максимально оптимизированный код (обычные компиляторы — как правило создают один код, который совместим с большинством существующих процессоров).

[править] Достоинства

Попадание в энтерпрайз имело и хорошие стороны. Прежде всего:

  • Долгий цикл поддержки: хеллоу ворлды на всех новых версиях обратно совместимы вплоть до java 1.0.2 (да, это камень в сторону .NET). Для программ сложнее обязательно всплывет десяток деталей реализации например.
  • Полная кроссплатформенность: работает на Windows, Linux, Mac OS X, BSD, Solaris, AIX, HP-UX, QNX, канувшим в Лету Irix, OS/2, а также совсем безумные реализации типа NanoVM для микроконтроллеров — подо всё есть совместимая реализация JDK, причём правильно написанная программа будет под всем этим работать одинаково. Естественно доступный функционал будет пересечением возможностей выбранных платформ. Вряд ли получится поиграть в написанного на JavaFX сапера на стиральной машине без долгого допиливания.
  • Наличие макро-языков: NetRexx, Clojure, Groovy, Scala (функциональный язык широкого назначения), Kotlin, Ceylon и таких библиотек, как Quercus, позволяющих повторно использовать код, написанный ранее на скриптовых языках.
  • Безопасность: такие вещи, как SQL Injection (разумеется, если не использовать SQL со строковыми параметрами, что вообще-то справедливо для любого языка), падения в корку при неправильной работе с памятью и ряд других «казалось бы мелочей» — в жаббе отсутствуют. Какой ценой та безопасность была достигнута — отдельный вопрос, но в мире корпораций (и в мире бумажных денег) это никого не интересует.

Жабба версий 1.2 и далее заточена на написание и поддержку сетевых приложений, которые по технической сложности могут превосходить самотрансформирующуюся шатл-субмарину с минимальными затратами на отладку и каждодневное латание дыр. Достигнуто это за счёт таких её особенностей, как:

  • Принудительное ООП. В правильных (то есть выпрямленных суровой необходимостью) руках, управляемых средней тупости головой (этими качествами обладают большинство прогеров на западе), ООП приводит к большей модульности кода с меньшим числом концептуально лишних зависимостей.
  • Программы пишутся не под настоящий процессор и исполняются не на физической машине, а в виртуальной, которая заточена на поддержку именно таких программ, что разом устраняет тонны матана, брэйнфака и задротства из проектной документации и списков требований к работникам (но не надо думать, что их остается так уж мало).
  • Существует поверие, что на SPARC-серверах Sun/Oracle с установленной солярой java-байткод исполняется аппаратно и даже опережает в производительности C/C++ чуть более, чем в два раза.
  • Отсутствие указателей, адресной арифметики и беззнаковых целых, что исключает как класс примерно 93,7438935621% всех ошибок и уж точно ВСЕ самые сложные и дорогостоящие из них (ошибки общего дизайна продукта здесь не к месту, так как такими вещами рядовые кодеры не занимаются). ЧСХ, на практике оказывается, что никакой нужды в адресной арифметике нет.
  • Тысячи их!

Этим всем жабба бессовестно и нагло лишила программинг романтики и обеспечила возможность добиваться сносного результата в приемлемые сроки даже с кудрявыми руками и не очень мощным мозгом. Чем, конечно же, вызвала неприятие Ъ гиков.

Жабба — язык для создания серверных приложений. Никто не пишет на ней тру-GUI-приложений на стандартных библиотеках AWT/SWING и нет в этом особой необходимости. Зато ещё как пишут распределённые вычислительные системы, разворачивают большие интернет-магазины, порталы и прочие вещи, на которых зарабатывают деньги. А всё потому, что это надёжно, просто и безопасно.

[править] А ещё

А главное, и, пожалуй, единственное реальное достоинство — востребованность на рынке труда, и, как следствие, оплачиваемость этого самого труда, особенно в международных корпорациях: при весьма низком пороге вхождения за 2-3 года опыта вполне можно получить зарплату на 50% выше средней в IT-индустрии, причём востребованность работодателем не падает уже которые годы (говоря проще, берут любой неадекват, часто с минимальным опытом, лишь бы умел набивать хоть как-то работающие строки кода). Как нетрудно догадаться — это достоинство запросто перекрывает и тормоза, и отсутствие метапрограммирования и этих ваших беззнаковых целых.

[править] НЕНАВИСТЬ!

Основными претензиями в сторону чудо-языка были, есть и будут:

  • Не слишком вменяемые стандартные библиотеки, тянущиеся ещё с 1-й версии. На самом деле, это проблема всего ООП, где ставят существительные перед глаголами и где описание предметной области важнее, чем описание действий в ней.
  • До сих пор указатели (только теперь они называются «ссылки на объекты»), до сих пор глобальные переменные (только здесь они были названы «поля в объектах»), что по прежнему вызывает зависимости в коде и затрудняет отладку — но надо учесть, что это не Java-машина создаёт несколько ссылок на одни и те же данные или циклические ссылки между ними, а кое-кто другой.
  • Отсутствие нормального метапрограммирования. Функций высшего порядка не было (сейчас уже есть, в 8-й версии запилили лямбды и функциональные интерфейсы): вместо них — па́ттерны. В версии 1.5 запилили обобщения и аннотации, но они не решают всех проблем. И количество ключевых слов в языке всё растёт и растёт, а выразительность языка — отнюдь нет. Впрочем, метапрограммирование — штука для тех еще месье и на практике требуется в исчезающе малой доли процента случаев, да и там при желании можно обойтись автогенерацией кода.
  • Сборщик мусора — в силу своей тупоголовости на машинах с недостаточным количеством этого вашего ОЗУ может запросто начать собирать мусор в swap-е, вызывая эпические тормоза и задержки; в системах с многими гигабайтами ОЗУ в силу изначальной дефективности модели сборки мусора — также может на несколько десятков (!) секунд отправить весь сервер в задумчивость, собирая мусор среди миллиардов объектов, тем самым вызывая вполне себе неиллюзорные отказы запросов клиентов по time-out; этот же сборщик мусора эпизодически вызывает весьма неиллюзорные залипания на несколько секунд сред Eclipse, Idea, Netbeans (все останавливается, код набивать нельзя, пока мусор не соберется), правда Java кодерам пофиг, они привыкли. Этот же гребанный сборщик мусора является причиной «залипания» и «дерганности» интерфеса Android: так как в андроиде всё, что отображается и нажимается на экране — всё пропускается через Java-код, то внезапные активации сборщика мусора могут запросто приостановить на доли секунд обработку действий пользователя и отображение результатов. Справедливости ради стоит отметить, что этой фигней со сборщиком мусора страдают и PHP, и Python/Ruby, только там эта проблема обычно стоит не так остро, т.к. большинство web-приложений на этих языках работает не в режиме демона.
  • Необходимость тонкой оптимизации, — как кода, так и настроек JVM, — для получения приличной скорости работы. Да, она возможна, но часто нетривиальна (флажки вроде «noasyncgc») и нереальна (см. выше про гигабайты heap).

Ещё не слишком обоснованные претензии:

  • Жаба тормозит (анонимусом проверено, что зачастую over 9000 денег и целого кластера буржуйских серверов не хватает, чтобы стабильно держать несколько серверных приложений).
  • Жаба хавает 9000+ МБайт памяти (волшебная JIT компиляция и утечки. Решается это просто: одна программа на Жабе — один сервер, но, будем честными с собой, в особых положениях планет солнечной системы, космическое излучение может парализовать всю работу).
  • Жаба кривая (особенно когда её держат кривые руки).
  • Жаба убогая. Да, в ней действительно нет прямой работы с памятью, что можно обойти через sun.misc.Unsafe, но это не соответствует идеологии языка.
  • Жаба ооочень медленно развивается, в то время как другие языки обрастают новыми фичами чуть ли не каждый день.

Алсо, Oracle, нынешний владелец торговой марки, делает только то, что Он хочет. Было бы странно верить в то, что Он любит всех нас и всех нас спасёт. С другой стороны, у все того же Oracle куча проектов на жабе, так что корпорация кровно заинтересована в дальнейшем развитии языка. Истина, как всегда.

О недостатках есть длинная бумага, которую никто не будет читать. А по мнению Никлауса Вирта, Java и JVM были слизаны с его языка Oberon чуть менее чем полностью и испорчены сишным синтаксисом. Честно говоря, у Вирта есть на то основания, но разве это кого-то волнует?…

[править] Связанные мемы

В основном мемы связаны со скоростью работы программ на Java, а также с тем, что это индустриальный язык программирования, точнее, с тем, что на нём пишет большое количество быдлокодеров-индусов. Типичные примеры:

  • «Жаба не тормозит», «просто gc невовремя запустился», «просто всё остальное слишком быстро работает» и тому подобное по вкусу.
  • «Томми», «убей себя как Томми». Применяется в основном к быдложабокодерам. Мем, связанный с тем, что на одной из гонок машин-роботов имени DARPA одна из машин по имени Томми, софт для которой был написан на Java, сошла с дистанции в связи со сбоем чипа, что привело к её убиению об стену. Что интересно, неполадка была чисто аппаратной, но всем похуй, а попадание Томми в стену фанатами Java было радостно воспринято со словами: «Вот видите, Java не тормозит!»
  • «Закат солнца вручную». Впарить корпоративному клиенту всемогущее, хоть и монстроватое поделие, которое вскоре потребует фирменного «железа» — очень мудрая стратегия, да.
  • «Серверная Java». Используется при обсуждении любых проблем с производительностью жабоприложений. Заявляется, что проблема в десктопной жабе, а с серверными настройками все нормально.

[править] Не тормозит


Java is high performance. By high performance we mean adequate. By adequate we mean slow.

В холиварах жабакодеры любят приводить в качестве главного аргумента того, что жаба не тормозит, линки на бенчмарки, в которых сравнивается скорость нативного кода и байт-кода. Обычно заголовки таких тестов выглядят в духе «Жаба обгоняет по производительности С++» и приводится код, где Java быстрее C/C++ (нужное подчеркнуть). Это происходит по нескольким причинам:

  1. Такие бенчмарки примитивны: они работают только со скалярными типами (с такими, как int, long, double), память под которые выделяется на стеке, используются простые операции и, наконец, весь код находится только в одном классе.
  2. Обычно такие тесты содержат много циклов, а замеры проводятся не во время первого запуска, а когда JIT-компилятор уже подставил на место байт-кода команды процессора. Значит, меряется скорость хорошо оптимизированного динамически скомпилированного кода. Да и garbage collector не успевает запуститься в таких программах.
  3. Авторы бенчмарков зачастую поверхностно знают С/C++ и используют его в «жабьем» стиле, например используя кучу там, где она не нужна и ноют про необходимость free и delete.

В реальных же приложениях ситуация с быстродействием несколько иная:

  1. Все объекты ссылочные и хранятся в куче, а значит, самые элементарные составные типы, такие, как Point, будут каждый раз создаваться в ней. А таких объектов сотни тысяч. Даже при хорошей оптимизации кучи, она медленнее стека, а стоимость алгоритма сборки мусора в общем случае — O(n²), то есть с ростом числа объектов в памяти затраты на сбор мусора растут нелинейно.
  2. Большое потребление памяти: один только хелловорлд при запуске отъедает 10 МБайт (они забираются виртуальной машиной на внутренние нужды), а так как с каждым объектом связаны метаданные (хотя бы ссылка на таблицу методов класса), то какой-нибудь массив объектов класса RGBColor будет занимать в 4-5 раз больше памяти, чем даже хотя бы в C# [1] . На венде, которая свопит всё подряд, это приводит к известным результатам [2] .
  3. Динамическая природа языка: динамическое связывание кода подразумевает постоянные проверки во время каждого приведения типов (например, при работе с коллекциями). Обобщения здесь не дают ничего, так как они были введены в язык позже, и, для сохранения обратной совместимости, действуют только на уровне исходного кода, в байт-коде их нет.
  4. Бросание исключений на каждый мелкий чих, чего язык не требует, но требует идеология и сторонние библиотеки, а выполняются исключения исключительно медленно. Оборотной стороной повышенной надежности является то, что многие программы в процессе работы генерируют over 9000 исключений [3] , а исправлять подобный код разработчики не спешат.
  5. Другая семантика языка: во избежание воспаления мозга от i++ + i++ были введены правила, устраняющие неоднозначности, что мешает компилятору заниматься оптимизацией. Так, например, на Java оператор вида x+=foo(); в отличие от Си требует запоминания предыдущего значения x перед вызовом foo() [4] .
  6. Garbage collector: хотя он проходится по памяти нечасто, на GUI приложениях это ощутимо. Алсо, для серверных приложений с огромным хипом (овер 100G) настройка GC при помощи свичей превращается в ад. И всё равно он тупит.
  7. Неотключаемая, если не заниматься извращениями вроде GCJ, проверка индексов при обращении к массивам.

С другой стороны, многие серверные приложения активно используют базы данных, так что большую часть времени занимает обработка запросов на стороне этой базы, так что хоть на чём пиши — разница в скорости будет небольшая. Более того, сервлет на Java может запросто обогнать какой-нибудь кривой cgi-скрипт на Си, потому что сервлету надо инициализироваться один раз, а cgi-скрипту может понадобится стучаться на базу при ответе на каждый запрос для получения каких-нибудь там настроек. Но к энтерпрайзу это как раз и не относится: потянуть тысячи объектов из базы через ORM, чтобы посчитать что-нибудь в цикле — это для них обычное дело.

Не стоит также забывать, что различных реализаций JVM много. Некоторые из них имеют узкую специализацию и умеют очень многое. Например, широко известный в узких кругах Zing от Azul позволяет в 1000+ раз уменьшить время (как единовременное, так и суммарное) блокировок мира на gc, ценой всего-навсего утроенного расхода памяти (минимум для работы 32 Гб), минимум 6 процессоров с поддержкой Intel-VT или AMD-V (только современные серверные модели) и примерно 14000 долларов за годовую лицензию на 1 сервер. Работает только под Linux и требует установки собственного модуля для ядра.

Многие причины тормозов жабы идут от работы с либами, ибо солнцевские пытаются обеспечить универсальность и безопасную среду выполнения. Ситуация усугубляется тем, что в стандартном наборе обычно имеется минимум две, а всяческих коллекций и вовсе зоопарк, библиотеки с одинаковой функциональностью, но различными механизмами. И если взять не ту, возможен былинный отказ. А выбирают их, как правило, «по указаниям технического директора», то есть — наугад.

Алсо, в соревнованиях по программированию алгоритмического толка ACM, авторы задач просто вынуждены удваивать/утраивать ограничение по времени или вовсе устанавливать специальные ограничения для жабы, чтобы каждый жабакодер мог сдать задачу. Для одинаковых лимитов времени часто бывало, что кривой алгоритм на жабе, не укладывающийся в рамки времени и/или памяти, будучи переписанным один-в-один на си, проходил. Обратные случаи неизвестны. Также, в ACM’е каждый жабокодер вынужден писать свой класс для чтения чисел из файла, так как поставляемый со стандартной библиотекой работает слишком медленно.

[править] Выполнение в мире существительных

Энтырпрайз-говно не проектируется, а лепится студентотой по хуй-знает-кем-написанным техзаданиям, с целью быть выкинутым и переписанным через пол года после сдачи (хотя сдача энтырпрайз-говна происходит чисто номинально, конечно же). Для джавы это нормально, поэтому мы и не увидим ни одной годной программы, написанной на джаве.

Когда язык разрабатывался, простота кода была одним из самых важных его параметров и это легко увидеть! Например, так Ъ жабакодеры пишут факториал:

Java против C#: какой язык производительнее в реальных проектах?

Сегодня мы познакомимся с одним опытом, при помощи которого усердный автор, Джефф Когсвелл, попытался рассмотреть производительность кода Java и C# «в реальных условиях». Этот опыт можно считать приглашением к анализу производительности двух языков, ведь полученные автором результаты не претендуют на абсолютную объективность. Но, учитывая соперничество и популярность затронутых в статье языков, надеемся, что материал станет для читателя вкусной пищей для размышлений.

Автор подготовил специальные тесты и решил проверить, какой из этих языков окажется лучше «в реальных условиях»

Давайте сравним Java и C# — два языка программирования, каждый из которых имеет широкий круг поклонников, но вместе с тем и яростных противников. Несмотря на все те прения, которые разворачиваются в онлайне, сложно найти четкие критерии, позволяющие оценить реальную производительность каждого из этих языков.

Какой смысл я вкладываю в понятие «реальный»? Дело в том, что меня не интересует еще один тест, скрупулезно вычисляющий значение числа Пи до миллиона знаков. Я хочу поговорить о решении прикладных задач на языке: какой язык мне выбрать, если требуется ежедневно выдавать клиентам миллионы веб-страниц? Какой из языков сильнее при выборке информации из базы данных и динамической сборке веб-страниц? Именно такая статистика обычно интересует технического специалиста, выбирающего подходящую платформу.

Прежде чем перейти к тестам, давайте определимся с терминологией. Когда вы пишете код Java, вы обычно планируете использовать его на виртуальной машине Java (JVM). Иными словами, ваш код компилируется в байт-код, а этот байт-код работает под управлением JVM. C#, в свою очередь, обычно работает в общеязыковой исполняющей среде (CLR) от Microsoft. C#, как и Java, компилируется в байт-код.

Java и C# — это просто языки. Теоретически вы могли бы писать код Java для исполняющей среды Microsoft CLR, а также код C# для JVM. Действительно, на работу с виртуальной машиной Java ориентирован и ряд других языков, в частности Erlang, Python и др. Самые распространенные языки, рассчитанные на работу с CLR (кроме C#), — собственный язык Microsoft Visual Basic.NET, а также майкрософтовская разновидность C++, называемая C++.NET. Общеязыковая исполняющая среда также поддерживает некоторые менее распространенные языки — уже упомянутый выше Python и F#.

Две эти исполняющие среды содержат фреймворки, представляющие собой наборы классов. Такие наборы для JVM были написаны в Oracle/Sun, а для CLR — в Microsoft. У Oracle есть платформа Java с разнообразными API. Фреймворк Microsoft .NET — это огромный набор классов, обеспечивающих разработку для CLR. На самом деле, многие специалисты называют всю систему просто .NET, а не CLR.

Мы не будем сравнивать языки как таковые. Нас интересуют лежащие в их основе исполняющие среды. Но мы заинтересованы не только и не столько быстродействием этих сред, сколько производительностью фреймворков. Поэтому я выполню несколько сравнений, но в итоге нам все равно придется соотнести яблоки с яблоками.

Например, вы вполне можете написать собственный HTTP-слушатель на C# или Java, а потом просто отправить клиенту динамически сгенерированную HTML-страницу. Но на практике почти никто не пишет низкоуровневых HTTP-слушателей; обычно мы стремимся использовать имеющиеся HTTP-серверы. Большинство веб-приложений на C# работают на базе майкрософтовского сервера IIS.

C другой стороны, серверный код на Java может работать с несколькими разными серверами, в частности Apache HTTP и Tomcat. Кстати, сервер Tomcat был специально разработан для взаимодействия с серверным кодом Java. Мы, конечно, хотели бы сравнивать сопоставимые величины, но в то же время должны сохранить реалистичность эксперимента. Скорее всего, отклик будет зависеть от сервера, а одни серверы работают быстрее других. Хотя HTTP-серверы технически и не входят в состав исполняющей среды, они применяются практически всегда, поэтому их производительность нельзя не учитывать. В первом тесте мы обойдемся без этих стандартных инструментов, а напишем собственные небольшие HTTP-серверы. Во втором случае мы опробуем подобные тесты с аналогичными HTTP-серверами, чтобы получить более точную и полную картину.

Еще одна проблема — это статические файлы, я собираюсь обойтись в данном опыте без них. Некоторые читатели могут со мной не согласиться, но при современных архитектурах, если вам требуется высокая скорость работы со статическими файлами, написанными на JavaScript или CSS, их просто можно загрузить на облачный сервер, данные которого тиражируются во всей стране. Далее по DNS-конфигурации определяем, какой сервер расположен ближе всего к клиенту, и отсылаем данные весьма быстро. Именно поэтому я собираюсь пропустить эту часть. Вдобавок, если вас интересует максимальная производительность, вы не будете нагружать ваше веб-приложение выдачей статических файлов, а постараетесь выполнять в нем только самую необходимую работу: считывание баз данных, сборка динамического контента и т. п.

Замечание об аппаратном обеспечении

Я хочу гарантировать, что применяемое в тестах оборудование привносит в опыт минимальное количество посторонних переменных факторов. На той машине, где я занимаюсь разработкой, стоит масса дополнительных программ, в частности многочисленные сервисы, которые запускаются автоматически и отхватывают процессорное время. В идеале следовало бы выделить под процесс Java или C# целое процессорное ядро, но, к сожалению, выделение ядер происходит иначе. Вы можете ограничить зону действия процесса одним ядром, но не можете «не допустить» в это ядро другие процессы. Поэтому я выделяю для опыта крупные серверы на Amazon EC2, системы которых можно считать базовыми. Поскольку здесь мы не собираемся сравнивать Linux и Windows, а C# ориентирован преимущественно на Windows (если не учитывать проект Mono, который мы и не будем учитывать), все тесты будут выполнены в Windows.

С клиентской стороны требовалось обеспечить, чтобы результаты не искажались из-за сетевых задержек. Единственная задержка могла испортить итоги всего теста. Поэтому я решил исполнять клиентский и серверный код на одной и той же машине. Итак, я не могу «заставить» операционную систему выделять по ядру на процесс, но могу заключить каждый процесс в своем ядре, что я и сделал.

Сбор результатов

Результаты хронометрировались на клиентской стороне. Делая это, лучше всего отследить время и сохранить его, потом по мере необходимости снова отмечать время, и так до конца теста. Лишь по окончании работы выполняются все подсчеты. Кроме того, я не выводил на консоль никаких данных до полного завершения теста. Распространенная ошибка, которую совершают в подобных тестах: в определенных точках отбираются значения времени, в каждой точке подсчитывается разница во времени, после чего результаты выводятся на консоль. Консоли работают медленно, особенно если их требуется прокручивать. Итак, работаем до конца, а потом вычисляем разностные значения и выводим данные на экран.

Клиентский код

Фактически неважно, какой код мы используем в качестве клиентского — важно последовательно задействовать его во всех тестах. Клиентский код будет имитировать работу браузера и измерять, сколько времени требуется на доставку страницы с сервера. Для этого можно использовать C# или Java. Я остановился на C#, поскольку в нем есть очень простой класс WebClient и несложный класс-таймер.

Первый тест: слушание HTTP

Начнем. Мы протестируем код, который просто открывает HTTP-слушатель и рассылает динамически сгенерированные веб-страницы.

Сначала попробуем Java. Мы можем реализовать описанную задачу несколькими способами, но я хотел бы обратить внимание на два подхода. Во-первых, попробуем открыть слушатель TCP/IP на порте 80 и дождаться входящих соединений. Это очень низкоуровневый метод, при котором мы будем пользоваться классом Socket. Другой интересующий нас вариант — использование класса HttpServer. Вот почему я собираюсь воспользоваться этим классом: если мы действительно хотим сравнить скорость Java и C#, без участия Веба, то можно применить некоторые базовые индикаторы, не связанные с работой в Интернете. Так, можно написать два консольных приложения, которые будут оперировать подборкой математических уравнений и, возможно, также выполнять кое-какой строковый поиск и конкатенацию — но это уже другая история. Здесь нас интересует Веб, поэтому займемся HttpServer и его эквивалентом на C#.

Сразу же я обнаружил одну аномалию: выполнение любого запроса в Java-версии длится почти в 2000 раз больше. На обработку 5 запросов при получении строки из CLR-программы, использующей класс HttpListener, ушло около 17 615 тактов процессора, а на 5 аналогичных запросов с применением сервера Java и класса HttpListener было израсходовано 7 882 975 тактов. Если выразить это соотношение в миллисекундах, то имеем 2 миллисекунды на 15 запросов на сервере C# и 4045 миллисекунд на сервере Java.

Добавив на сервер Java некоторую отладочную информацию, я выяснил, что сама функция, занятая ответами на входящие запросы и рассылкой данных, работает довольно быстро — ни разу не приближаясь даже к показателю в 3 секунды. Вероятно, узкое место расположено где-то во фреймворке Java, при отправке данных обратно к клиенту. Но такая проблема отсутствует при обмене информацией с клиентом C#.


Чтобы докопаться до сути проблемы, я решил перейти на другой Java-клиент. Отказался от сравнительно тяжеловесного класса HttpServer, а взамен создал простой сокет, слушающий TCP/IP — для этого воспользовался классом ServerSocket. Вручную создал строку заголовка и основной текст, совпадающий с отправленным в версию на C#.

Ситуация значительно улучшилась. Могу запускать множество тестов; выполняю 2000 запросов один за другим, но не собираю данных о времени, пока не завершатся все 2000 вызовов к серверу Java. Потом осуществляю аналогичный процесс с сервером C#. В данном случае время измеряется в миллисекундах. На 2000 запросов к серверу Java уходит 2687 миллисекунд. На 2000 запросов к серверу на C# тратится 214 миллисекунд. C# по-прежнему гораздо быстрее.

Поскольку сохраняется такая значительная разница, мне ничего не оставалось, кроме как испробовать версию Java на сервере Linux. Я воспользовался сервером «c1.medium» на Amazon EC2. Установил оба упомянутых класса Java и получил фактически такие же скорости. Класс HttpServer тратит около 14 секунд на обработку 15 запросов. Плоховато.

И, наконец, чтобы быть абсолютно уверенным, я написал на Java эквивалентную клиентскую программу, получающую данные. Временные показатели существенно не изменились.

Второй тест: полнофункциональный сайт

Как я уже указывал, мы редко пользуемся самодельными HTTP-серверами. Программисты, работающие с C#, обычно прибегают к IIS. У приверженцев Java есть несколько вариантов, в частности, Tomcat. В моих тестах я использовал именно эти два сервера. В варианте с C# я задействовал платформу ASP.NET MVC 4, работающую на IIS 8. Применил два метода: в первом случае возвращал HTML-строку от самого контроллера; во втором — возвращал представление, содержащее справку даты/времени.

В тестах Java можно применять два похожих метода. Можно работать с сервлетом, возвращающим информацию на HTML, либо возвращать результаты на странице JSP. Эти методы аналогичны приемам C#, в первом из которых задействуется контроллер, а во втором — представление. Можно было бы применить более новые Java Faces или любые другие фреймворки; оставляю эти задачи всем заинтересованным для самостоятельного изучения.

Контроллер C# просто возвращает HTML-строку. При прогоне моего клиентского теста с 2000 итераций на него уходит 991 миллисекунда. Опять же, гораздо быстрее, чем версия с сокетом Java.

Та версия приложения C#, которая работает с представлением, создает полнофункциональную HTML-страницу, соответствующую всем стандартам. Здесь есть элементы HTML, head, meta, title, body и внутренний элемент div, содержащий текст «The date and time is» с указанием даты и времени. Дату и время мы получаем в экземпляре DateTime.Now, динамически записывая эту информацию при каждом запросе.

Прогон клиентского теста (2000 итераций) в такой версии с представлением занимает 1804 миллисекунды; примерно вдвое дольше, чем напрямую. Напрямую мы возвращаем более краткий HTML, но если увеличить HTML до размеров, сопоставимых с вариантом-представлением, разница практически отсутствует; длительность колеблется в пределах 950—1000 миллисекунд. Даже при добавлении динамической записи даты и времени процесс существенно не замедляется. В любых условиях версия с представлением выполняется примерно вдвое дольше, чем версия с контроллером.

Перейдем к Java. Сервлет не сложнее, чем контроллер C#. Он просто возвращает строку, содержащую HTML-страницу. На возврат 2000 экземпляров уходит 479 миллисекунд. Это примерно вдвое быстрее, чем с контроллером C# — действительно впечатляет.

Возврат JSP-страницы также происходит очень быстро. Как и в случае с C#, второй вариант протекает дольше первого. В данном случае на возврат 2000 экземпляров расходуется 753 миллисекунды. Если добавить к JSP-файлу вызов, возвращающий дату, заметной разницы не возникает. На самом деле, на сервере Tomcat явно выполняется какая-то оптимизация, так как в последующих попытках на возврат 2000 экземпляров тратится уже 205 миллисекунд.

Заключение

Эти результаты кажутся мне довольно интересными. Я много лет профессионально занимался программированием на C#, мне неоднократно говорили, ссылаясь на личный опыт, что .NET — одна из самых быстрых существующих сред исполнения. Но эти тесты свидетельствуют об обратном. Разумеется, они минимальны; я не делал никаких крупных вычислений, активных запросов к базе данных. Возможно, я еще проведу тесты с базой данных и уточню результаты. Но в моем опыте Java побеждает с явным преимуществом.

H Сравнение производительности языков программирования в черновиках Из песочницы

Построим график по полученным результатам:

Вывод:

комментарии ( 10 )

А как же параллельные вычисления в С#?)

Значит, говорите, что вы Наполеон для С++ вы использовали компилятор .net 4.5.1? Чудно

C# разрабатывался как язык программирования прикладного уровня для CLR и, как таковой, зависит, прежде всего, от возможностей самой CLR. Это касается, прежде всего, системы типов C#, которая отражает BCL. Присутствие или отсутствие тех или иных выразительных особенностей языка диктуется тем, может ли конкретная языковая особенность быть транслирована в соответствующие конструкции CLR. Так, с развитием CLR от версии 1.1 к 2.0 значительно обогатился и сам C#; подобного взаимодействия следует ожидать и в дальнейшем. (Однако, эта закономерность была нарушена с выходом C# 3.0, представляющего собой расширения языка, не опирающиеся на расширения платформы .NET.) CLR предоставляет C#, как и всем другим .NET-ориентированным языкам, многие возможности, которых лишены «классические» языки программирования. Например, сборка мусора не реализована в самом C#, а производится CLR для программ, написанных на C# точно так же, как это делается для программ на VB.NET, J# и др.

Странное сравнение, исходники совершенно разные, хотя бы тот факт, что StringTokenizer работает совершенно не так, как Split вас не смутил? Еще вы взяли буферизованный ридер для джавы, а для C# буферизацию не настроили.

Сравнивалось только время сортировки, время парсинга строк не учитывалось.

Часть 2. Увеличение скорости

Серия контента:

Этот контент является частью # из серии # статей: Оптимизация производительности Java в AIX

Этот контент является частью серии: Оптимизация производительности Java в AIX

Следите за выходом новых статей этой серии.

Это вторая статья в серии из пяти статей о настройке производительности Java на AIX. Перед прочтением этой статьи настоятельно рекомендуется ознакомиться с первой частью, если по каким-то причинам этого не было сделано.

Эта статья рассматривает способы максимально повысить скорость выполнения приложений и пропускную способность системы. Для приложений с интерфейсом пользователя также объясняется, как гарантировать приемлемую скорость отклика системы.

В первом разделе даются общие советы, которые можно применять в различных ситуациях. Также представлены ссылки на инструменты, которые могут использоваться при определении/изучении узких мест, связанных с CPU (центральным процессором). В следующем разделе описаны различные типы приложений и советы по их настройке с учетом специфики приложения. В третьем разделе даются различные советы. Статья заканчивается обзором следующей статьи серии.

CPU как узкое место системы

В этой статье объясняется, как можно повысить скорость работы приложения, в том числе скорость реакции на запросы к нему.

Чтобы определить, работает ли приложение медленно, достаточно сравнить реальные и ожидаемые показатели производительности. Аналогично, интерфейс пользователя в приложении может периодически «зависать» или сетевые подключения могут разрываться по тайм-ауту из-за загруженности приложения. Использование команд topas или tprof покажет, используется ли CPU на 100% или нет. Необходимо отличать нештатные ситуации от ситуаций с недостаточным масштабированием (если требуются более быстрые процессоры или больше процессоров, то эту проблему нельзя решить изменением параметров настройки).

Сначала необходимо с помощью topas или другого аналогичного инструмента посмотреть, является ли Java основным пользователем CPU. Если видно, что Java находится внизу списка пользователей CPU, то детальная настройка процессора вряд ли поможет (краткий обзор topas приведен в первой статье серии).

Идеальный случай, когда приложение использует CPU на 90% или больше. Если достигнуто такое состояние, а пропускная способность остается неудовлетворительной, то причина проблемы — недостаточная мощность системы. Если используется DLPAR, можно попробовать добавить еще один или два CPU и замерить прирост производительности.

В остальной части раздела представлен краткий обзор стандартных инструментов и того, как распознавать проблемы, связанные с Java. Подробная информация приведена в документах AIX 5L Performance Tools Handbook и Understanding IBM eServer pSeries Performance and Sizing.

vmstat


vmstat может использоваться для сбора различной статистки по системе. Для работы с CPU используется следующая команда:

В результате сбор статистики будет производиться 3 раза с интервалом в 1 секунду и с указанием временных меток. Параметры можно настроить, как того требует ситуация. Результат работы инструмента показан ниже:

Интересующая нас информация в этом отчете:

  • Значения столбцов r (очередь запуска) и b (блокировано) начинают возрастать, особенно после 10. Это обычно означает, что имеется слишком много процессов, конкурирующих за CPU.
  • Если значение cs (переключения контекста) слишком большое по сравнению с числом процессов, то, возможно, систему надо настроить с помощью vmtune . Данная тема выходит за рамки этой серии статей.
  • В разделе cpu значение us (время пользователя) означает время, использованное программами. Если Java находится вверху списка tprof , тогда необходимо настроить Java-приложение.
  • Если в разделе cpu значение sys (время системы) выше, чем ожидалось и при этом все равно имеется оставшееся id время (время простоя), то это может означать конфликт блокировок. Необходимо проверить через tprof методы, связанные с блокировками, во время работы ядра. Можно попробовать запустить несколько экземпляров JVM. Также можно поискать взаимоблокировки в файле javacore .
  • В разделе cpu, если значение wa (ожидание ввода/вывода) высокое, это может означать, что узким местом является диск, и следует использовать iostat и другие инструменты для изучения использования диска.
  • Ненулевые значения в столбцах pi, po (страницы ввода/вывода) означают, что наблюдается подкачка страниц и необходима дополнительная память. Также может оказаться, что размер стека был установлен слишком большим для некоторых экземпляров JVM. Также это может означать, что была выделена «куча» большего размера, чем количество памяти в системе. Конечно, другие приложения также могут использовать память, или же файлы подкачки занимают слишком много места в памяти.

iostat

Можно использовать iostat , чтобы получить такую же информацию о CPU, как и от vmstat , вместе со статистикой дискового ввода/вывода.

ps — это очень гибкий инструмент для идентификации программ, запущенных на системе и используемых ими ресурсов. Он отображает такую статистику и информацию о статусе процессов в системе, как идентификатор процесса или потока, активность ввода/вывода, использование CPU и памяти.

Эта команда позволить найти идентификаторы (ID) всех активных Java-процессов. Многие другие команды потребуют сначала найти ID процесса, использование -ef поможет выделить отличия в нескольких Java-процессах, показав аргументы командной строки, использовавшиеся для их запуска.

Используя PID (ID процесса) интересующего нас Java-процесса, можно проверить, сколько было создано потоков. Это особенно важно в случае, когда требуется выполнить мониторинг крупного приложения, так как можно перенаправить вывод этой команды в команду wc -l для получения числа потоков, созданных JVM. Это может выполняться циклически, так что можно определить, какие потоки запускаются или умирают в неположенное время.

Полезно получить значения %CPU%Memory, отсортированные по самым активным пользователям. Это полезно для быстрого поиска узких мест в системе.

Показывает использование виртуальной памяти. Стоит отметить, что предпочтительнее выполнять мониторинг платформенно-зависимой и Java-«кучи» через svmon . Подробнее это объясняется в третьей статье этой серии.

Используя PID (ID процесса), можно получить информацию о настройках среды процесса. Например, показать полный файловый путь выполняемого Java-приложения, который может не показываться в обычном выводе команды ps. Отметим, что для того чтобы получить полную информацию о среде, рекомендуется создать файл javadump (дамп Java) (см. статью IBM developer kits — diagnosis documentation).

sar -u -P ALL x y можно использовать для проверки баланса использования CPU при использовании нескольких центральных процессоров. Если распределение не сбалансировано, это может означать, что приложение однопоточное и, возможно, требуется запустить несколько экземпляров приложения. В примере, приведенном ниже, выполняется сбор статистики 2 раза с интервалом в 5 секунд на двухпроцессорной системе, которая используется на 80%.

Можно видеть, что все CPU используются на 100% (когда выполняются продолжительные benchmark-циклы), или только один CPU используется на 100% (когда JVM выполняет сжатие памяти). Это значит, что необходимо выполнить настройку с помощью verbosegc ; см. статью Fine-tuning Java garbage collection performance.

tprof

tprof — это один из традиционных инструментов AIX, предоставляющий детальную информацию по профилю использования CPU для каждого AIX-процесса по имени или идентификатору. Он был полностью переписан в AIX 5.2, а в примере, приведенном ниже, используется синтаксис AIX 5.1 syntax. С новым синтаксисом можно ознакомиться в статье AIX 5.2 Performance Tools update: Part 3.

Самый простой способ вызвать эту команду:

Через 10 секунд будет создан новый файл __prof.all , содержащий информацию о том, какие команды используют CPU-системы. Если отсортировать данные по FREQ (частота), то информацию можно представить следующим образом:

Этот пример показывает, что половина времени CPU связана с приложением Oracle, и Java использует около 3970/19577 или 1/5 ресурсов CPU. Значение в ряду wait обычно означает время простоя, но может также включать долю использования CPU, затраченного на ожидание ввода/вывода.

Чтобы увидеть, есть ли в системе конфликт блокировок, необходимо проверить раздел KERNEL (ядро):

В разделе Shared Objects (общие объекты) необходимо обратить внимание на записи с libjvm.a и особенно на записи с именем gc_* или именами, похожими на названия этапов деятельности (Mark, Sweep, Compact). Если таких записей много, то, возможно, необходимо выполнить настройку GC (сборщика мусора) для процесса JVM.

Также нужно обращать внимание на важные процедуры, которые потребляют большое число тактов (Ticks) процессора. Например, один из отчетов tprof показывает, что значение для метода clProgramCounter2Method достаточно высокое:

После исследования нескольких таких примеров было установлено, что удаление вызовов метода Throwable.printStackTrace значительно улучшает производительность. Расследование, которое привело к этому методу, было начато с анализа информации, полученной от tprof .

Советы, относящиеся к Java

В большинстве случаев (см. советы для исключительных ситуаций) JIT-компилятор должен быть включен, так как это приводит к разнице в производительности, сравнимой с разницей между исполнением Java-байт-кода и платформенно-зависимого кода. JIT-компилятор может увеличить производительность в 25 раз, так что это жизненно важный компонент для производительности Java.

Garbage Collection (сборщик мусора) — это еще один жизненно важный для производительности компонент, так что он должен быть исследован и настроен при необходимости. Отметим, что хотя включение трассировки GC (с помощью параметра -verbosegc ) оказывает небольшое негативное влияние, преимущество возможности мониторинга и анализа Java-«кучи» превышает отрицательный эффект. Если взглянуть с другой стороны, то правильно функционирующая, здоровая «куча» минимизирует количество информации, выводимой через -verbosegc , так что с настройкой «кучи» можно заодно минимизировать затраты на дополнительную трассировку.

Советы по настройке, основанные на специфике приложения

Далее будут рассмотрены различные характеристики типичных приложений. Следует определить специфику приложения исходя из его архитектуры или путем его анализа, и затем применить соответствующие советы.

Длительность работы приложения

Реализация Java от IBM спроектирована так, чтобы обеспечить лучшие характеристики для приложений, рассчитанных на работу в течение длительного времени, например, серверного кода. Если по какой-то причине запускается сценарий тестирования, длящийся 5 минут или меньше, то можно обнаружить, что приготовления, выполняемые Java от IBM для подготовки к длительной работе, влияют на время запуска. Стоит посмотреть на советы CPU001: быстрый запуск приложения и CPU004: полное освобождение от GC, если быстрый запуск для приложения важнее, чем длительная работа. Если совет CPU004: полное освобождение от GC не помогает, вместо него можно попробовать использовать совет CPU012: как избежать изменения размеров «кучи». В сложных случаях можно попробовать выполнить тестирование с отключенным JIT, если сценарий тестирования настолько короткий, что даже инициализация JIT оказывается затратной. Стоит отметить, что отключение JIT до сих пор не упоминалось в качестве самостоятельного совета по настройке производительности, так как в предыдущей статье говорилось, что это, возможно, самая худшая вещь для производительности приложения.

Если приложение может позволить себе небольшую задержку во время запуска, стоит изучить советы CPU003: скомпилировать все при первой возможности и CPU008: использование небольшой «кучи». Для долго работающих приложений, которые имеют четко выделенные фазы «инициализация» и «работа», совет CPU003: скомпилировать все при первой возможности очень полезен.

Уровень взаимодействия с пользователем

В зависимости от того, насколько интенсивны вычисления, выполняемые в коде, скорость отклика JVM может колебаться от критической до несущественной. Если настраиваемая JVM исполняет GUI-приложение, то длинные паузы, вызываемые GC, будут неприемлемыми. В то же время, если работают несколько экземпляров JVM, которые позволяют балансировать нагрузку, или выполняется обработка данных в пакетном режиме, то долгие паузы вполне допустимы.

Для приложений, у которых недопустимы длинные паузы в работе, подходят советы CPU002: использование параллельного GC, CPU004: полное освобождение от GC, CPU007: отключение явных вызовов метода System.gc() , CPU008: использование небольшой «кучи «, CPU009: устранение переполнения стека и CPU012: как избежать изменения размеров «кучи». Совет CPU004 в большинстве ситуаций можно применять только для недолго работающих приложений. Отметим, что совет CPU008 должен рассматриваться вместе с характеристиками приложения, связанными с памятью, иначе он может привести к обратному эффекту, если будет применен неправильно.

Для приложений, у которых допустимы длинные паузы в работе, можно применить совет CPU003: скомпилировать все при первой возможности. Стоит отметить, что наличие длинных пауз в работе — это плохой признак в большинстве случаев, так что стоит изучить и устранить проблему, потому что невозможно получить никаких преимуществ от ненастроенного экземпляра JVM.

Потребление ресурсов CPU


Если приложение запускается с числом потоков большим, чем число установленных CPU, и при этом считается нормальной ситуация, когда общее использование CPU находится на уровне 90% или выше, то любая фоновая обработка данных ухудшит пропускную способность приложения. С другой стороны, если приложение — это сервер, потоки которого «спят» большую часть времени и «просыпаются» только для обработки входящих запросов, то можно уменьшить эффект от длительных пауз на работу GC с помощью фоновой обработки.

Для приложений, которые интенсивно используют CPU и поэтому требуют минимизации фоновой обработки данных, стоит рассмотреть советы: CPU007 : отключение явных вызовов метода System.gc() , CPU008 : использование небольшой «кучи», CPU009 : устранение переполнения стека. Совет CPU008: использование небольшой «кучи» необходимо рассматривать одновременно с характеристиками памяти, как упоминалось ранее.

Для приложений, неинтенсивно использующих CPU, крайне рекомендуется совет CPU002: использование параллельного GC. Это позволит получить преимущества, снизив общее время пауз в приложении, когда выполняется рабочий цикл GC.

Правильно определенное местонахождение ссылок

Если в приложении имеются несколько методов, которые выполняются очень часто, а другие методы исполняются редко, то совет CPU003: скомпилировать все при первой возможности может оказаться очень полезным для улучшения производительности.

Степень параллелизма

Если приложение запускает несколько потоков для выполнения работы, то оно получит преимущество от системы, в которой имеется большое число CPU. Для динамических разделов (dynamic partition) добавление дополнительных CPU сразу же даст положительный эффект, так как Java-потоки могут быть немедленно запланированы на исполнение на вновь добавленных CPU. В советах CPU005: большое число потоков, CPU006: уменьшение конфликтов блокировок и CPU011: системы с более чем 24 CPU обсуждаются другие варианты оптимизации, которые можно попробовать.

Но если приложение имеет только один поток исполнения, то оно будет ограничено вычислительной мощностью единственного CPU. В этом случае можно попробовать использовать советы CPU002: использование параллельного GC и CPU010: системы с единственным CPU. Совет CPU010: системы с единственным CPU особенно полезен, если требуется запустить несколько экземпляров JVM в системе (например, в кластерной среде).

Стандартный набор советов

В тексте, приведенном ниже, имеются ссылки на аргументы командной строки для Java, указываемые перед именем класса/jar-файла и известные как «переключатели» (switches). Например, в строке java -mx2g hello есть единственный переключатель -mx2g .

Совет CPU001: быстрый запуск приложения

Нестандартный переключатель -Xquickstart может использоваться для сокращения времени запуска приложения. Этот переключатель снижает уровень JIT-оптимизаций до минимума и применяет их снова, только если подходящие методы снова становятся активно используемыми. В результате приложения, где логика исполнения не концентрируется в небольшом числе методов, запускаются намного быстрее.

Примечание: стоит отметить, что с точки зрения многоэтапного подхода к оптимизации этот переключатель может оказать неблагоприятное влияние на длительно работающие приложения.

Совет CPU002: Использование параллельного GC

Можно указать параллельную политику для сбора мусора (Concurrent Mark Garbage Collection Policy) для того, чтобы снизить число пауз, возникающих из-за рабочих циклов GC. Эта политика определяется с использованием переключателя -Xgcpolicy:optavgpause .

Примечание: в некоторых случаях приложения, интенсивно использующие ресурсы CPU, могут показать снижение пропускной способности при использовании параллельного режима.

Совет CPU003: скомпилировать все (или выбранные методы) при первой возможности

Переменная окружения IBM_MIXED_MODE_THRESHOLD может быть установлена в 0 для того чтобы отключить интерпретатор смешанного режима (mixed-mode interpreter). Это приведет к тому, что все методы будут откомпилированы JIT после того как их вызовут в первый раз. Необходимо добавить эту строку в настройки окружения или просто запустить ее перед запуском Java:

Также можно поэкспериментировать с ненулевыми значениями чтобы увидеть, какой конкретно порог MMI дает производительность лучше, чем у нулевого значения. Для AIX Java 1.3.1 использует 600 в качестве порогового значения, тогда как Java 1.4 использует значение больше 1000 (отметим, что эти значения имеют обыкновение меняться со временем). В статье IBM developer kits — diagnosis documentation , в главе «JIT Diagnostics», есть раздел «Selecting the MMI Threshold», в котором предоставляется дополнительная информация.

Если имеются только определенные классы, на которые необходимо воздействовать, то можно вместо этого использовать JITC_COMPILEOPT=FORCE(0) . Пример:

Этот пример компилирует все методы в пакете com.myapp.* при первой загрузке.

Этот пример компилирует все методы, называемые «uniqueName», при их первой загрузке.

Этот пример компилирует только этот конкретный метод при первой загрузке. Вместе с шаблоном * (используется как «0 или более символов») можно использовать шаблон ? для отдельных символов.

Множество классов и/или методов может быть определено с помощью следующего синтаксиса:

Важно четко отразить в документации, что это оптимизация, а не исправление ошибки!

Примечание: время запуска приложений может возрасти из-за этой настройки.

Совет CPU004: полное отключение GC

Стартовый и максимальный размеры «кучи» могут быть установлены в такие большие значения, что за время работы не произойдет ошибок во время выделения памяти. Обязательно необходимо включить verbosegc при подобном режиме работы, чтобы убедиться, что эта стратегия действительно работает!

Примечание: Когда происходит срабатывание GC, цикл его работы, скорее всего, займет много времени, так что этот совет можно использовать в очень редких случаях.

Совет CPU005: большое число потоков

Для масштабирования на большое число потоков стоит использовать переключатель -Xss для определения значения меньшего, чем по умолчанию (обычно 512 КБ, но может меняться в зависимости от версии Java). Это позволит выполнить масштабирование на большее число потоков, одновременно снизив количество платформенно-зависимой памяти, используемой приложением.

Примечание: Если размер стека слишком мал, может возникнуть исключительная ситуация Stack Overflow (переполнение стека).

Совет CPU006: уменьшение числа конфликтных блокировок

Можно попробовать запустить несколько экземпляров Java, если архитектура приложения позволяет это, чтобы уменьшить число конфликтных блокировок. Это поддерживается серверами приложений, позволяющими такой тип конфигурации, например, WebSphere позволяет использовать несколько экземпляров на одном компьютере.

Примечание: стоит отметить, что это может только скрыть проблему, так что стоит еще раз изучить фрагменты кода, которые приводят к чрезмерному числу конфликтных блокировок. Можно использовать tprof или средство профилирования для Java для поиска областей, которые следует изучить еще раз.

Совет CPU007: Отключение явных вызовов метода System.gc()

Используя нестандартный переключатель -Xdisableexplicitgc , можно избавиться от необходимости удалять вызовы метода System.gc() из кода. Удаление этих вызовов вернет управление GC обратно к JVM.

Примечание: Если вызов System.gc() необходим для функциональности, например, для кнопки на экране приложения, то это будет плохая идея, так как кнопка перестанет функционировать. Могут также быть другие обоснованные причины для присутствия вызовов System.gc() в коде.


Совет CPU008: использование «кучи» небольшого размера

Можно использовать «кучу» такого размера, что она никогда не потребует недопустимо много времени для уплотнения информации. Если по какой-то причине, приложение прекращает работу из-за необходимости длительного сжатия информации, то «куча» в 256 MБ потребует куда меньше времени для сжатия, чем «куча» в 1 ГБ.

Примечание: Если в результате уменьшения размера «кучи» потребуется больше сжатий, то оптимизация сработает в противоположную сторону. Этот совет можно использовать только в ситуациях, когда создается много временных объектов.

Совет CPU009: устранение переполнения стека

Если в журналах verbosegc присутствуют сообщения Mark Stack Overflow (зафиксировано переполнение стека), стоит уменьшить число объектов, хранящихся в «куче», чтобы эти сообщения исчезли. Новые сборки Java обладают улучшенной обработкой ситуаций с MSO. Эта возможность была добавлена, так как проблема MSO может серьезно повредить производительности приложения и должна рассматриваться как дефект, а не оптимизация.

Совет CPU010: системы с единственным CPU

Можно использовать команду bindprocessor , чтобы привязать Java-процесс к определенному процессору. Этот вариант можно рассмотреть, чтобы избежать ситуации, когда несколько экземпляров JVM конкурируют за ресурсы CPU. Также можно установить переключатель -Xgcthreads0 если система не однопроцессорная.

Если приложение работает на 1-CPU LPAR, который нельзя переконфигурировать, чтобы динамически добавить еще несколько CPU, можно также экспортировать переменную окружения NO_LPAR_RECONFIGURATION=1 для получения лучшей производительности в определенных случаях.

Примечание: Лучшие возможности Java для производительности отключаются, когда ее заставляют работать в однопроцессорной конфигурации. Переменная окружения NO_LPAR_RECONFIGURATION также отключит динамическую конфигурируемость Java для адаптации к DLPAR, так что ее следует использовать с осторожностью.

Совет CPU011: системы более чем с 24 CPU

Для систем, у которых от 24 до 32 процессоров, стоит протестировать переключатель -Xgcpolicy:subpool , так как эта политика GC настроена для обеспечения лучшей производительности для крупных конфигураций.

Совет CPU012: отключение изменения размеров «кучи»

Можно поддерживать «кучу» фиксированного размера чтобы избежать траты времени на изменение размера «кучи», когда процент свободного пространства уменьшается или вырастает выше определенного значения. Подробная информация приведена в статье Fine-tuning Java garbage collection performance.

Примечание: Количество памяти, используемой приложением, будет соответствовать размеру, установленному для «кучи», даже если «куча» используется на 10% от максимального уровня.

Заключение

Эта статья рассказывает, как использовать инструменты AIX для мониторинга производительности Java и содержит советы, которые можно применять для оптимизации использования CPU приложением. В следующей статье серии будут разбираться приемы для настройки памяти для Java-приложений в ОС AIX.

Ресурсы для скачивания

Похожие темы

  • Maximizing Java performance on AIX: Part 2: The need for speed : оригинал статьи (EN).
  • Другие статьи из серии «Оптимизация производительности Java в AIX (EN):
    • Part 1
    • Part 3
    • Part 4
    • Part 5
  • IBM developer kits for AIX, Java technology edition (EN): наборы для Java-разработки для AIX.
  • IBM developer kits — diagnosis documentation (EN): информация о диагностике Java-приложений на AIX.
  • AIX Performance PMR Data Collection Tools (EN): инструменты сбора данных о производительности AIX.
  • AIX 5L Performance Tools Handbook (EN): учебник IBM по инструментам для измерения и отладки производительности.
  • Understanding IBM eServer pSeries Performance and Sizing (EN): статья об особенностях производительности серверов eServer pSeries.
  • Fine-tuning Java garbage collection performance (EN): статья о настройке производительности при очистке памяти в Java.
  • Getting more memory in AIX for your Java applications (EN): статья о настройке размера памяти, выделяемой для Java-приложений.
  • AIX 5.2 performance tools update, Part 1 (EN): обновления для инструментов по отслеживанию производительности для AIX, часть 1.
  • AIX 5.2 Performance Tools update, Part 2 (EN): обновления для инструментов по отслеживанию производительности для AIX, часть 2.
  • AIX 5.2 Performance Tools update: Part 3 (EN): обновления для инструментов по отслеживанию производительности для AIX, часть 3.
  • AIX and UNIX: в разделе AIX and UNIX developerWorks размещена различная информация по всем аспектам системного администрирования AIX, которая поможет лучше изучить UNIX.
  • Примите участие в форумах AIX и UNIX:(EN)
    • Управление кластерными системами
    • Поддержка IBM
    • Инструменты управления производительностью — технический форум
    • Виртуализация — технический форум
  • IBM trial software: ознакомительные версии программного обеспечения для разработчиков, которые можно загрузить прямо со страницы сообщества developerWorks.(EN)

Комментарии


Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

Блог сурового челябинского программиста

Are you aware how much time I’ve spent learning for details of Java? Thread management, dynamics, CORBA.

четверг, 14 ноября 2013 г.

Сравнение производительности механизмов ввода-вывода в Java: классического (блокирующего), неблокирующего и асинхронного

Не всегда при обеспечении взаимодействия нескольких программ можно полагаться на существующие транспортные механизмы. Да, у нас есть вся мощь веб-сервисов, при использовании серверов приложений нам доступна технология Java EE Connector Architecture (JCA), при взаимодействии с СУБД можно использовать JDBC, а в случае асинхронного взаимодействия можно использовать Java Message Service (JMS). Однако, не смотря на такое обилие технологий, бывает нужно обеспечить нетривиальное взаимодействие с системами, используя сокеты и самостоятельно реализуя протокол прикладного уровня. В данной заметке приведены результаты сравнения производительности трех существующих на данный момент в Java механизмов обеспечения сетевого взаимодействия: классического блокирующего, неблокирующего и асинхронного.

Краткая характеристика существующих подходов к организации ввода-вывода

В ответе на вопрос на сайте StackOverflow приведено сравнение имеющихся в Java механизмов ввода-вывода, при этом логика работы каждого из них продемонстрирована на соответствующей диаграмме. Кратко охарактеризуем данные подходы на примере сетевого взаимодействия.

Блокирующий ввод-вывод (IO). Данный механизм взаимодействия появился в Java самым первым и долгое время оставался единственным возможным подходом к обеспечению сетевого взаимодействия. Суть его заключается в том, что со стороны сервера и клиента создается сокет (со стороны клиента явно, со стороны сервера — в ответ на запрос соединения со стороны клиента). С каждым сокетом связаны два потока (InputStream и OutputStream): один служит для отправки сообщений, другой — для их приема. При этом все операции с данными потоками являются блокирующими, т.е. исполнение команд прерывается на время выполнения данных операций. Предположим, нам необходимо разработать индексатор сайтов (Web Crowler), являющийся частью поискового робота. Обращение к одному сайту занимает 300 мс. Если нам нужно проиндексировать 1000 сайтов, то в блокирующем режиме это займет 300 x 1000 мс, т.е. 300 секунд.

Путем ускорения работы приложений при использовании IO является многопоточность — задача делится на части, каждая из которых выполняется своим потоком управления. Например, чтобы обойти 1000 сайтов можно разбить их на четыре группы по 250 сайтов и обойти сайты из каждой группы параллельно. Самой популярной моделью взаимодействия является создание отдельного потока на каждое подключение. Таким образом все соединения обслуживаются параллельно и не мешают друг-другу. Недостатком такого подхода является линейный рост числа потоков и объема требуемой памяти при увеличении числа обслуживаемых соединений. Если потоков будет создано очень много (десятки тысяч), то операционная система сервера будет занята переключенем контекстов потоков, а на полезную работу времени может не остаться.

Неблокирующий ввод-вывод (NIO). Для решения проблем с блокирующим вводом-выводом был придуман механизм, основанный на мультиплексировании каналов. Данный механизм появился в Java 1.4.2 и был назван New IO (NIO). Суть механизма в следующем: существует мультиплексор (в терминах Java называемый селектором, java.nio.channels.Selector), который в одном потоке, последовательно, производит опрос каналов (в случае сетевого взаимодействия реализуемых классами java.nio.channels.SocketChannel и java.nio.channels.ServerSocketChannel). В результате каждого опроса селектор возвращает идентификаторы каналов, готовых к выполнению операций ввода-вывода (т.е. канал соединился с удаленной системой и в него теперь можно отправлять запрос или, наоборот, удаленная система что-то записала в канал и из него теперь эти данные можно читать). Такие идентификаторы называются «ключами» (java.nio.channels.SelectionKey). Каждый ключ содержит информацию о том, к выполнению какой операции готов канал. Задача приложения — в цикле обойти все ключи и выполнить соответствующие операции.

При использовании данного подхода решение проблемы линейного роста числа потоков при увеличении числа соединений заключается в том, что все подключенные каналы обслуживаются в одном потоке. Однако, за такое решение приходится платить тем, что выполняемые над полученными в результате сетевого взаимодействия данными операции должны быть очень короткими. Любое блокирование обслуживания одного канала сказывается на всех остальных. Если блокировки длительны, то пока поток управления дойдет до последних готовых каналов, установленные ими соединения могут быть уже разорваны по причине бездействия. Решением данной проблемы может быть так называемая отложенная обработка — на основании принятых в одном NIO-потоке данных формируются команды, которые помещаются в неблокируемую очередь, а исполняются отдельным потоком или несколькими потоками, которые в свою очередь не отвлекаются на операции ввода-вывода.

Асинхронный ввод-вывод (AIO, NIO.2). NIO является хорошим средством масштабирования приложений, активно использующих сетевые соединения, но данный подход имеет серьезный недостаток. Поток NIO вынужден явно опрашивать каналы (по-другому это называется «полинг»). При этом, если готовых к осуществлению взаимодействия каналов нет, то данный поток блокируется. Не будет ли лучше, если инициатором взаимодействия с каналами будет не приложение, а сама операционная система? Именно операционная система знает какие каналы готовы к осуществлению взаимодействия, ведь именно она осуществляет обслуживание сетевых карт, портов и прочих механизмов ввода-вывода. Как говорится, ей и карты в руки.

В Java 7 появились механизмы для обеспечения асинхронного сетевого взаимодействия. Это каналы java.nio.channels.AsynchronousSocketChannel и java.nio.channels.AsynchronousServerSocketChannel. Данные каналы содержат методы для неблокирующего установления соединения, приема соединения, записи и чтения. Каждый метод имеет два варианта: один реализован с использованием java.util.concurrent.Future — метод запускает требуемую операцию в отдельном потоке и сразу же возвращает в качестве результата объект класса java.util.concurrent.Future. Для получения результата операции необходимо вызвать Future#get(). Т.к. операция уже запущена в отдельном потоке, то она к моменту вызова Future#get() может быть уже выполнена, тогда метод get() сразу же вернет результат. В противном случае данный метод блокирует поток, в котором он вызван, до завершения операции. Другой вариант реализации асинхронных методов сетевого взаимодействия основан на использовании обработчиков обратного вызова — callback. Данный вариант реализует паттерн Proactor. Суть в следующем — каждый метод, например write, принимает в качестве параметра обработчик завершения — реализацию интерфейса java.nio.channels.CompletionHandler. Метод запускает требуемую операцию в отдельном потоке и передает управление дальше. Когда требуемая операция полностью выполняется, срабатывает один из методов переданного при старте операции обработчика завершения. Основное отличие между асинхронным и неблокирующим вводом-выводом заключается в том, что асинхронный ввод-вывод работает в многопоточной среде: операции выполняются не в тех потоках, из которых были запущены. Операции неблокирующего ввода-вывода выполняются в одном потоке путем мультиплексирования каналов.

Сравнение производительности операций ввода-вывода без отвлечения ресурсов процессора

После понимания сути различных подходов к организации ввода-вывода можно начать сравнивать их с точки зрения производительности. Для сравнения был разработан «индексатор страниц», который отправлять 1000 запросов по протоколу HTTP на специальный сервер. На сервере эмулируется формирование ответа с задержкой 300 мс. Это нужно, чтобы продемонстрировать потери на ввод-вывод.

В первом тесте обработка ответов не производится, т.е. ресурсы процессора не отвлекаются и единственной нагрузкой приложения является ввод-вывод. В реальном мире такому поведению соответствует обновление статуса некой сущности в удаленной системе. Предположим, мы эту сущность из системы получили и обработали, а теперь хотим просто передать результат данной обработки. Пример вполне реален и встречается в промышленных системах.

Тестирование производится на следующей конфигурации: 2-х ядерный процессор Intel i5, 8 Гб ОЗУ, компьютер работает под управлением Windows 7 x64, используется Oracle Java 1.7.0_u40 x64. Серверная часть находится на другой машине и запущена на сервере приложений WebLogic 10g. Связь между клиентом и сервером осуществляется по 100 Мбит/с Ethernet.

Проводятся последовательные запуски клиента в различных режимах, в итоге в каждом режиме собираются результаты семи замеров, по которым считается среднее арифметическое. Измеряется время обработки 1000 запросов на сервере, начиная от команды на установку соединения на клиенте и заканчивая закрытием всех соединений. Время в результатах приведено в миллисекундах.

Задержка при формировании ответа 300 мс, размер ответа — 1 Кб

Задержка при формировании ответа 300 мс, размер ответа — 50 Кб

Задержка при формировании ответа 50 мс, размер ответа — 500 Кб

(для снижения времени выполнения замеров задержка при формировании ответов на сервере снижена до 50мс. Измерения при IO в 1000 потоков прерваны, т.к. надежность данного режима крайне низка — наблюдаются разрывы соединений и превышение таймаутов операций ввода-вывода. Асинхронный ввод-вывод не тестировался так же из-за его низкой надежности, см. ниже.)

Прежде всего стоит развенчать популярный миф, что NIO быстрее IO. Этот миф очень популярен, особо в среде начинающих программистов. Даже при обработке небольших ответов, размером в 1 кб, модель «по потоку на соединение» оказалась самой быстрой, на 10% быстрее NIO, на 5% быстрее AIO. Хотя данные числа и не превышают погрешности измерений. С ростом же интенсивности ввода вывода видно, что IO уверенно обгоняет NIO, работая в 64 потока на 26% быстрее при обработке 50-ти килобайтных ответов и на 40% быстрее при приеме 500-т килобайтных сообщений. Так же следует учесть, что программировать в старом-добром блокирующем стиле проще. Если говорить о ресурсах, то расход процессора в блокирующем режиме даже на 1000 потоков составляет единицы процентов, в то время как в режиме NIO расход процессора составляет 20-30%.

В статье Java IO Faster Than NIO – Old is New Again! приведена ссылка на сравнение серверов, написанных по модели «поток на соединение» и с использованием NIO. IO показал производительность на 25% больше чем NIO. Вообще статья и комментарии к ней довольно интересны.

Отдельно хочется сказать про AIO. При работе с асинхронными сокетами мы можем управлять пулом потоков, в которых будут обрабатываться операции. Если процессор не отвлекается на вычислительные задачи, а полностью обслуживает только управление потоками и ввод-вывод, то в принципе не сильно важно сколько потоков в пуле для асинхронных сокетов, но чтобы система не «залипала», лучше если их будет 2 — 4.

Стоит отметить, что при росте объема принимаемых/передаваемых данных при одновременном осуществлении тысячи подключений, AIO как минимум на Windows начинает вести себя очень ненадежно. При считывании с сервера 500-т килобайтных ответов появляются ошибки соединения: java.io.IOException: Превышен таймаут семафора. Увеличением числа потоков в пуле решить данную проблему не удается.

Сравнение производительности операций ввода-вывода при необходимости в обработке данных

Программа, выполняющая только операции ввода-вывода, интересна, но гораздо интереснее проанализировать поведение программы, обрабатывающей полученные данные. Пронаблюдаем за эмуляцией ресурсоемких для процессора задач, таких как разбор XML, индексация HTML, поиск с помощью регулярных выражений, проверка ЭЦП, шифрование и т.д. Встроим во все примеры после считывания сообщений эмуляцию их обработки с помощью математических операций. Участок обработки данных при выполнении операций в один поток будет занимать 5 — 8 мс. Размер считываемого с сервера сообщения составляет 1 Кб, ответы сервер формирует с задержкой в 300 мс.

«Победителем» при таком сравнении является асинхронный ввод-вывод, при котором данные обрабатываются в один поток. Производительность AIO выше чем у модели «поток на соединение» на 46%. Стоит отметить, что при увеличении числа потоков, не блокируемых на вводе-выводе, нагрузка на процессор пропорционально растет, а вот скорость выполнения математических операций процессором снижается. Соответственно, тот блок операций, который в один поток выполнялся 8 мс., при загруженности процессора двумя потоками выполняется за 10 — 20 мс., четырьмя потоками — 40-60 мс., а шестнадцатью потоками — уже 100 — 256 мс, что становится сопоставимым с длительностью операций ввода-вывода!

Стоит отметить, что при всех своих преимуществах возможно пока и возможно только на Windows, асинхронный ввод-вывод может «залипать» на некоторое время, ожидая событий от операционной системы. Такие залипания бывают даже видны на глаз.

Отдельно хочется сказать про неблокирующий ввод-вывод. Режим, при котором данные обрабатываются по месту их получения, является очень опасным. Так как обработка каждой порции данных занимает время, а работа приложения осуществляется в один поток, то каждая пауза влияет не только на обработку текущего блока данных, но и на обработку данных из всех оставшихся каналов. При этом данные паузы накапливаются.

Решением проблемы с блокировками является отложенная обработка данных. Данные не обрабатываются по месту получения из канала, а помещаются в неблокирующую очередь, например в экземпляр класса java.util.concurrent.ConcurrentLinkedQueue, появившегося в Java 5. Обрабатываются же данные другим потоком (одним или несколькими). В Java 6, до появления AIO, данный режим является самым быстрым, обгоняя на 40% модель «поток на соединение». При увеличении числа потоков-обработчиков данных, время обработки растет, т.к. данные потоки не блокируются и полностью нагружают процессор. Уже при пяти потоках программа намертво блокирует компьютер так, что работать с ним становится невозможно.

Исходные коды программ

Замечание. Исходные коды не предназначены для использования в реальных приложениях и требуют рефакторинга. Вся логика каждого микробенчмарка помещена в один класс, обработка исключений минимальна.

Выводы

Конечно, приведенные микробенчмарки далеки от совершенства, например в них не производится прогрев программы перед замером времени, а так же автоматический подсчет среднего арифметического результатов замеров. Но с другой стороны флуктуации в загруженности сети, сервера и тестовой машины окажут влияние на результат гораздо больший чем JIT-компилятор. Скачки из-за качества сети составляли при измерениях единицы секунд, такие «вылеты» приходилось исключать из результатов измерений. Порядок полученных результатов является верным, а показанный в таблицах разброс помогает понять, что о некоторых вещах нельзя говорить однозначно. Например, в случае отсутствия обработки результатов модель «поток на соединение» оказалась на 5% быстрее AIO. Значит ли это, что всегда нужно использовать модель «поток на соединение»? Вовсе нет, хотя данная модель и является наиболее легкой в реализации. Если мы добавляем обработку данных и наша программа начинает эксплуатировать процессор, то модель «поток на соединение» оказывается на десятки процентов медленнее других подходов. Такая разница следует уже из принципиальных различий в моделях организации ввода-вывода.

Каковы же области применения у каждого подхода? На основании полученных данных можно сформулировать следующие принципы. Если ваше приложение должно обеспечить одновременное обслуживание десятков тысяч подключений, при этом каждому клиенту отправляется небольшая порция данных, т.е. речь идет об онлайн-игре, чат-сервере, P2P-клиенте, то модель NIO и появившаяся в Java 7 модель AIO являются лучшими кандидатами.

Если же приложение обслуживает относительно небольшое число соединений, но интенсивность обмена с каждым клиентом высока, отправляются и принимаются большие порции данных, т.е. речь идет об интеграции приложений, ESB, файловом сервере, то лучшей моделью будет старый-добрый блокирующий ввод-вывод и модель «поток на соединение». С точки зрения надежности многопоточный блокирующий ввод-вывод так же выигрывает у неблокирующего и асинхронного при повышении интенсивности обмена.

Поводом разобраться с моделями организации ввода-вывода для меня послужила статья Ивана Сагалаева Надо все переписать. Несмотря на то, что автор в одной статье смешал критику и «толстых» потоков операционной системы, и блокирующего ввода-вывода, статья все равно очень интересная и о многом заставляет задуматься. Надеюсь полезной она окажется и вам.

Илон Маск рекомендует:  Фильтр в CSS
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL
«