Leistung ruinieren

Dieser Hinweis ist eine schriftliche Version meines Berichts „So ruinieren Sie die Leistung mit ineffizientem Code“ von der JPoint 2018-Konferenz. Sie können Videos und Folien auf der Konferenzseite ansehen. Im Zeitplan ist der Bericht mit einem anstößigen Glas Smoothie gekennzeichnet, so dass es nichts Super-Kompliziertes gibt, dies ist eher für Anfänger.


Gegenstand des Berichts:


  • wie man den Code betrachtet, um Engpässe darin zu finden
  • gemeinsame Antimuster
  • nicht offensichtlicher Rechen
  • Rechen Bypass

Am Rande wiesen sie auf einige Ungenauigkeiten / Auslassungen im Bericht hin, die hier vermerkt sind. Kommentare sind ebenfalls willkommen.


Auswirkungen der Leistung auf die Leistung


Es gibt eine Benutzerklasse:


class User { String name; int age; } 

Wir müssen die Objekte miteinander vergleichen, also deklarieren wir die Methoden equals und hashCode :


 import lombok.EqualsAndHashCode; @EqualsAndHashCode class User { String name; int age; } 

Der Code ist funktionsfähig, die Frage ist anders: Wird die Leistung dieses Codes die beste sein? Um dies zu beantworten, erinnern wir uns an die Funktionen der Object::equals Methode: Sie gibt nur dann ein positives Ergebnis zurück, wenn alle verglichenen Felder gleich sind, andernfalls ist das Ergebnis negativ. Mit anderen Worten, ein Unterschied reicht bereits für ein negatives Ergebnis.


Nachdem wir uns den für @EqualsAndHashCode generierten Code @EqualsAndHashCode wir @EqualsAndHashCode :


 public boolean equals(Object that) { //... if (name == null && that.name != null) { return false; } if (name != null && !name.equals(that.name)) { return false; } return age == that.age; } 

Die Reihenfolge der Überprüfung der Felder entspricht der Reihenfolge ihrer Deklaration, was in unserem Fall nicht die beste Lösung ist, da der Vergleich von Objekten mit equals „schwieriger“ ist als der Vergleich einfacher Typen.


Ok, versuchen wir, mit der Idee equals/hashCode Methoden zu erstellen:


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(name, that.name); } 

Eine Idee erstellt intelligenteren Code, der die Komplexität des Vergleichs verschiedener Datentypen kennt. Nun, wir werden @EqualsAndHashCode und explizit equals/hashCode @EqualsAndHashCode schreiben. Nun wollen wir sehen, was passiert, wenn die Klasse erweitert wird:


 class User { List<T> props; String name; int age; } 

Neuerstellung equals/hashCode :


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(props, that.props) // <---- && Objects.equals(name, that.name); } 

Listen werden verglichen, bevor Zeichenfolgen verglichen werden. Dies ist nicht sinnvoll, wenn Zeichenfolgen unterschiedlich sind. Auf den ersten Blick gibt es keinen großen Unterschied, da Zeichenfolgen gleicher Länge durch Vorzeichen verglichen werden (d. H. Die Vergleichszeit wächst mit der Länge der Zeichenfolge):


Es gab eine Ungenauigkeit

Die Methode java.lang.String::equals ist aufdringlich , sodass bei der Ausführung kein Anmeldevergleich stattfindet.


 //java.lang.String public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { // <---- if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } 

Vergleichen Sie nun zwei ArrayList (als am häufigsten verwendete Listenimplementierung). ArrayList wir ArrayList , stellen wir überrascht fest, dass es keine eigene Implementierung von equals , sondern eine geerbte Implementierung verwendet:



 //AbstractList::equals public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof List)) { return false; } ListIterator<E> e1 = listIterator(); ListIterator<?> e2 = ((List<?>) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { // <---- E o1 = e1.next(); Object o2 = e2.next(); if (!(o1 == null ? o2 == null : o1.equals(o2))) { return false; } } return !(e1.hasNext() || e2.hasNext()); } 

Wichtig ist hier die Erstellung von zwei Iteratoren und deren paarweiser Durchgang. Angenommen, es gibt zwei ArrayList :


  • in einer Zahl von 1 bis 99
  • in der zweiten Zahl von 1 bis 100

Im Idealfall würde es ausreichen, die Größen der beiden Listen zu vergleichen und, wenn sie nicht übereinstimmen, sofort ein negatives Ergebnis zurückzugeben (wie dies bei AbstractSet Fall ist). In der Realität werden 99 Vergleiche durchgeführt, und erst am hundertsten wird klar, dass die Listen unterschiedlich sind.


Was gibt es mit den Kotliniten?


 data class User(val name: String, val age: Int); 

Hier ist alles wie bei einem Lombok - die Vergleichsreihenfolge entspricht der Reihenfolge der Ankündigung:


 public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof User) { User u = (User) o; if (Intrinsics.areEqual(name, u.name) && age == u.age) { // <---- return true; } } return false; } 

Wie kann man die Situation verbessern? - beschwere dich!

IDEA-170178 Der Vergleich von Sammlungen sollte ganz am Ende von IDEA-generierten equals () ausgeführt werden.
https://youtrack.jetbrains.com/issue/IDEA-170178


Neuordnung des Vergleichs in @EqualsAndHashCode für eine bessere Leistung # 1543
https://github.com/rzwitserloot/lombok/issues/1543


KT-23184 Automatisch generierte Gleichungen () der Datenklasse sind nicht optimal
https://youtrack.jetbrains.com/issue/KT-23184


Um dieses Problem zu umgehen, können Sie Felddeklarationen manuell organisieren.


Lassen Sie uns die Aufgabe komplizieren


 void check(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); boolean valid = dto.isValid(); if (valid && entity.hasGoodRating()) { // <---- //do smth } } 

Der Code beinhaltet den Zugriff auf die Datenbank, auch wenn das Ergebnis der Überprüfung der durch den Pfeil angegebenen Bedingungen im Voraus vorhersehbar ist. Wenn der Wert der valid Variablen false ist, wird der Code im if Block niemals ausgeführt. Dies bedeutet, dass Sie auf eine Anforderung verzichten können:


 void check(Dto dto) { boolean valid = dto.isValid(); if (valid && hasGoodRating(dto)) { //do smth } } //       ,    boolean hasGoodRating(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); return entity.hasGoodRating(); } 

Hinweis von der Seitenlinie

Das Sinken kann unbedeutend sein, wenn sich die von JpaRepository::findOne Entität bereits im Cache der ersten Ebene befindet - dann erfolgt keine Anforderung.


Ein ähnliches Beispiel ohne explizite Verzweigung:


 boolean checkChild(Dto dto) { Long id = dto.getId(); Entity entity = jpaRepository.findOne(id); return dto.isValid() && entity.hasChild(); } 

Durch eine schnelle Rücksendung können Sie die Anfrage verzögern:


 boolean checkChild(Dto dto) { if (!dto.isValid()) { return false; } return jpaRepository.findOne(dto.getId()).hasChild(); } 

Eine ziemlich offensichtliche Ergänzung, die nicht im Bericht enthalten war

Stellen Sie sich vor, dass eine bestimmte Prüfung eine ähnliche Entität verwendet:


 @Entity class ParentEntity { @ManyToOne(fetch = LAZY) @JoinColumn(name = "CHILD_ID") private ChildEntity child; @Enumerated(EnumType.String) private SomeType type; 

Wenn die Prüfung dieselbe Entität verwendet, sollten Sie sicherstellen, dass der Aufruf der "faulen" untergeordneten Entitäten / Sammlungen nach dem Aufruf der bereits geladenen Felder ausgeführt wird. Auf den ersten Blick hat eine zusätzliche Anforderung keinen wesentlichen Einfluss auf das Gesamtbild, aber alles kann sich ändern, wenn eine Aktion in einer Schleife ausgeführt wird.


Schlussfolgerung: Die Aktions- / Überprüfungsketten sollten in der Reihenfolge der zunehmenden Komplexität der einzelnen Vorgänge angeordnet werden. Möglicherweise müssen einige davon nicht durchgeführt werden.


Zyklen und Massenverarbeitung


Das folgende Beispiel benötigt keine besonderen Erklärungen:


 @Transactional void enrollStudents(Set<Long> ids) { for (Long id : ids) { Student student = jpaRepository.findOne(id); // <---- O(n) enroll(student); } } 

Aufgrund mehrerer Datenbankabfragen ist der Code langsam.


Bemerkung

Die Leistung kann noch weiter sinken, wenn die Methode enrollStudents außerhalb einer Transaktion ausgeführt wird: Jeder Aufruf von osdjrJpaRepository::findOne wird in einer neuen Transaktion ausgeführt (siehe SimpleJpaRepository ). Dies bedeutet, dass eine Verbindung zur Datenbank empfangen und zurückgegeben sowie der Cache der ersten Ebene erstellt und osdjrJpaRepository::findOne wird.


Fix:


 @Transactional void enrollStudents(Set<Long> ids) { if (ids.isEmpty()) { return; } for (Student student : jpaRepository.findAll(ids)) { enroll(student); } } 

Messen wir die Laufzeit (in Mikrosekunden) für eine Sammlung von Schlüsseln (10 und 100 Stück).


Benchmark


Bemerkung

Wenn Sie Oracle verwenden und mehr als 1000 Schlüssel an findAll , wird die Ausnahme ORA-01795: maximum number of expressions in a list is 1000 .
Außerdem kann das Ausführen einer schweren (mit vielen Schlüsseln) Abfrage schlechter sein als n Abfragen. Es hängt alles von der spezifischen Anwendung ab, sodass der mechanische Austausch des Zyklus zur Massenverarbeitung die Leistung beeinträchtigen kann.


Ein komplexeres Beispiel zum gleichen Thema


 for (Long id : ids) { Region region = jpaRepository.findOne(id); if (region == null) { // <----  region = new Region(); region.setId(id); } use(region); } 

In diesem Fall können wir die Schleife nicht durch JpaRepository::findAll , JpaRepository::findAll dies die Logik JpaRepository::findAll von JpaRepository::findAll erhaltenen Werte sind nicht null und der if Block funktioniert nicht.


Die Tatsache, dass wir für jeden Datenbankschlüssel diese Schwierigkeit beheben können
Gibt entweder den tatsächlichen Wert oder dessen Abwesenheit zurück. Das heißt, in gewissem Sinne ist eine Datenbank ein Wörterbuch. Java aus der Box gibt uns eine vorgefertigte Implementierung des Wörterbuchs - HashMap - auf der wir die Logik zum Ersetzen der Datenbank aufbauen werden:


 Map<Long, Region> regionMap = jpaRepository.findAll(ids) .stream() .collect(Collectors.toMap(Region::getId, Function.identity())); for (Long id : ids) { Region region = map.get(id); if (region == null) { region = new Region(); region.setId(id); } use(region); } 

Beispiel umkehren


 // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

Dieser Code erstellt immer eine neue Transaktion, um eine Liste von Entitäten zu speichern. Das Absacken beginnt mit mehreren Aufrufen einer Methode, die eine neue Transaktion öffnet:


 //   @Transactional public void audit(List<AuditDto> inserts) { inserts.map(this::toEntities).forEach(saver::save); // <---- } // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) // <---- public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

Lösung: Wenden Sie die Saver::save Methode sofort auf den gesamten Datensatz an:


 @Transactional public void audit(List<AuditDto> inserts) { List<AuditEntity> bulk = inserts .map(this::toEntities) .flatMap(List::stream) // <---- .collect(toList()); saver.save(bulk); } 

Viele Transaktionen verschmelzen zu einer, was zu einer spürbaren Zunahme führt (Zeit in Mikrosekunden):


Benchmark


Ein Beispiel mit mehreren Transaktionen ist schwer zu formalisieren, was nicht über den Aufruf von JpaRepository::findOne in einer Schleife gesagt werden kann.


Deshalb werden wir Maßnahmen ergreifen

https://youtrack.jetbrains.com/issue/IDEA-165730
IDEA-165730 Warnung vor ineffizienter Verwendung von JpaRepository


https://youtrack.jetbrains.com/issue/IDEA-165942
IDEA-165942 Inspektion zum Ersetzen des Methodenaufrufs in einer Schleife durch Massenoperation
Behoben in 2017.1


Der Ansatz ist nicht nur auf die Datenbank anwendbar, daher ging Tagir lany Valeev noch weiter. Und wenn wir früher so geschrieben haben:


 List<Long> list = new ArrayList<>(); for (Long id : items) { list.add(id); } 

und alles war in Ordnung, jetzt schlägt die "Idee" vor, sich selbst zu korrigieren:


 List<Long> list = new ArrayList<>(); list.addAll(items); 

Aber selbst diese Option erfüllt sie nicht immer, da Sie sie noch kürzer und schneller machen können:


 List<Long> list = new ArrayList<>(items); 

Vergleichen (Zeit in ns)

Für ArrayList bedeutet diese Verbesserung eine spürbare Steigerung:



Für HashSet ist es nicht so rosig:



Benchmark


Nützlich für den Entwickler:

https://youtrack.jetbrains.com/issue/IDEA-138456
IDEA-138456 Neue Inspektion: Collection.addAll () kann durch parametrisierten Konstruktor ersetzt werden
Behoben in 142.1217


https://youtrack.jetbrains.com/issue/IDEA-178761
IDEA-178761 Die Inspektion 'Collection.addAll (), die durch einen parametrisierten Konstruktor ersetzt werden kann' sollte standardmäßig aktiviert sein
Behoben in 2017.3


Entfernen aus der ArrayList


 for (int i = from; i < to; i++) { list.remove(from); } 

Das Problem liegt in der Implementierung der List::remove Methode:


 public E remove(int index) { Objects.checkIndex(index, size); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(array, index + 1, array, index, numMoved); // <---- } array[--size] = null; // clear to let GC do its work return oldValue; } 

Lösung:


 list.subList(from, to).clear(); 

Was aber, wenn der Remote-Wert im Quellcode verwendet wird?


 for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed); } 

Jetzt müssen Sie zuerst die bereinigte Liste durchgehen:


 List<String> removed = list.subList(from, to); removed.forEach(this::use); removed.clear(); 

Wenn Sie wirklich im Zyklus löschen möchten, hilft eine Änderung der Durchgangsrichtung durch die Liste, die Schmerzen zu lindern. Seine Bedeutung ist es, eine kleinere Anzahl von Elementen nach dem Reinigen der Zelle zu verschieben:


 //   , . .       for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed, i); } //  , . .    for (int i = to - 1; i >= from; i--) { E removed = list.remove(i); use(removed, reverseIndex(i)); } 

Vergleichen Sie alle drei Methoden (unter den Spalten sind% entfernte Elemente aus einer Liste der Größe 100):


Java 8


Java 9


Hat übrigens jemand die Anomalie bemerkt?


Zu sehen


Wenn wir die Hälfte aller Daten löschen, die vom Ende verschoben werden, wird immer das letzte Element gelöscht und es gibt keine Verschiebung:


 // ArrayList public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { // <----     System.arraycopy(elementData, index+1, elementData, index, numMoved); } elementData[--size] = null; // clear to let GC do its work return oldValue; } 

Benchmark


Nützlich für den Entwickler

https://youtrack.jetbrains.com/issue/IDEA-177466
IDEA-177466 In einer Schleife aufgerufene List.remove (Index) erkennen
Behoben in 2018.2


Schlussfolgerung: Massenoperationen sind oft schneller als Einzeloperationen.


Umfang und Leistung


Dieser Code benötigt keine besonderen Erklärungen:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); List<Student> underAchieving = repository.findUnderAchieving(); // <---- if (settings.leaveBothCategories()) { leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

Wir schränken den Bereich ein, der minus 1 Abfrage ergibt:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); if (Settings.leaveBothCategories()) { List<Student> underAchieving = repository.findUnderAchieving(); // <---- leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

Und hier sollte der aufmerksame Leser fragen: Was ist mit statischer Analyse? Warum hat uns Idea nichts über die Verbesserung an der Oberfläche erzählt?


Tatsache ist, dass die Möglichkeiten der statischen Analyse begrenzt sind: Wenn die Methode komplex ist (insbesondere mit der Datenbank interagiert) und den allgemeinen Status beeinflusst, kann die Übertragung ihrer Ausführung die Anwendung beschädigen. Der statische Analysator kann sehr einfache Ausführungen melden, deren Übertragung beispielsweise innerhalb des Blocks nichts unterbricht.


Sie können die variable Hervorhebung als Ersatz verwenden, aber verwenden Sie sie auch hier vorsichtig, da Nebenwirkungen immer möglich sind. Sie können die Annotation @org.jetbrains.annotations.Contract(pure = true) , die in der Jetbrains-Annotations- Bibliothek verfügbar ist, um zustandslose Methoden anzugeben :


 // com.intellij.util.ArrayUtil @Contract(pure = true) public static int find(@NotNull int[] src, int obj) { return indexOf(src, obj); } 

Fazit: Überschüssige Arbeit verschlechtert häufig nur die Leistung.


Das ungewöhnlichste Beispiel


 @Service public class RemoteService { private ContractCounter contractCounter; @Transactional(readOnly = true) // <---- public int countContracts(Dto dto) { if (dto.isInvalid()) { return -1; // <---- } return contractCounter.countContracts(dto); } } 

Diese Implementierung öffnet eine Transaktion, auch wenn keine Transaktion erforderlich ist (schnelle Rückgabe -1 von der Methode).


Alles, was Sie tun müssen, ist, die Transaktionalität innerhalb der ContractCounter::countContracts entfernen, wo sie benötigt wird, und sie aus der "externen" Methode zu entfernen.


Vergleichen Sie die Ausführungszeit für den Fall, dass -1 (ns) zurückgegeben wird:


Vergleichen Sie den Speicherverbrauch (Bytes):


Benchmark


Schlussfolgerung: Controller und "nach außen gerichtete" Dienste müssen von der Transaktionsfähigkeit befreit werden (dies liegt nicht in ihrer Verantwortung), und die gesamte Logik der Überprüfung von Eingabedaten, die keinen Zugriff auf die Datenbank und die Transaktionskomponenten erfordert, sollte dort herausgenommen werden.


Konvertieren Sie Datum / Uhrzeit in Zeichenfolge


Eine der ewigen Aufgaben besteht darin, Datum und Uhrzeit in eine Zeichenfolge umzuwandeln. Vor dem G8 haben wir Folgendes gemacht:


 SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy"); String dateAsStr = formatter.format(date); 

Mit der Veröffentlichung von JDK 8 haben wir LocalDate/LocalDateTime und dementsprechend DateTimeFormatter


 DateTimeFormatter formatter = ofPattern("dd.MM.yyyy"); String dateAsStr = formatter.format(localDate); 

Lassen Sie uns seine Leistung messen:


 Date date = new Date(); LocalDate localDate = LocalDate.now(); SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy"); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy"); @Benchmark public String simpleDateFormat() { return sdf.format(date); } @Benchmark public String dateTimeFormatter() { return dtf.format(localDate); } 

Zeit (ns):


Speicher (Bytes):


Frage: Nehmen wir an, unser Service empfängt Daten von außerhalb und wir können java.util.Date nicht ablehnen. Wäre es für uns von Vorteil, Date in LocalDate zu konvertieren, wenn letzteres schneller in einen String konvertiert wird? Berechnen Sie:


 @Benchmark public String measureDateConverted(Data data) { LocalDate localDate = toLocalDate(data.date); return data.dateTimeFormatter.format(localDate); } private LocalDate toLocalDate(Date date) { return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); } 

Zeit (ns):


Speicher (Bytes):


Daher ist das Konvertierungsdatum -> LocalDate bei Verwendung der "Neun" von Vorteil. Bei G8 verschlingen die Konvertierungskosten alle Vorteile von DateTimeFormatter -a.


Benchmark


Fazit: Nutzen Sie neue Lösungen.


Eine weitere "Acht"


In diesem Code sehen wir offensichtliche Redundanz:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .collect(toList()) // <----    ? .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

Wir entfernen es:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

Mal sehen, wie viel Leistung sich verbessert hat:


Vergleiche mit den neun:


Erstaunlich, oder? Ich habe oben argumentiert, dass übermäßige Arbeit die Leistung beeinträchtigt. Aber hier entfernen wir den Überschuss - und (plötzlich) wird es schlimmer. Um zu verstehen, was passiert, nehmen Sie zwei Iteratoren und betrachten Sie sie unter einer Lupe:


Offenlegen
 Iterator iterator1 = items.stream().collect(toList()).iterator(); Iterator iterator2 = items.stream().iterator(); 


Der erste Iterator ist die reguläre ArrayList$Itr .


Der Durchgang ist einfach:
 public boolean hasNext() { return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) { throw new NoSuchElementException(); } Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } cursor = i + 1; return (E) elementData[lastRet = i]; } 


Der zweite ist interessanter, es ist Spliterators$Adapter , der auf ArrayList$ArrayListSpliterator basiert.


Es ist schwieriger, durchzukommen
 // java.util.Spliterators$Adapter public boolean hasNext() { if (!valueReady) spliterator.tryAdvance(this); return valueReady; } public T next() { if (!valueReady && !hasNext()) throw new NoSuchElementException(); else { valueReady = false; return nextElement; } } 


Schauen wir uns die Iterator-Iteration durch den Async-Profiler an :


 15.64% juArrayList$ArrayListSpliterator.tryAdvance 10.67% jusSpinedBuffer.clear 9.86% juSpliterators$1Adapter.hasNext 8.81% jusStreamSpliterators$AbstractWrappingSpliterator.fillBuffer 6.01% oojiBlackhole.consume 5.71% jusReferencePipeline$3$1.accept 5.57% jusSpinedBuffer.accept 5.06% cllbir.IteratorFromStreamBenchmark.iteratorFromStream 4.80% jlLong.valueOf 4.53% cllbiIteratorFromStreamBenchmark$$Lambda$8.885721577.apply 

Es ist ersichtlich, dass die meiste Zeit damit verbracht wird, den Iterator zu durchlaufen, obwohl wir ihn im Großen und Ganzen nicht benötigen, da die Suche folgendermaßen durchgeführt werden kann:


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

Vergleichen Sie mit dem Rest:


Stream::forEach eindeutig ein Gewinner, aber das ist seltsam: Es basiert immer noch auf ArrayListSpliterator , aber seine Verwendung hat sich erheblich verbessert.


Sehen wir uns das Profil an:
 29.04% oojiBlackhole.consume 22.92% juArrayList$ArrayListSpliterator.forEachRemaining 14.47% jusReferencePipeline$3$1.accept 8.79% jlLong.valueOf 5.37% cllbiIteratorFromStreamBenchmark$$Lambda$9.617691115.accept 4.84% cllbiIteratorFromStreamBenchmark$$Lambda$8.1964917002.apply 4.43% jusForEachOps$ForEachOp$OfRef.accept 4.17% jusSink$ChainedReference.end 1.27% jlInteger.longValue 0.53% jusReferencePipeline.map 

In diesem Profil wird die meiste Zeit damit verbracht, die Werte im Blackhole „schlucken“. Im Vergleich zu einem Iterator wird ein wesentlich größerer Teil der Zeit direkt für die Ausführung des Java-Codes aufgewendet. Es kann angenommen werden, dass der Grund das geringere spezifische Gewicht der Speicherbereinigung im Vergleich zur Iterator-Brute-Force ist. Überprüfen Sie:


 forEach:·gc.alloc.rate.norm 100 avgt 30 216,001 ± 0,002 B/op iteratorFromStream:·gc.alloc.rate.norm 100 avgt 30 416,004 ± 0,006 B/op 

In der Tat liefert Stream::forEach die Hälfte des Speicherverbrauchs.


Warum ist es schneller?

Die Anrufkette vom Anfang bis zum Schwarzen Loch sieht folgendermaßen aus:



Wie Sie sehen können, ist der Aufruf von ArrayListSpliterator::tryAdvance aus der Kette verschwunden, und ArrayListSpliterator::forEachRemaining :


 // ArrayListSpliterator public void forEachRemaining(Consumer<? super E> action) { int i, hi, mc; // hoist accesses and checks from loop ArrayList<E> lst; Object[] a; if (action == null) throw new NullPointerException(); if ((lst = list) != null && (a = lst.elementData) != null) { if ((hi = fence) < 0) { mc = lst.modCount; hi = lst.size; } else mc = expectedModCount; 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; } } throw new ConcurrentModificationException(); } 

Highspeed ArrayListSpliterator::forEachRemaining erreicht, indem in einem Methodenaufruf das gesamte Array ArrayListSpliterator::forEachRemaining . Bei Verwendung eines Iterators ist die Passage auf ein Element beschränkt, sodass wir uns immer gegen ArrayListSpliterator::tryAdvance .
ArrayListSpliterator::forEachRemaining hat Zugriff auf das gesamte Array und ArrayListSpliterator::forEachRemaining mit einem ArrayListSpliterator::forEachRemaining ohne zusätzliche Aufrufe darüber.


Wichtiger Hinweis

Bitte beachten Sie den mechanischen Austausch


 Iterator<Long> iterator = items .stream() .map(Long::valueOf) .collect(toList()) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

auf


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

Dies ist nicht immer gleichwertig, da wir im ersten Fall eine Kopie der Daten für die Passage verwenden, ohne den Stream selbst zu beeinflussen, und im zweiten Fall werden die Daten direkt aus dem Stream entnommen.


Benchmark


Fazit: Seien Sie beim Umgang mit komplexen Darstellungen von Daten darauf vorbereitet, dass selbst die "eisernen" Regeln (zusätzliche Arbeitsschäden) nicht mehr funktionieren. Das obige Beispiel zeigt, dass die scheinbar überflüssige Zwischenliste den Vorteil einer schnelleren Implementierung der Aufzählung bietet.


Zwei Tricks


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays .asList(trace) .subList(0, depth) .toArray(new StackTraceElement[newDepth]); // <---- 

Das erste, was Collection::toArray , ist eine faule "Verbesserung", nämlich das Übergeben eines Arrays mit einer Länge ungleich Null an die Collection::toArray Methode. Es wird ausführlich erklärt, warum dies schädlich ist.


Das zweite Problem ist nicht so offensichtlich, und für sein Verständnis können wir eine Parallele zwischen der Arbeit des Rezensenten und des Historikers ziehen.


Dies ist, was Robin Collingwood darüber schreibt:

, , [] – . . , , ...


. :


1)
2)
3)


, :


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays.copyOf(trace, depth); //    0 //  StackTraceElement[] newTrace = Arrays.copyOfRange(trace, 0, depth); //   0 

():




 List<T> list = getList(); Set<T> set = getSet(); return list.stream().allMatch(set::contains); //     ? 

, , :


 List<T> list = getList(); Set<T> set = getSet(); return set.containsAll(list); 

():




:


 interface FileNameLoader { String[] loadFileNames(); } 

:


 private FileNameLoader loader; void load() { for (String str : asList(loader.loadFileNames())) { // <----   use(str); } } 

, forEach , :


 private FileNameLoader loader; void load() { for (String str : loader.loadFileNames()) { // <----    use(str); } } 

():



https://youtrack.jetbrains.com/issue/IDEA-182206
IDEA-182206 Simplification for Arrays.asList().subList().toArray()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-180847
IDEA-180847 Inspection 'Call to Collection.toArray with zero-length array argument' brings pessimization
2018.1


https://youtrack.jetbrains.com/issue/IDEA-181928
IDEA-181928 Stream.allMatch(Collection::contains) can be simplified to Collection.containsAll()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-184240
IDEA-184240 Unnecessary array-to-collection wrapping should be detected
2018.1


: :



, , , . , : "" ( ), "" ( ), .



Source: https://habr.com/ru/post/de423305/


All Articles