Итератор против потока Java 8

чтобы воспользоваться широким спектром методов запроса включенными в java.util.stream Jdk 8 я пытаюсь разработать модели доменов, где геттеры отношений с * кратность (с нулем или более экземпляров ) возвращает Stream<T>, вместо Iterable<T> или Iterator<T>.

я сомневаюсь, есть ли какие-либо дополнительные накладные расходы, понесенные Stream<T> по сравнению с Iterator<T>?

Итак, есть ли какой-либо недостаток в компрометации моей модели домена с помощью Stream<T>?

или вместо этого я должен всегда возвращать Iterator<T> или Iterable<T>, и оставьте конечному пользователю решение о том, использовать ли поток или нет, преобразовав этот итератор с помощью StreamUtils?

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

2 ответов


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

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

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

есть некоторые дополнительные фиксированные накладные расходы при запуске создания a Stream по сравнению с созданием Iterator -- еще несколько объектов, прежде чем начать вычисления. Если ваш набор данных большой, это не имеет значения; это небольшая стоимость запуска, амортизированная за много вычислений. (И если ваш набор данных мал, это, вероятно, также не имеет значения -- потому что если ваша программа работает на небольших наборах данных, производительность, как правило, не ваша забота #1 либо.) Где это тут дело в том, когда идет параллельно; любое время, потраченное на настройку конвейера, переходит в последовательную часть закона Amdahl; если вы посмотрите на реализацию, мы упорно работаем над тем, чтобы во время настройки потока объект считался, но я был бы рад найти способы уменьшить его, так как это напрямую влияет на размер набора данных безубыточности, где параллель начинает выигрывать по порядку.

но более важной, чем фиксированная стоимость запуска, является стоимость доступа к каждому элементу. Здесь потоки на самом деле выигрывают-и часто выигрывают по-крупному-что некоторых может удивить. (В наших тестах производительности мы обычно видим трубопроводы потока, которые могут превзойти их for-loop over Collection коллегами.) И есть простое объяснение этому:Spliterator имеет фундаментально более низкие цены доступа в-элемента чем Iterator, даже последовательно. Существует несколько причин этот.

  1. протокол итератора принципиально менее эффективен. Для получения каждого элемента требуется вызвать два метода. Далее, потому что итераторы должны быть надежными для таких вещей, как вызов next() без hasNext() или hasNext() несколько раз без next(), оба этих метода обычно должны выполнять некоторое защитное кодирование (и, как правило, больше статичности и ветвления), что добавляет неэффективности. С другой стороны, даже медленный способ пересечь spliterator (tryAdvance) не имеет такого бремени. (Это еще хуже для параллельных структур данных, потому что next/hasNext двойственность в основе своей колоритна, и Iterator реализации должны выполнять больше работы для защиты от параллельных модификаций, чем Spliterator реализаций.)

  2. Spliterator далее предлагает итерацию "быстрого пути"-- forEachRemaining -- который можно использовать большую часть времени (уменьшение, forEach), дальнейшее уменьшение накладных расходов кода итерации, который обеспечивает доступ к внутренней структуре данных. Это также очень хорошо встроено, что, в свою очередь, повышает эффективность других оптимизаций, таких как движение кода, устранение проверки границ и т. д.

  3. далее, проходящего через Spliterator как правило, гораздо меньше кучи пишет, чем с Iterator. С Iterator, каждый элемент вызывает одну или несколько записей кучи (если только Iterator может быть scalarized через Escape-анализ и подсадил в регистры.) Среди другие проблемы, это вызывает активность метки карты GC, что приводит к конфликту строк кэша для меток карты. С другой стороны,--20-- > клонат иметь меньше государства, и промышленн-прочности forEachRemaining реализации, как правило, откладывают запись чего-либо в кучу до конца обхода, вместо этого сохраняя ее состояние итерации в локальных файлах, которые естественно сопоставляются с регистрами, что приводит к снижению активности шины памяти.

резюме: не волнуйтесь, будьте счастливы. Spliterator - это лучше Iterator, даже без параллельности. (Они также, как правило, просто легче писать и труднее ошибиться.)


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

  • Collection.forEach

    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    
  • Iterator.forEachRemaining

    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
        throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
        consumer.accept((E) elementData[i++]);
    }
    
  • Stream.forEach который в конечном итоге вызовет Spliterator.forEachRemaining

    if ((i = index) >= 0 && (index = hi) <= a.length) {
       for (; i < hi; ++i) {
           @SuppressWarnings("unchecked") E e = (E) a[i];
           action.accept(e);
       }
       if (lst.modCount == mc)
           return;
    }
    

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

подобные вещи применяются ко всем стандартным коллекциям JRE, все они адаптировали реализации для всех способов сделать это, даже если вы используете оболочку только для чтения. В в последнем случае Stream API даже слегка выиграет,Collection.forEach должен вызываться в представлении только для чтения, чтобы делегировать в исходную коллекцию forEach. Аналогично, итератор должен быть обернут для защиты от попыток вызвать remove() метод. В отличие от этого, spliterator() может напрямую вернуть оригинальную коллекцию Spliterator поскольку он не имеет поддержки модификации. Таким образом, поток представления только для чтения точно такой же, как поток оригинала коллекция.

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

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

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