Есть ли прямые или косвенные преимущества в производительности последовательных потоков Java 8? - PullRequest
0 голосов
/ 17 января 2019

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

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

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
})
    .forEach(s -> System.out.println("forEach: " + s));

Использование классической Java:

String[] strings = {"d2", "a2", "b1", "b3", "c"};
        for (String s : strings)
        {
            System.out.println("Before filtering: " + s);
            if (s.startsWith("a"))
            {
                System.out.println("After Filtering: " + s);
            }
        }

Точка. Здесь в потоках обработка a2 начинается только после того, как все операции над d2 завершены (Ранее я думал, что пока d2 обрабатывается foreach, фильтр должен был работать на a2, но это не так, как в этой статье : https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/), То же самое относится и к классической Java, так что должно быть мотивом использования потоков помимо "выразительного" и "элегантного" стиля кодирования? Я знаю, что есть компромисс производительности для компилятора при обработке потоков, кто-нибудь знает / испытали ли вы какие-либо преимущества в производительности при использовании последовательных потоков?

Ответы [ 3 ]

0 голосов
/ 17 января 2019

Если бы вы все еще использовали потоки, вы могли бы создать поток из вашего массива, используя Arrays.stream и использовать forEach как:

Arrays.stream(strings).forEach(s -> {
    System.out.println("Before filtering: " + s);
    if (s.startsWith("a")) {
        System.out.println("After Filtering: " + s);
    }
});

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

0 голосов
/ 18 января 2019

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


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

Но… когда мы сравниваем «поток» с «циклом», действительно ли разумно предположить, что все ручные циклы написаны наиболее эффективным образом для конкретного варианта использования? Конкретная реализация Stream будет применять свои оптимизации ко всем случаям использования, где это применимо, независимо от уровня опыта автора вызывающего кода. Я уже видел циклы, в которых отсутствует возможность короткого замыкания или выполнения избыточных операций, которые не нужны для конкретного случая использования.

Другим аспектом является информация, необходимая для выполнения определенных оптимизаций. Stream API построен на интерфейсе Spliterator, который может предоставлять характеристики исходных данных, например, это позволяет выяснить, имеет ли данные значимый порядок, который необходимо сохранить для определенных операций, или он уже предварительно отсортирован, к естественному порядку или с конкретным компаратором. Он также может предоставить ожидаемое количество элементов, в качестве приблизительного или точного, когда это предсказуемо.

Метод, получающий произвольный Collection, для реализации алгоритма с обычным циклом, будет трудно выяснить, есть ли такие характеристики. List подразумевает значимый порядок, тогда как Set обычно нет, если только это не SortedSet или LinkedHashSet, тогда как последний является конкретным классом реализации, а не интерфейсом. Таким образом, при проверке всех известных группировок все еще могут отсутствовать реализации сторонних производителей со специальными контрактами, которые нельзя выразить с помощью предварительно определенного интерфейса.

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


Существует также еще одно интересное отличие между решениями Stream на основе Spliterator и обычными циклами, использующими Iterator при итерации по чему-то другому, чем массив. Шаблон должен вызывать hasNext на итераторе, за которым следует next, если hasNext не вернул false. Но контракт Iterator не предписывает эту модель. Вызывающая сторона может вызывать next без hasNext, даже несколько раз, когда известно, что она успешна (например, вы уже знаете размер коллекции). Кроме того, вызывающий абонент может вызывать hasNext несколько раз без next в случае, если вызывающий абонент не запомнил результат предыдущего вызова.

Как следствие, реализации Iterator должны выполнять избыточные операции, например, условие цикла эффективно проверяется дважды, один раз в hasNext, чтобы вернуть boolean, и один раз в next, чтобы бросить NoSuchElementException, когда не выполняется. Часто hasNext должен выполнить фактическую операцию обхода и сохранить результат в экземпляре Iterator, чтобы гарантировать, что результат остается действительным до следующего вызова next. Операция next, в свою очередь, должна проверить, был ли такой обход уже произошел или он должен выполнить саму операцию. На практике оптимизатор горячих точек может или не может устранить накладные расходы, налагаемые конструкцией Iterator.

В отличие от Spliterator имеет единственный метод обхода boolean tryAdvance(Consumer<? super T> action), который выполняет фактическую операцию , а возвращает наличие элемента. Это значительно упрощает логику цикла. Существует даже void forEachRemaining(Consumer<? super T> action) для операций без короткого замыкания, что позволяет фактической реализации обеспечить всю логику зацикливания. Например, в случае ArrayList операция закончится простым циклом подсчета индексов, обеспечивающим простой доступ к массиву.

Вы можете сравнить такой дизайн, например, с readLine() из BufferedReader, который выполняет операцию и возвращает null после последнего элемента, или find() регулярного выражения Matcher, который выполняет поиск, обновляет состояние сопоставителя и возвращает состояние успеха.

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

0 голосов
/ 17 января 2019

Потоки могут (и уже имеют некоторые хитрости) под капотом, чего нет в традиционном цикле for. Например:

Arrays.asList(1,2,3)
      .map(x -> x + 1)
      .count();

Поскольку java-9, map будет пропущен, так как вы на самом деле не заботитесь об этом.

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

someSource.stream()
          .sorted()
          ....

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

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