Iterator versus Stream von Java 8

Lesezeit: 9 Minuten

Benutzer-Avatar
Miguel Gamboa

Um die breite Palette an Abfragemethoden zu nutzen, die in enthalten sind java.util.stream von Jdk 8 habe ich versucht, Domänenmodelle zu entwerfen, bei denen Getter der Beziehung mit * Multiplizität (mit null oder mehr Instanzen) Rückgabe a Stream<T>statt ein Iterable<T> oder Iterator<T>.

Ich bezweifle, dass dadurch zusätzliche Kosten entstehen Stream<T> im Vergleich zum Iterator<T>?

Gibt es also einen Nachteil, wenn ich mein Domänenmodell mit a Stream<T>?

Oder sollte ich stattdessen immer ein zurückgeben Iterator<T> oder Iterable<T>und überlassen Sie dem Endbenutzer die Entscheidung, ob er einen Stream verwenden möchte oder nicht, indem Sie diesen Iterator mit konvertieren StreamUtils?

Notiz dass die Rückkehr a Collection ist keine gültige Option, da in diesem Fall die meisten Beziehungen faul sind und eine unbekannte Größe haben.

  • Ich würde a zurückgeben Collection oder ein Map oder ähnliches und lassen Sie den Client-Code entscheiden, was damit geschehen soll.

    – Luiggi Mendoza

    3. Juli 2015 um 16:15 Uhr


  • Relevant: Soll ich eine Sammlung oder einen Stream zurückgeben?

    – Sotirios Delimanolis

    3. Juli 2015 um 16:23 Uhr


  • Ich vermute, dass Stream etwas langsamer ist; nicht nur wegen seiner Interna, sondern auch, weil es auf der Callsite verwendet wird. Aber ich bezweifle, dass der Leistungsunterschied viel ausmachen würde. Wenn Sie sich auf dieser Ebene um CPU-Zyklen kümmern, ist Iterator auch sehr schrecklich.

    – ZhongYu

    3. Juli 2015 um 16:32 Uhr

  • Ich nehme an, Sie werden auch Kopien für die einzelnen Objekte zurückgeben, selbst wenn Sie einen Stream verwenden.

    – uraimo

    3. Juli 2015 um 16:34 Uhr


  • Ok, wenn die Größe unbekannt ist, Collection kommt nicht in Frage. Streamen ist…

    – ZhongYu

    3. Juli 2015 um 16:41 Uhr

Benutzer-Avatar
Brian Götz

Hier gibt es viele Ratschläge zur Leistung, aber leider sind viele davon Vermutungen und wenig davon weist auf die tatsächlichen Überlegungen zur Leistung hin.

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

Es gibt zwar unzählige Überlegungen, die einen Stream in jedem Fall langsamer, gleich oder schneller als eine andere Form der Traversierung machen können, aber es gibt einige Faktoren, die darauf hindeuten, dass Streams dort einen Leistungsvorteil haben, wo es darauf ankommt – bei großen Datensätze.

Es gibt einen zusätzlichen festen Start-Overhead von Erstellen a Stream im Vergleich zum Erstellen einer Iterator — ein paar weitere Objekte, bevor Sie mit der Berechnung beginnen. Wenn Ihr Datensatz groß ist, spielt es keine Rolle; Es sind kleine Startkosten, die sich über eine Menge Berechnungen amortisieren. (Und wenn Ihr Datensatz klein ist, spielt es wahrscheinlich auch keine Rolle – denn wenn Ihr Programm mit kleinen Datensätzen arbeitet, ist die Leistung im Allgemeinen auch nicht Ihr Hauptanliegen.) Wo dies tut wichtig ist, wenn man parallel geht; jede Zeit, die für die Einrichtung der Pipeline aufgewendet wird, geht in den seriellen Bruchteil von Amdahls Gesetz ein; Wenn Sie sich die Implementierung ansehen, arbeiten wir hart daran, den Objekt-Countdown während der Stream-Einrichtung niedrig zu halten, aber ich würde gerne Wege finden, ihn zu reduzieren, da dies einen direkten Einfluss auf die Break-Even-Datensatzgröße hat, bei der die Parallelität beginnt, sich durchzusetzen sequentiell.

Aber wichtiger als die festen Startkosten sind die Zugriffskosten pro Element. Hier gewinnen Streams tatsächlich – und oft große Gewinne – was einige überraschen mag. (In unseren Leistungstests sehen wir routinemäßig Stream-Pipelines, die ihre for-Schleife übertreffen können Collection Gegenstücke.) Und dafür gibt es eine einfache Erklärung: Spliterator hat grundsätzlich niedrigere Zugriffskosten pro Element als Iterator, sogar nacheinander. Dafür gibt es mehrere Gründe.

  1. Das Iterator-Protokoll ist grundsätzlich weniger effizient. Es erfordert den Aufruf von zwei Methoden, um jedes Element zu erhalten. Außerdem, weil Iteratoren robust gegenüber Dingen wie Aufrufen sein müssen next() ohne hasNext()oder hasNext() mehrfach ohne next(), müssen beide Methoden im Allgemeinen etwas defensives Codieren (und im Allgemeinen mehr Statefulness und Verzweigung) durchführen, was zur Ineffizienz beiträgt. Andererseits ist auch der langsame Weg, einen Splitter zu durchqueren (tryAdvance) hat diese Last nicht. (Es ist noch schlimmer für gleichzeitige Datenstrukturen, weil die next/hasNext Dualität ist grundsätzlich rassig, und Iterator Implementierungen müssen mehr Arbeit leisten, um sich gegen gleichzeitige Änderungen zu verteidigen, als sie tun Spliterator Implementierungen.)

  2. Spliterator bietet ferner eine “Fast-Path”-Iteration — forEachRemaining — die die meiste Zeit verwendet werden kann (Reduktion, forEach), wodurch der Overhead des Iterationscodes weiter reduziert wird, der den Zugriff auf die Interna der Datenstruktur vermittelt. Dies neigt auch dazu, sehr gut zu inlinen, was wiederum die Effektivität anderer Optimierungen wie Codebewegung, Eliminierung von Begrenzungsprüfungen usw. erhöht.

  3. Weiter Traversal via Spliterator neigen dazu, viel weniger Heap-Schreibvorgänge zu haben als with Iterator. Mit Iteratorverursacht jedes Element einen oder mehrere Heap-Schreibvorgänge (es sei denn, die Iterator kann über Escape-Analyse skalarisiert und seine Felder in Register hochgezogen werden.) Unter anderem verursacht dies GC-Kartenmarkierungsaktivität, was zu Cache-Line-Konkurrenz um die Kartenmarkierungen führt. Auf der anderen Seite, Spliterators neigen dazu, weniger Staat und Industriestärke zu haben forEachRemaining Implementierungen neigen dazu, das Schreiben von Daten auf den Heap bis zum Ende des Durchlaufs aufzuschieben und stattdessen den Iterationszustand in Lokalen zu speichern, die natürlich auf Register abgebildet werden, was zu einer verringerten Aktivität des Speicherbusses führt.

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

  • Ich denke, die Verwendung der Stream-API mit zusätzlichen Lambda-involvierenden Operationen wie filter oder map könnte im Vergleich zu pre-java-8 langsamer sein for(Obj obj : collection) {...} Schleife, da funktionale Schnittstellen verwendet werden, die viele Implementierungen haben können, daher sollte für jeden Filter-/Mapping-Funktionsaufruf eine megamorphe Suche durchgeführt werden. Na sicher iterator.hasNext/iterator.next Aufrufe können auch megamorph sein, aber die Stream-API kann mehr Schritte enthalten, einschließlich intern erstellter Lambdas, die auch dieselben funktionalen Schnittstellen implementieren.

    – Tagir Walejew

    4. Juli 2015 um 9:10 Uhr

  • @TagirValeev Klingt nach Raten? Jedenfalls klingt es für mich nicht richtig. Sobald wir die Terminaloperation inline durchlaufen, werden alle Aufrufseiten wieder monomorph (aufgrund der Typenschärfung und des Class-per-Lambda-Übersetzungsschemas).

    – Brian Götz

    5. Juli 2015 um 3:39 Uhr

  • Meine Vermutung wird durch die Tatsache unterstützt +PrintInlining sagt immer java.util.stream.AbstractPipeline::evaluate (94 bytes) callee is too largedaher scheint es, dass praktisch fast jede Terminaloperation nicht in die Aufrufermethode eingebunden werden kann.

    – Tagir Walejew

    5. Juli 2015 um 3:48 Uhr

  • Hier ist eine einfache Benchmark Dies zeigt, dass frühere Filter im Setup den einfachen Stream-Vorgang je nach JVM-Version und Aufgabengröße um 15-35 % verlangsamen. Beachten Sie, dass der Splitter hier monomorph ist (ArraySpliterator). Daher denke ich, dass die megamorphe Suche hier tatsächlich wichtig ist. Haben Sie eine andere Erklärung für die Ergebnisse?

    – Tagir Walejew

    5. Juli 2015 um 16:59 Uhr

Benutzer-Avatar
Holger

Vergleichen wir die übliche Operation der Iteration über alle Elemente, wobei wir davon ausgehen, dass die Quelle eine ist 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 was am Ende anrufen wird 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;
    }
    

Wie Sie sehen können, ist die innere Schleife des Implementierungscodes, in der diese Operationen enden, im Grunde dieselbe, sie wird über Indizes iteriert und das Array direkt gelesen und das Element an die übergeben Consumer.

Ähnliches gilt für alle Standardsammlungen der JRE, alle haben angepasste Implementierungen für alle Möglichkeiten, dies zu tun, selbst wenn Sie einen Nur-Lese-Wrapper verwenden. Im letzteren Fall ist die Stream API würde sogar leicht gewinnen, Collection.forEach muss in der schreibgeschützten Ansicht aufgerufen werden, um an die ursprünglichen Sammlungen zu delegieren forEach. Ebenso muss der Iterator umschlossen werden, um vor Versuchen zu schützen, die aufzurufen remove() Methode. Im Gegensatz, spliterator() kann die ursprüngliche Sammlung direkt zurückgeben Spliterator da es keine Modifikationsunterstützung hat. Daher ist der Stream einer schreibgeschützten Ansicht genau derselbe wie der Stream der ursprünglichen Sammlung.

Obwohl all diese Unterschiede bei der Messung der realen Leistung kaum zu bemerken sind, wie gesagt, die innere Schleifewas am leistungsrelevantesten ist, ist in allen Fällen gleich.

Die Frage ist, welche Schlüsse daraus zu ziehen sind. Sie können immer noch eine schreibgeschützte Wrapperansicht an die ursprüngliche Sammlung zurückgeben, da der Aufrufer immer noch aufrufen kann stream().forEach(…) um direkt im Kontext der ursprünglichen Sammlung zu iterieren.

Da sich die Leistung nicht wirklich unterscheidet, sollten Sie sich eher auf das Design auf höherer Ebene konzentrieren, wie in „Soll ich eine Sammlung oder einen Stream zurückgeben?“ besprochen.

  • Berufung trysplit() on collection gibt keine parallele Verarbeitung, wir verwenden dann wieder 2 Spliteratoren, um nacheinander zu verarbeiten, also wie ermöglicht Spliterator die parallele Verarbeitung, wenn es um die Sammlung geht?

    – amarnath harish

    29. August 2018 um 6:44 Uhr

  • @amarnathharish das liegt völlig außerhalb des Bereichs dieser Frage, aber gut, wenn Sie nach dem Anruf zwei Spliter haben trySplitverarbeiten Sie jede davon sequentiell, aber jede von ihnen in einem anderen Thread, wobei Sie im besten Fall zwei CPU-Kerne verwenden.

    – Holger

    29. August 2018 um 6:53 Uhr

  • noch eine, also wenn wir etwas codieren müssen, um uns darum zu kümmern, wie es nach dem Iterieren verbunden wird (wie ein Combiner ).

    – amarnath harish

    29. August 2018 um 7:27 Uhr

  • @amarnathharish Ich bin mir nicht sicher, ob Ihr letzter Kommentar eine Frage enthält. Für bestimmte Aufgaben kann eine teure Kombinatorfunktion die parallele Verarbeitung weniger effizient machen als die sequentielle Ausführung. Beachten Sie, dass für Operationen wie collect(toList()) oder collect(joining())die Kosten der Zusammenführung liegen in etwa gleichauf mit dem Gewinn durch die Parallelverarbeitung (im besten Fall), sodass Sie kaum einen Vorteil sehen werden someCollection.parallelStream().collect(toList()) mit der aktuellen Umsetzung. Sie benötigen also zusätzliche erhebliche Zwischenoperationen, um in diesem Beispiel von der Parallelität zu profitieren.

    – Holger

    29. August 2018 um 7:56 Uhr

1098810cookie-checkIterator versus Stream von Java 8

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy