Миф о Java развеян (или развеян?)

tl;dr for-loops необязательно намного производительнее, чем stream()

когда в JDK 8 stream() был представлен в java Collections, немедленной реакцией было: «они настолько
намного медленнее, чем обычные for-петли».
Я протестировал его, и он действительно был медленнее. намного. (Я помню, что в какой-то момент oracle признал, что он медленнее, но основное внимание было уделено функциональности, и производительность, вероятно, улучшится в будущем. увы, я не могу найти источник для моей памяти, так что давайте предположим, что она ошибочна).

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

Некоторые разработчики приняли этот факт и сохранили его в своем мозгу навсегда. но потоки были представлены в 2014 году. Прошло 8 лет. как это выглядит сегодня? действительно ли это так медленно, как некоторые постоянно заявляют? давайте узнаем.

Я написал набор бенчмарков, которые (насколько мне известно) используют правильную процедуру в JMH.

  • создать данные в объекте @State
  • уничтожить результат в объекте @State.

Я позволил ему обрабатывать циклы из 10, 10_000 и 10_000_000 записей. и вот результаты:

10 записей

Benchmark                                               Mode  Cnt       Score       Error  Units
JmhStreamPerformanceMeasurement.collectFilteredFor     thrpt   25  691000.985 ±  5338.170  ops/s
JmhStreamPerformanceMeasurement.collectFilteredForGet  thrpt   25  687244.094 ±  2287.375  ops/s
JmhStreamPerformanceMeasurement.collectFilteredStream  thrpt   25  620127.959 ± 11149.611  ops/s

JmhStreamPerformanceMeasurement.collectFor             thrpt   25  601047.148 ±  6901.828  ops/s
JmhStreamPerformanceMeasurement.collectForGet          thrpt   25  593137.918 ±  7027.976  ops/s
JmhStreamPerformanceMeasurement.collectStream          thrpt   25  583345.516 ±  2706.945  ops/s

JmhStreamPerformanceMeasurement.easyTaskFor            thrpt   25  752205.384 ±  3155.479  ops/s
JmhStreamPerformanceMeasurement.easyTaskForGet         thrpt   25  753751.877 ±  2618.748  ops/s
JmhStreamPerformanceMeasurement.easyTaskStream         thrpt   25  732847.868 ±  1268.374  ops/s

JmhStreamPerformanceMeasurement.heavyTaskFor           thrpt   25  725538.827 ±   859.032  ops/s
JmhStreamPerformanceMeasurement.heavyTaskForGet        thrpt   25  725200.238 ±   825.300  ops/s
JmhStreamPerformanceMeasurement.heavyTaskStream        thrpt   25  723650.793 ±  1007.079  ops/s
Вход в полноэкранный режим Выход из полноэкранного режима

10_000 записей

Benchmark                                               Mode  Cnt      Score     Error  Units
JmhStreamPerformanceMeasurement.collectFilteredFor     thrpt   25   4700.019 ±  13.206  ops/s
JmhStreamPerformanceMeasurement.collectFilteredForGet  thrpt   25   4613.177 ±  52.664  ops/s
JmhStreamPerformanceMeasurement.collectFilteredStream  thrpt   25   4718.937 ± 232.897  ops/s

JmhStreamPerformanceMeasurement.collectFor             thrpt   25   1369.088 ±  10.711  ops/s
JmhStreamPerformanceMeasurement.collectForGet          thrpt   25   1337.578 ±  10.015  ops/s
JmhStreamPerformanceMeasurement.collectStream          thrpt   25   1383.158 ±  49.265  ops/s

JmhStreamPerformanceMeasurement.easyTaskFor            thrpt   25  39043.233 ± 708.907  ops/s
JmhStreamPerformanceMeasurement.easyTaskForGet         thrpt   25  42027.702 ±  91.457  ops/s
JmhStreamPerformanceMeasurement.easyTaskStream         thrpt   25  40108.355 ± 123.484  ops/s

JmhStreamPerformanceMeasurement.heavyTaskFor           thrpt   25   9309.883 ±  13.252  ops/s
JmhStreamPerformanceMeasurement.heavyTaskForGet        thrpt   25  14033.988 ±  13.011  ops/s
JmhStreamPerformanceMeasurement.heavyTaskStream        thrpt   25  13440.062 ±  98.916  ops/s
Войти в полноэкранный режим Выход из полноэкранного режима

10_000_000 записей

Benchmark                                               Mode  Cnt   Score   Error  Units
JmhStreamPerformanceMeasurement.collectFilteredFor     thrpt   25   1.256 ± 0.044  ops/s
JmhStreamPerformanceMeasurement.collectFilteredForGet  thrpt   25   1.240 ± 0.038  ops/s
JmhStreamPerformanceMeasurement.collectFilteredStream  thrpt   25   1.182 ± 0.052  ops/s

JmhStreamPerformanceMeasurement.collectFor             thrpt   25   0.321 ± 0.006  ops/s
JmhStreamPerformanceMeasurement.collectForGet          thrpt   25   0.324 ± 0.005  ops/s
JmhStreamPerformanceMeasurement.collectStream          thrpt   25   0.322 ± 0.006  ops/s

JmhStreamPerformanceMeasurement.easyTaskFor            thrpt   25  39.874 ± 0.326  ops/s
JmhStreamPerformanceMeasurement.easyTaskForGet         thrpt   25  40.546 ± 0.356  ops/s
JmhStreamPerformanceMeasurement.easyTaskStream         thrpt   25  40.263 ± 0.374  ops/s

JmhStreamPerformanceMeasurement.heavyTaskFor           thrpt   25  14.993 ± 0.083  ops/s
JmhStreamPerformanceMeasurement.heavyTaskForGet        thrpt   25  14.795 ± 0.091  ops/s
JmhStreamPerformanceMeasurement.heavyTaskStream        thrpt   25  14.746 ± 0.076  ops/s
Войти в полноэкранный режим Выход из полноэкранного режима

Заключение

бенчмарки работали в течение 6 часов и сгладили большинство пиков.
результат в операциях в секунду, поэтому чем больше, тем лучше.

на случай, если вам лень смотреть, что означают бенчмарки:

Мои ожидания

Я бы предположил, что бенчмарк ForGet будет самым быстрым из трех, потому что не будет генерироваться итератор, а get(i) на ArrayList — это, по сути, только обернутый доступ к массиву.

Я также предполагаю, что For быстрее, чем Stream, потому что он генерирует только один экземпляр Iterator, в то время как stream() генерирует кучу экземпляров для обработки данных.

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

Результат

Данные выглядят почти так, как ожидалось, за исключением того, что stream() вовсе не выглядит как всегда самый медленный. Не стесняйтесь проверить мой код бенчмарка, возможно, я допустил ошибку.

Похоже, что при коротких циклах (несколько записей) поток на 11% медленнее, чем цикл for. но это сильно зависит от того, что вы выполняете. easyTask хуже всего. filteredCollect тоже не очень хорошо выглядит при 10 записях.
Но ситуация меняется уже при 10_000 записей: тогда filteredCollect с потоком оказывается самым быстрым.

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

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

Примерное правило:

  • для коротких простых задач лучше использовать цикл for-loop.
  • длинные сложные задачи, вероятно, лучше stream()

как только вам придется обрабатывать исключения, for-loop в любом случае лучше, потому что в stream() это ужасно.

Есть один бенчмарк, который выглядит подозрительно: heavyTaskFor с 10_000 записей. Я повторю его еще раз и прокомментирую. Я предполагаю, что моя машина сделала что-то странное в то время. Не обращайте на это внимания, пожалуйста, пока.

Спасибо, что прочитали.

Оцените статью
devanswers.ru
Добавить комментарий