Для чего нужны параллельные стримы java

[Перевод] Когда использовать параллельные stream-ы

Рассмотрим использование S.parallelStream().operation(F) вместо S.stream().operation(F) при условии, что операции независимы друг от друга, и, либо затратны с точки зрения вычислений, либо применяются к большому числу элементов эффективно расщепляемой (splittable) структуры данных, либо и то и другое. Точнее:

Фреймворк потоковой обработки не будет (и не может) настаивать на чем-либо из перечисленного выше. Если вычисления зависимы между собой, то их параллельное выполнение не имеет смысла, либо вообще будет вредным и приведет к ошибкам. К другим, производным от приведенных выше инженерных ограничений (issues) и компромиссов (tradeoffs), критериям относятся:

Если перефразировать все вышесказанное, то использование parallel() в случае неоправданно малого объема вычислений может стоить около 100 микросекунд, а использование в противном случае должно сохранить, по крайней мере, само это время (или возможно часы для очень больших задач). Конкретная стоимость и польза будет различаться со временем и для различных платформ, и, также, в зависимости от контекста. Например, запуск небольших вычислений параллельно внутри последовательного цикла усиливает эффект подъемов и спадов (микротесты производительности, в которых проявляется подобное, могут не отражать реальную ситуацию).

Вопросы и ответы

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

Эту идею можно распространить и на другие соображения о том когда и как использовать параллелизм.

На данный момент, использующие I/O генераторы Stream ов JDK (например, BufferedReader.lines() ), приспособлены, главным образом, для использования в последовательном режиме, обрабатывая элементы один за другим по мере поступления. Поддержка высокоэффективной массовой (bulk) обработки буферизированного I/O возможна, но, на данный момент, это требует разработки специальных генераторов Stream ов, Spliterator ов и Collector ов. Поддержка некоторых общих случаев может быть добавлена в будущих релизах JDK.

Машины, обычно, располагают фиксированным числом ядер, и не могут магическим образом создавать новые при выполнении параллельных операций. Однако, до тех пор пока критерии выбора параллельного режима явно говорят за, сомневаться не в чем. Ваши параллельные задачи будут конкурировать за ЦПУ с другими и вы заметите меньшее ускорение. В большинстве случаев это все равно более эффективно, чем другие альтернативы. Лежащий в основе механизм спроектирован так, что если доступные ядра отсутствуют, то вы заметите лишь небольшое замедление в сравнение с последовательным вариантом, за исключением случаев когда система настолько перегружена, что тратит все свое время на переключение контекста вместо выполнения какой-то реальной работы, или настроена в расчете на то, что вся обработка выполняется последовательно. Если у вас такая система, то, возможно, администратор уже отключил использование много- поточности/ядерности в настройках JVM. А если администратором системы являетесь вы сами, то есть смысл это сделать.

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

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

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

Источник

Русские Блоги

Java8-17-Stream параллельная обработка данных и производительность

Каталог статей

Параллельная обработка данных и производительность

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

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

В Java 7 представлена ​​структура, называемая ветвлением / слиянием, чтобы сделать эти операции более стабильными и менее подверженными ошибкам.

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

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

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

Параллельный поток

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

пример

Давайте попробуем эту идею на простом примере.

В более традиционных терминах Java этот код эквивалентен следующей итерации:

Кажется, это хорошая возможность воспользоваться преимуществами параллельной обработки, особенно когда n велико.

Как начать? Вы хотите синхронизировать переменную результата? Сколько потоков используется? Кто отвечает за создание номеров? Кто будет делать дополнение?

Совершенно не о чем беспокоиться. С параллельными потоками проблема намного проще!

Преобразование последовательного потока в параллельный поток

Мы можем преобразовать поток в параллельный поток, чтобы предыдущий процесс сокращения функции (то есть суммирование) выполнялся параллельно, вызывая параллельный метод в последовательном потоке:

Процесс выполнения параллельного потока:

Для чего нужны параллельные стримы java. Смотреть фото Для чего нужны параллельные стримы java. Смотреть картинку Для чего нужны параллельные стримы java. Картинка про Для чего нужны параллельные стримы java. Фото Для чего нужны параллельные стримы java

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

Фактически он устанавливает внутренний логический флаг, указывая, что вы хотите, чтобы все операции, выполняемые после вызова parallel, выполнялись параллельно.

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

Например, вы можете:

Но последний параллельный или последовательный вызов повлияет на весь конвейер.

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

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

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

Измерение производительности потока

Мы утверждаем, что параллельный метод суммирования должен работать лучше, чем последовательный и итерационный методы. Однако в разработке программного обеспечения угадывание определенно не лучший способ!

Вы всегда должны следовать трем золотым правилам, особенно при оптимизации производительности: измерять, измерять и измерять.

Измеряет производительность функции, которая суммирует первые n натуральных чисел.

Этот метод принимает функцию и параметры типа long as. Он применяет функцию 10 раз к long, переданному методу, записывает время (в миллисекундах) каждого выполнения и возвращает самое короткое время выполнения.

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

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

Вы можете запустить эти коды на своей машине. Запустив его на ноутбуке i5 6200U, результат будет примерно таким:

Итеративная версия традиционного цикла for должна выполняться намного быстрее, потому что она более низкоуровневая и, что более важно, не требует упаковки или распаковки примитивных типов.

Если вы попытаетесь измерить его производительность:

Теперь протестируем параллельную версию функции:

Смотрите, что происходит:

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

Это очень обидно, ведь параллельная версия метода суммирования намного медленнее последовательной.

Как вы объясните этот неожиданный результат?

На самом деле здесь есть две проблемы:

Iterate генерирует упакованные объекты, которые необходимо распаковать в числа для суммирования.

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

Второй вопрос немного интереснее, потому что вы должны понимать, что некоторые потоковые операции легче распараллелить, чем другие.

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

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

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

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

Используйте более адресный подход

Так как же использовать многоядерные процессоры для эффективного параллельного суммирования с потоками?

Мы обсуждали метод LongStream.rangeClosed в главе 5.

Этот метод имеет два преимущества перед итерацией.

LongStream.rangeClosed напрямую производит примитивные длинные числа без накладных расходов на упаковку и распаковку.

LongStream.rangeClosed сгенерирует диапазон чисел, который легко разделить на отдельные части.

Например, диапазон 120 можно разделить на 15、610、1115 и 16-20.

Давайте посмотрим на его производительность, когда он используется для потока последовательности, и посмотрим, важны ли накладные расходы на распаковку:

На этот раз результат:

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

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

Но что, если к этой новой версии применяется параллельная потоковая передача?

Теперь передайте эту функцию методу тестирования:

ps: стократное улучшение производительности.

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

Тем не менее помните, что распараллеливание не обходится без затрат.

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

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

Давайте рассмотрим распространенную ошибку.

Правильно используйте параллельные потоки

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

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

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

Что не так с таким кодом? К сожалению, это действительно безнадежно, потому что оно носит последовательный характер.

Каждый раз, когда вы посещаете total, будет конкуренция данных. Если вы попытаетесь исправить это с помощью синхронизации, вы потеряете смысл параллелизма.

Чтобы проиллюстрировать это, давайте попробуем сделать Stream параллельным:

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

Вы можете получить следующий результат:

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

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

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

Теперь вы знаете, что общее изменяемое состояние влияет на параллельные потоки и параллельные вычисления.

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

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

Эффективно используйте параллельные потоки

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

Обратите внимание на упаковку. Операции автоматической упаковки и распаковки значительно снизят производительность.

В Java 8 есть примитивные типы потоков (IntStream, LongStream, DoubleStream), чтобы избежать такого рода операций, но эти потоки следует использовать везде, где это возможно.

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

В частности, такие операции, как limit и findFirst, которые зависят от порядка элементов, очень дороги для выполнения в параллельных потоках.

Например, findAny будет работать лучше, чем findFirst, потому что его не нужно выполнять последовательно. Вы всегда можете вызвать неупорядоченный метод, чтобы превратить упорядоченный поток в неупорядоченный поток. Затем, если вам нужно n элементов в потоке вместо, в частности, первого n, ограничение на вызов для неупорядоченного параллельного потока может быть более эффективным, чем для одного упорядоченного потока (например, источником данных является список).

Для меньших объемов данных выбор параллельных потоков почти никогда не бывает хорошим решением.

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

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

Кроме того, поток примитивного типа, созданный с помощью метода фабрики диапазонов, также можно быстро разложить.

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

Также учтите, велика или мала стоимость шага слияния в операции терминала (например, метод объединения в Collector). Если этот этап является дорогостоящим, тогда стоимость объединения частичных результатов каждого подпотока может превысить улучшение производительности, полученное с помощью параллельных потоков.

Принцип реализации, лежащий в основе

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

Источник

Java Stream API: что делает хорошо, а что не очень

Для чего нужны параллельные стримы java. Смотреть фото Для чего нужны параллельные стримы java. Смотреть картинку Для чего нужны параллельные стримы java. Картинка про Для чего нужны параллельные стримы java. Фото Для чего нужны параллельные стримы java

Настолько ли «энергичен» Java 8 Stream API? Возможно ли «превращение» обработки сложных операций над коллекциями в простой и понятный код? Где та выгода от параллельных операций, и когда стоит остановиться? Это одни из многочисленных вопросов, встречающихся читателям. Попробуем разобрать подводные камни Stream API с Тагиром Валеевым aka @lany. Многие читатели уже знакомы с нашим собеседником по статьям, исследованиям в области Java, выразительным докладам на конференциях. Итак, без проволочек, начинаем обсуждение.

Тагир, у вас отличные показатели на ресурсе StackOverflow (gold status в ветке «java-stream»). Как вы думаете, динамика применения Java 8 Stream API и сложность конструкций выросла (на основе вопросов и ответов на данном ресурсе)?

Для чего нужны параллельные стримы java. Смотреть фото Для чего нужны параллельные стримы java. Смотреть картинку Для чего нужны параллельные стримы java. Картинка про Для чего нужны параллельные стримы java. Фото Для чего нужны параллельные стримы java— Верно, одно время я много времени проводил на StackOverflow, постоянно отслеживая вопросы по Stream API. Сейчас заглядываю периодически, так как, на мой взгляд, на большинство интересных вопросов уже есть ответы. Безусловно, чувствуется, что люди распробовали Stream API, было бы странно, если бы это было не так. Первые вопросы по этой теме появились ещё до выпуска Java 8, когда люди экспериментировали с ранними сборками. Расцвет пришёлся на конец 2014 и 2015-й год.
Многие интересные вопросы связаны не только с тем, что можно сделать со Stream API, но и с тем, чего нормально сделать нельзя без сторонних библиотек. Пользователи, постоянно спрашивая и обсуждая, стремились раздвинуть рамки Stream API. Некоторые из этих вопросов послужили источниками идей для моей библиотеки StreamEx, расширяющей функциональность Java 8 Stream API.

Вы упомянули про StreamEx. Расскажите, что побудило вас к созданию? Какие цели вы преследовали?

— Мотивы были сугубо практические. Когда на работе мы перешли на Java 8, первая эйфория от красоты и удобства довольно быстро сменилась чередой спотыканий: хотелось сделать с помощью Stream API определённые вещи, которые вроде делаться должны, но по факту не получались. Приходилось удлинять код или отступать от спецификации. Я начал добавлять в рабочие проекты вспомогательные классы и методы для решения данных проблем, но выглядело это некрасиво. Потом я догадался обернуть стандартные стримы в свои классы, которые предлагают ряд дополнительных операций, и работать стало существенно приятнее. Эти классы я выделил в отдельный открытый проект и начал развивать его.

На ваш взгляд, какие виды расчетов и операций и над какими данными действительно стоит реализовать c использованием Stream API, а что не очень подходит для обработки?

— Stream API любит неизменяемые данные. Если вы хотите поменять существующие структуры данных, а не создать новые, вам нужно что-то другое. Посмотрите в сторону новых стандартных методов (например, List.replaceAll).

Stream API любит независимые данные. Если для получения результата вам нужно использовать одновременно несколько элементов из входного набора, без сторонних библиотек будет очень коряво. Но библиотеки вроде StreamEx часто решают эту проблему.

Stream API любит решать одну задачу за проход. Если вы хотите в один обход данных решить несколько разных задач, готовьтесь писать свои коллекторы. И не факт, что это вообще получится.

Stream API не любит проверяемые исключения. Вам будет не очень удобно кидать их из операций Stream API. Опять же есть библиотеки, которые пытаются это облегчить (скажем, jOOλ), но я бы рекомендовал отказываться от проверяемых исключений.

В стандартном Stream API не хватает некоторых операций, которые очень нужны. Например, takeWhile, появится только в Java 9. Может оказаться, что вы хотите чего-то вполне разумного и несложного, но сделать это не получится. Опять же, стоит заметить, что библиотеки вроде jOOλ и StreamEx решают большинство таких проблем.

Для чего нужны параллельные стримы java. Смотреть фото Для чего нужны параллельные стримы java. Смотреть картинку Для чего нужны параллельные стримы java. Картинка про Для чего нужны параллельные стримы java. Фото Для чего нужны параллельные стримы java

Как вы считаете, есть ли смысл использовать parallelStream всегда? Какие проблемы могут возникнуть при «переключении» методов из stream на parallelStream?

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

Во-первых, большинство задач, решаемых с помощью Stream API, слишком быстрые по сравнению с накладными расходами на распределение задач по ForkJoinPool и их синхронизацию. Известная статья Дага Ли (Doug Lea) «When to use parallel streams» приводит правило большого пальца: на современных машинах обычно распараллеливать имеет смысл задачи, время выполнения которых превышает 100 микросекунд. Мои тесты показывают, что иногда и 20-микросекундная задача ускоряется от распараллеливания, но это уже зависит от многих факторов.

Во-вторых, даже если ваша задача выполняется долго, не факт, что параллелизм её ускорит. Это зависит и от качества источника, и от промежуточных операций (например, limit для упорядоченного стрима может долго работать), и от терминальных операций (скажем, forEachOrdered может иногда свести на нет выгоду от параллелизма). Самые хорошие промежуточные операции — это операции без состояния (filter, map, flatMap и peek), а самые хорошие терминальные — это семейство reduce/collect, которые ассоциативны, то есть могут эффективно разбить задачу на подзадачи и потом объединить их результаты. И то процедура объединения иногда не очень оптимальна (к примеру, для сложных цепочек groupingBy).

В-третьих, многие люди используют Stream API неверно, нарушая спецификацию. Например, передавая лямбды с внутренним состоянием (stateful) в операции вроде filter и map. Или нарушая требования к единице и ассоциативности в reduce. Не говоря уж о том, сколько неправильных коллекторов пишут. Это часто простительно для последовательных стримов, но совершенно недопустимо для параллельных. Конечно, это не повод писать неправильно, но факт налицо: параллельными стримами пользоваться сложнее, это не просто дописать parallel() где-нибудь.

И, наконец, даже если у вас стрим выполняется долго, операции в нём легко параллелятся и вы всё делаете правильно, стоит задуматься, действительно ли у вас простаивают ядра процессора, что вы готовы их отдать параллельным стримам? Если у вас веб-сервис, который постоянно загружен запросами, вполне возможно, что обрабатывать каждый запрос отдельным потоком будет разумнее. Только если у вас ядер достаточно много, либо система не загружена полностью, можно задуматься о параллельных стримах. Возможно, кстати, стоит устанавливать java.util.concurrent.ForkJoinPool.common.parallelism для ограничения параллельных стримов.

Например, если у вас 16 ядер и обычно 12 загружено, попробуйте установить уровень параллелизма 4, чтобы занять стримами оставшиеся ядра. Общих советов, конечно, нет: надо всегда проверять.

В продолжение разговора о параллелизации, можно ли говорить о том, что на производительность влияет объем и структура данных, количество ядер процессора? Какие источники данных (например, LinkedList) не стоит обрабатывать в параллель?

— LinkedList ещё не самый худший источник. Он, по крайней мере, свой размер знает, что позволяет Stream API удачнее дробить задачи. Хуже всего для параллельности источники, которые по сути последовательны (как LinkedList) и при этом не сообщают свой размер. Обычно это то, что создано через Spliterators.spliteratorUnknownSize(), либо через AbstractSpliterator без указания размера. Примеры из JDK — Stream.iterate(), Files.list(), Files.walk(), BufferedReader.lines(), Pattern.splitAsStream() и так далее. Я говорил об этом на докладе «Странности Stream API» на JPoint в этом году. Там очень плохая реализация, которая приводит, например, к тому, что если этот источник содержит 1024 элемента или менее, то он не параллелится вообще. И даже потом параллелится довольно плохо. Для более или менее нормального параллелизма вам нужно, чтобы в нём были десятки тысяч элементов. В StreamEx реализация лучше. Например, StreamEx.ofLines(reader) (аналог BufferedReader.lines()) будет параллелиться неплохо даже для небольших файлов. Если у вас плохой источник и вы хотите его распараллелить, часто эффективнее сперва последовательно его собрать в список (например, Stream.iterate(…).collect(toList()).parallelStream()…)

Большинство стандартных структур данных из JDK являются хорошими источниками. Опасайтесь структур и обёрток из сторонних библиотек, которые совместимы с Java 7. В них не может быть переопределён метод spliterator() (потому что в Java 7 нет сплитераторов), поэтому они будут использовать реализацию Collection.spliterator() или List.spliterator() по умолчанию, которая, конечно, плохо параллелится, потому что ничего не знает о вашей структуре данных и просто оборачивает итератор. В девятке это улучшится для списков со случайным доступом.

При использовании промежуточных операций, на ваш взгляд, какое пороговое значение их в Streamконвейере и как это определяется? Существуют ли ограничения (явные и неявные)?

Наличие методов упорядочивания коллекций во время обработки (промежуточная операция sorted()) или упорядоченного источника данных и последующая работа с ним с помощью map, filter и reduce операций могут привести к повышению производительности?

Нет, вряд ли. Только операция distinct() использует тот факт, что вход сортирован. Она меняет алгоритм, сравнивая элемент с предыдущим, а без сортировки приходится держать HashSet. Однако для этого источник должен сообщить, что он сортирован. Все сортированные источники из JDK (BitSet, TreeSet, IntStream.range) уже содержат уникальные элементы, поэтому для них distinct() бесполезен. Ну, теоретически операция filter может что-то выиграть из-за лучшего предсказания ветвлений в процессоре, если она на первой половине набора истинна, а на второй ложна. Но если данные уже отсортированы по предикату, эффективнее не использовать Stream API, а найти границу с помощью бинарного поиска. Причём сортировка сама по себе медленная, если данные на входе плохо сортированы. Поэтому, скажем, sorted().distinct() для случайных данных будет медленнее, чем просто distinct(), хотя сам distinct() ускорится.

Для чего нужны параллельные стримы java. Смотреть фото Для чего нужны параллельные стримы java. Смотреть картинку Для чего нужны параллельные стримы java. Картинка про Для чего нужны параллельные стримы java. Фото Для чего нужны параллельные стримы javaНеобходимо затронуть важные вопросы, связанные с отладкой кода. Вы используете метод peek(), для получения промежуточных результатов? Возможно, что у вас есть свои секреты тестирования? Поделитесь, пожалуйста, ими с читателями.

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

Когда я разрабатываю новые сплитераторы и коллекторы, я использую вспомогательные методы в тестах, которые подвергают их всестороннему тестированию, проверяя различные инварианты и запуская в разных условиях. Скажем, я не только сравниваю, что результат параллельного и последовательного стрима совпадает, а могу в параллельный стрим вставить искусственный сплитератор, который наплодит пустых фрагментов при создании параллельных задач. Они не должны влиять на результат и помогают найти нетривиальные баги. Или при тестировании сплитераторов я случайным образом дроблю их на подзадачи, которые выполняю в случайном порядке (но в одном потоке) и сверяю результат с последовательным. Это стабильный воспроизводимый тест, который хотя и однопоточный, но позволяет отловить большинство ошибок в распараллеленных сплитераторах. Вообще, крутая система тестов, которая всесторонне проверяет каждый кирпичик кода и в случае ошибок выдаёт вменяемый отчёт, обычно вполне заменяет отладку.

Какое развитие Stream API вы видите в будущем?

— Сложный вопрос, я не умею предсказывать будущее. Сейчас многое упирается в наличие четырёх специализаций Stream API (Stream, IntStream, LongStream, DoubleStream), поэтому многий код приходится дублировать четыре раза, чего мало кому хочется. Все с нетерпением ждут специализацию дженериков, которую, вероятно, доделают в Java 10. Тогда будет проще.

Также есть проблемы с расширением Stream API. Как известно, Stream — это интерфейс, а не какой-нибудь финальный класс. С одной стороны, это позволяет расширять Stream API сторонним разработчикам. С другой стороны, добавлять новые методы в Stream API теперь не так-то легко: надо не сломать все те классы, который уже в Java 8 реализовали этот интерфейс. Каждый новый метод должен предоставить реализацию по умолчанию, выраженную в терминах существующих методов, что не всегда возможно и легко. Поэтому взрывного роста функциональности вряд ли стоит ожидать.

Самое важное, что появится в Java 9, — это методы takeWhile и dropWhile. Будут мелкие приятные штуки — Stream.ofNullable, Optional.stream, iterate с тремя аргументами и несколько новых коллекторов — flatMapping, filtering. Но, в целом, многого всё ещё будет не хватать. Зато появятся дополнительные методы в JDK, которые создают стрим: новые API теперь разрабатывают с оглядкой на стримы, да и старые подтягивают.

Многие запомнили ваше выступление в 2015 году с докладом «Что же мы измеряем?». В этом году вы планируете выступить с новой темой на Joker? О чем пойдет речь?

— Я решил делать новый доклад, который не очень творчески назову «Причуды Stream API». Это будет в некотором смысле продолжение доклада «Странности Stream API» с JPoint: я расскажу о неожиданных эффектах производительности и скользких местах Stream API, акцентируя внимание на том, что будет исправлено в Java 9.

Спасибо большое за интересные и подробные ответы. С нетерпением ждем ваше новое выступление.

Прикоснуться к миру Stream API и другого Java-хардкора можно будет на конференции Joker 2016. Там же — вопросы спикерам, дискуссии вокруг докладов и бесконечный нетворкинг.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *