it-swarm.com.de

Iterator vs Stream von Java 8

Um die breite Palette der in Java.util.stream Von Jdk 8 enthaltenen Abfragemethoden zu nutzen, wird versucht, Domänenmodelle zu entwerfen, bei denen Beziehungsstärken mit * - Multiplizität (mit null oder mehr Instanzen) ein Stream<T> Anstelle von Iterable<T> Oder Iterator<T>.

Ich bezweifle, dass durch den Stream<T> Im Vergleich zum Iterator<T> Zusätzlicher Aufwand entsteht.

Gibt es also einen Nachteil, mein Domain-Modell mit einem Stream<T> Zu kompromittieren?

Oder sollte ich stattdessen immer einen Iterator<T> Oder Iterable<T> Zurückgeben und dem Endbenutzer die Entscheidung überlassen, ob ein Stream verwendet werden soll oder nicht, indem ich diesen Iterator mit dem StreamUtils?

Anmerkung dass die Rückgabe eines Collection keine gültige Option ist, da in diesem Fall die meisten Beziehungen faul und mit unbekannter Größe sind.

43
Miguel Gamboa

Hier gibt es viele Tipps zur Leistung, aber leider ist vieles davon eine Vermutung, und wenig davon weist auf die tatsächlichen Leistungsaspekte hin.

@ Holger macht es richtig indem wir darauf hinweisen, dass wir der scheinbar überwältigenden Tendenz widerstehen sollten, den Performance-Schwanz mit dem API-Design-Hund wedeln zu lassen.

Zwar gibt es eine Unmenge von Überlegungen, die einen Stream in jedem Fall langsamer als, genauso wie oder schneller als irgendeine andere Form der Durchquerung machen können, aber es gibt einige Faktoren, die darauf hindeuten, dass Streams einen Leistungsvorteil haben, wenn es darauf ankommt - auf große Datenmengen Datensätze.

Es gibt einen zusätzlichen festen Startaufwand für das Erstellen eines Stream im Vergleich zum Erstellen eines Iterator - einiger weiterer Objekte bevor Sie anfangen zu berechnen. Wenn Ihr Datensatz groß ist, spielt es keine Rolle. Es handelt sich um geringe Startkosten, die über einen hohen Rechenaufwand amortisiert werden. (Und wenn Ihre Datenmenge klein ist, spielt es wahrscheinlich auch keine Rolle - denn wenn Ihr Programm mit kleinen Datenmengen arbeitet, ist die Leistung im Allgemeinen auch nicht Ihr Hauptanliegen.) Dabei ist Ist wichtig, wenn man parallel geht; Jede Zeit, die für die Einrichtung der Pipeline aufgewendet wird, fällt in den seriellen Bruchteil des Amdahlschen Gesetzes. Wenn Sie sich die Implementierung ansehen, arbeiten wir hart daran, die Anzahl der Objekte während der Stream-Einrichtung niedrig zu halten, aber ich würde gerne Möglichkeiten finden, sie zu reduzieren, da dies einen direkten Einfluss auf die Größe des Breakeven-Datensatzes hat, bei dem Parallelität zu gewinnen beginnt sequentiell.

Wichtiger als die festen Startkosten sind jedoch die Zugriffskosten pro Element. Hier gewinnen Streams tatsächlich - und gewinnen oftmals auch sehr - was manche vielleicht überraschen kann. (In unseren Leistungstests sehen wir routinemäßig Stream-Pipelines, die ihre for-Schleife über die Entsprechungen von Collection übertreffen können.) Und es gibt eine einfache Erklärung dafür: Spliterator hat grundsätzlich einen geringeren Zugriff pro Element kostet als Iterator, auch sequentiell. Dafür gibt es mehrere Gründe.

  1. Das Iterator-Protokoll ist grundsätzlich weniger effizient. Es sind zwei Methoden erforderlich, um jedes Element abzurufen. Da Iteratoren für Dinge wie den mehrfachen Aufruf von next() ohne hasNext() oder hasNext() ohne next() robust sein müssen, sind beide Methoden Im Allgemeinen müssen einige defensive Codierungen (und im Allgemeinen mehr Statefulness und Verzweigung) durchgeführt werden, was zu Ineffizienz beiträgt. Andererseits ist auch der langsame Weg, einen Spliterator (tryAdvance) zu durchlaufen, nicht mit dieser Belastung verbunden. (Bei gleichzeitigen Datenstrukturen ist dies sogar noch schlimmer, da die Dualität von next/hasNext im Grunde genommen rassig ist und Iterator Implementierungen mehr Arbeit leisten müssen, um sich gegen gleichzeitige Änderungen zu verteidigen, als dies bei Spliterator Implementierungen.)

  2. Spliterator bietet außerdem eine "schnelle" Iteration - forEachRemaining -, die die meiste Zeit verwendet werden kann (Reduction, forEach), wodurch der Overhead des Iterationscodes, der den Zugriff vermittelt, weiter verringert wird auf die Datenstruktur Interna. Dies neigt auch dazu, sehr gut inline zu sein, was wiederum die Wirksamkeit anderer Optimierungen wie Codebewegung, Beseitigung von Grenzprüfungen usw. erhöht.

  3. Darüber hinaus weisen Traversen über Spliterator in der Regel viel weniger Heap-Schreibvorgänge auf als mit Iterator. Mit Iterator führt jedes Element zu einem oder mehreren Heap-Schreibvorgängen (es sei denn, das Iterator kann über die Escape-Analyse skaliert und seine Felder in Registern abgelegt werden.) Dies führt unter anderem zu einer GC-Kartenmarkierungsaktivität. Dies führt zu einem Cache-Zeilenkonflikt für die Kartenmarkierungen. Andererseits neigen Spliterators dazu, weniger Status zu haben, und Implementierungen mit industrieller Stärke forEachRemaining verschieben das Schreiben von Daten auf den Heap bis zum Ende der Durchquerung, statt dessen Iterationsstatus in lokalen Dateien zu speichern die natürlich Registern zugeordnet sind, was zu einer verringerten Speicherbusaktivität führt.

Fazit: Mach dir keine Sorgen, sei glücklich. Spliterator ist ein besseres Iterator, auch ohne Parallelität. (Sie sind im Allgemeinen auch einfacher zu schreiben und schwerer zu verwechseln.)

57
Brian Goetz

Vergleichen wir den allgemeinen Vorgang des Iterierens über alle Elemente, vorausgesetzt, die Quelle ist ein ArrayList. Dann gibt es drei Standardmethoden, um dies zu erreichen:

  • 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 Welches am Ende Spliterator.forEachRemaining aufruft

    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;
    }
    

Wie Sie sehen, ist die innere Schleife des Implementierungscodes, in der diese Operationen enden, im Grunde dieselbe. Sie durchläuft Indizes, liest das Array direkt und übergibt das Element an Consumer.

Ähnliche Dinge gelten für alle Standardauflistungen der JRE. Alle haben angepasste Implementierungen für alle Möglichkeiten, auch wenn Sie einen schreibgeschützten Wrapper verwenden. In letzterem Fall würde die Stream -API sogar leicht gewinnen. Collection.forEach Muss in der schreibgeschützten Ansicht aufgerufen werden, um an die forEach der ursprünglichen Sammlung zu delegieren. Ebenso muss der Iterator umbrochen werden, um den Aufruf der remove() -Methode zu verhindern. Im Gegensatz dazu kann spliterator() das Spliterator der ursprünglichen Sammlung direkt zurückgeben, da es keine Änderungsunterstützung bietet. Daher ist der Stream einer schreibgeschützten Ansicht genau der gleiche wie der Stream der ursprünglichen Sammlung.

Obwohl all diese Unterschiede bei der Messung der tatsächlichen Leistung kaum zu bemerken sind, ist, wie gesagt, die innere Schleife , die die leistungsrelevanteste Sache ist, die in allen Fällen gleich.

Die Frage ist, welche Schlussfolgerung daraus zu ziehen ist. Sie können weiterhin eine schreibgeschützte Wrapper-Ansicht zur ursprünglichen Sammlung zurückgeben, da der Aufrufer möglicherweise weiterhin stream().forEach(…) aufruft, um direkt im Kontext der ursprünglichen Sammlung zu iterieren.

Da sich die Leistung nicht wirklich unterscheidet, sollten Sie sich eher auf das übergeordnete Design konzentrieren, wie in "Soll ich eine Sammlung oder einen Stream zurückgeben?"

14
Holger