Beschleunigung der Erstellung von ConcurrentReferenceHashMap

Grüße, in diesem Artikel werde ich diskutieren, wie Sie die Erstellung von org.springframework.util.ConcurrentReferenceHashMap mit geringem Aufwand beschleunigen können.


Möchten Sie die Leistung steigern? Willkommen zurück!


Intelligenz


Wir werden natürlich mit Messungen beginnen und versuchen zu verstehen, was genau wir verbessern werden. Nehmen Sie dazu JMH 1.21, JDK 8 und JDK 11 sowie den Async-Profiler .


Um herauszufinden, wie viel Zeit erforderlich ist, um ein leeres Wörterbuch zu erstellen, haben wir eine einfache Erfahrung gemacht:


 @Benchmark public Object original() { return new ConcurrentReferenceHashMap(); } 

Das Profil sieht folgendermaßen aus:


 55.21% 2429743 osuConcurrentReferenceHashMap.calculateShift 20.30% 891404 osuConcurrentReferenceHashMap$Segment.<init> 8.79% 387198 osuConcurrentReferenceHashMap.<init> 3.35% 147651 java.util.concurrent.locks.ReentrantLock.<init> 2.34% 102804 java.lang.ref.ReferenceQueue.<init> 1.61% 70748 osuConcurrentReferenceHashMap.createReferenceManager 1.53% 67265 osuConcurrentReferenceHashMap$Segment.createReferenceArray 0.78% 34493 java.lang.ref.ReferenceQueue$Lock.<init> 0.76% 33546 osuConcurrentReferenceHashMap$ReferenceManager.<init> 0.36% 15948 osuAssert.isTrue 

Die Richtung ist klar, Sie können fortfahren.


Mathe


Wir verbringen also den Löwenanteil der Zeit mit der calculateShift Methode. Da ist er:


 protected static int calculateShift(int minimumValue, int maximumValue) { int shift = 0; int value = 1; while (value < minimumValue && value < maximumValue) { value <<= 1; shift++; } return shift; } 

Es ist schwer, sich etwas Neues auszudenken. Wechseln wir also zur Verwendung:


 public ConcurrentReferenceHashMap(/*...*/ int concurrencyLevel, /*...*/) { //... this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); //... } // ConcurrentReferenceHashMap$Segment public Segment(int initialCapacity) { this.referenceManager = createReferenceManager(); this.initialSize = 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE); this.references = createReferenceArray(this.initialSize); this.resizeThreshold = (int) (this.references.length * getLoadFactor()); } 

Beachten Sie die Verwendung des Segment :


 int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); //... for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } 

Der Wert von roundedUpSegmentCapacity beim Durchlaufen der Schleife konstant, daher ist der im 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE) ausgeführte Ausdruck 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE) ebenfalls immer konstant. Somit können wir den angegebenen Ausdruck außerhalb des Konstruktors und der Schleife verwenden.


Die gleiche Anweisung gilt für den Ausdruck (int) (this.references.length * getLoadFactor()) , da das references mit der Variablen initialCapacity wird und seine Größe beim Erstellen jedes Segments konstant ist. Ziehen Sie den Ausdruck aus den Grenzen von Konstruktor und Schleife heraus.


Arrays


Betrachten Sie die Methode createReferenceArray :


 private Reference<K, V>[] createReferenceArray(int size) { return (Reference<K, V>[]) Array.newInstance(Reference.class, size); } 

Die Verwendung von Array::newInstance eindeutig redundant. Nichts hindert uns daran, ein Array mit dem Konstruktor zu erstellen:


 private Reference<K, V>[] createReferenceArray(int size) { return new Reference[size]; } 

Die Leistung des Konstruktors ist dem Aufruf von Array::newInstance auf C2-Ebene nicht unterlegen, übertrifft sie jedoch bei kleinen Arrays in C1-Modi ( -XX:TieredStopAtLevel=1 Eigenschaft -XX:TieredStopAtLevel=1 ) und dem Interpreter ( -Xint Eigenschaft) -Xint :


 //C2 length Mode Cnt Score Error Units constructor 10 avgt 50 5,6 ± 0,0 ns/op constructor 100 avgt 50 29,7 ± 0,1 ns/op constructor 1000 avgt 50 242,7 ± 1,3 ns/op newInstance 10 avgt 50 5,5 ± 0,0 ns/op newInstance 100 avgt 50 29,7 ± 0,1 ns/op newInstance 1000 avgt 50 249,3 ± 9,6 ns/op //C1 length Mode Cnt Score Error Units constructor 10 avgt 50 6,8 ± 0,1 ns/op constructor 100 avgt 50 36,3 ± 0,6 ns/op constructor 1000 avgt 50 358,6 ± 6,4 ns/op newInstance 10 avgt 50 91,0 ± 2,4 ns/op newInstance 100 avgt 50 127,2 ± 1,8 ns/op newInstance 1000 avgt 50 322,8 ± 7,2 ns/op //-Xint length Mode Cnt Score Error Units constructor 10 avgt 50 126,3 ± 5,9 ns/op constructor 100 avgt 50 154,7 ± 2,6 ns/op constructor 1000 avgt 50 364,2 ± 6,2 ns/op newInstance 10 avgt 50 251,2 ± 11,3 ns/op newInstance 100 avgt 50 287,5 ± 11,4 ns/op newInstance 1000 avgt 50 486,5 ± 8,5 ns/op 

Der Austausch wirkt sich nicht auf unseren Benchmark aus, beschleunigt jedoch den Code beim Start der Anwendung, wenn C2 noch nicht funktioniert hat. Mehr über diesen Modus erfahren Sie am Ende des Artikels.


Wichtige Kleinigkeiten


Wenden wir uns noch einmal dem Konstruktor ConcurrentReferenceHashMap


 ConcurrentReferenceHashMap(/*...*/) { Assert.isTrue(initialCapacity >= 0, "Initial capacity must not be negative"); Assert.isTrue(loadFactor > 0f, "Load factor must be positive"); Assert.isTrue(concurrencyLevel > 0, "Concurrency level must be positive"); Assert.notNull(referenceType, "Reference type must not be null"); this.loadFactor = loadFactor; this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); int size = 1 << this.shift; this.referenceType = referenceType; int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); this.segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } } 

Von einem für uns merkwürdigen: Das Ersetzen von Array.newInstance durch einen Konstruktor führt zu einem Kompilierungsfehler, den wir passieren. Aber der Zyklus ist sehr interessant, oder vielmehr die Attraktivität für das segments . Um zu sehen, wie verheerend (manchmal) Leistung sein kann, kann ein solcher Appell von Nitzan Wakarts Artikel The volatile read suprise empfohlen werden .


Der im Artikel beschriebene Fall korreliert meines Erachtens mit dem fraglichen Code. Segmente im Fokus:


 this.segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } 

Unmittelbar nach dem Erstellen des Arrays wird es in das Feld ConcurrentReferenceHashMap.segments , und mit diesem Feld interagiert die Schleife. Innerhalb des Segmentkonstruktors gibt es einen Datensatz in den Volatilitätsfeldreferenzen:


 private volatile Reference<K, V>[] references; public Segment(int initialCapacity) { //... this.references = createReferenceArray(this.initialSize); //... } 

Dies bedeutet, dass es unmöglich ist, den Zugriff auf das segments zu verbessern, mit anderen Worten, sein Inhalt wird bei jeder Umdrehung des Zyklus ausgelesen. Wie kann man die Richtigkeit dieser Aussage überprüfen? Am einfachsten ist es, den Code in ein separates Paket zu kopieren und volatile aus der Deklaration des Segment.references entfernen:


 protected final class Segment extends ReentrantLock { //  private volatile Reference<K, V>[] references; //  private Reference<K, V>[] references; } 

Überprüfen Sie, ob sich etwas geändert hat:


 @Benchmark public Object original() { return new tsypanov.map.original.ConcurrentReferenceHashMap(); } @Benchmark public Object nonVolatileSegmentReferences() { return new tsypanov.map.nonvolatile.ConcurrentReferenceHashMap(); } 

Wir finden signifikante Leistungssteigerungen (JDK 8):


 Benchmark Mode Cnt Score Error Units original avgt 100 732,1 ± 15,8 ns/op nonVolatileSegmentReferences avgt 100 610,6 ± 15,4 ns/op 

Bei JDK 11 wurde der Zeitaufwand reduziert, aber die relative Lücke blieb nahezu unverändert:


 Benchmark Mode Cnt Score Error Units original avgt 100 473,8 ± 11,2 ns/op nonVolatileSegmentReferences avgt 100 401,9 ± 15,5 ns/op 

Natürlich müssen volatile an den Ort zurückgebracht werden und nach einem anderen Weg suchen. Ein Engpass wird entdeckt - dies ist ein Appell an das Feld. Wenn ja, können Sie die Segmentvariable erstellen, das Array füllen und erst dann in das Feld schreiben:


 Segment[] segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < segments.length; i++) { segments[i] = new Segment(roundedUpSegmentCapacity); } this.segments = segments; 

Infolgedessen wurde trotz derart einfacher Verbesserungen ein gutes Wachstum erzielt:


Jdk 8


 Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 712,1 ± 7,2 ns/op patchedConcurrentReferenceHashMap avgt 100 496,5 ± 4,6 ns/op 

Jdk 11


 Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 536,0 ± 8,4 ns/op patchedConcurrentReferenceHashMap avgt 100 486,4 ± 9,3 ns/op 

Was bedeutet es, 'Arrays :: newInstance' durch 'new T []' zu ersetzen?


Beim Starten der Spring Booth-Anwendung von Idea setzen Entwickler häufig das Flag " -XX:TieredStopAtLevel=1 -noverify ", wodurch den VM-Argumenten -XX:TieredStopAtLevel=1 -noverify , wodurch der Start durch Deaktivieren von Profiling und C2 beschleunigt wird. Lassen Sie uns eine Messung mit den angegebenen Argumenten durchführen:


 // JDK 8 -XX:TieredStopAtLevel=1 -noverify Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 1920,9 ± 24,2 ns/op patchedConcurrentReferenceHashMap avgt 100 592,0 ± 25,4 ns/op // JDK 11 -XX:TieredStopAtLevel=1 -noverify Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 1838,9 ± 8,0 ns/op patchedConcurrentReferenceHashMap avgt 100 549,7 ± 6,7 ns/op 

Mehr als 3-fache Steigerung!


Wofür ist das?


Dies ist insbesondere erforderlich, um Abfragen zu beschleunigen, die Projektionen in Spring Data JPA zurückgeben.



Das JMC-Profil zeigt, dass das Erstellen einer ConcurrentReferenceHashMap fast ein Fünftel der Zeit für die Ausführung einer Abfrage des Formulars benötigt


 public interface SimpleEntityRepository extends JpaRepository<SimpleEntity, Long> { List<HasIdAndName> findAllByName(String name); } 

Dabei ist HasIdAndName eine Ansichtsprojektion


 public interface HasIdAndName { int getId(); String getName(); } 

Außerdem wird ConcurrentReferenceHashMap im Spring-Code mehrmals verwendet, sodass es definitiv nicht überflüssig wird.


Schlussfolgerungen


  • Die Verbesserung der Leistung ist nicht so schwierig, wie es auf den ersten Blick scheint
  • Der flüchtige Zugang in der Nähe des Zyklus ist einer der möglichen Engpässe
  • Suchen Sie nach Invarianten und nehmen Sie sie aus den Zyklen heraus

Was zu lesen


Artikel von Nitzan Wakart


Codebeispiel


Änderungen:
https://github.com/spring-projects/spring-framework/pull/1873
https://github.com/spring-projects/spring-framework/pull/2051

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


All Articles